@hasna/hooks 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -12
- package/bin/index.js +2089 -470
- package/bin/serve.js +5248 -0
- package/dist/cf/provision.d.ts +24 -0
- package/dist/config.d.ts +18 -0
- package/dist/db/legacy-import.d.ts +1 -1
- package/dist/db/migrations/004_hooks_table.d.ts +9 -0
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/db/storage-sync.d.ts +26 -6
- package/dist/index.d.ts +18 -2
- package/dist/index.js +5341 -286
- package/dist/lib/custom-install.d.ts +19 -0
- package/dist/lib/manifest.d.ts +70 -0
- package/dist/lib/resolve.d.ts +19 -0
- package/dist/lib/run.d.ts +37 -0
- package/dist/lib/store.d.ts +69 -0
- package/dist/lib/sync.d.ts +35 -0
- package/dist/serve.d.ts +36 -0
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +133 -42
- package/hooks/codewith-native-common.test.ts +1521 -3
- package/hooks/codewith-native-common.ts +1627 -37
- package/hooks/hook-scanoutput/README.md +151 -0
- package/hooks/hook-scanoutput/package.json +12 -0
- package/hooks/hook-scanoutput/src/hook.test.ts +217 -0
- package/hooks/hook-scanoutput/src/hook.ts +319 -0
- package/hooks/mention-context/README.md +109 -0
- package/hooks/mention-context/package.json +9 -0
- package/hooks/mention-context/src/hasna-mention-context.py +1218 -0
- package/hooks/mention-context/src/hasna-mention-warm.py +521 -0
- package/hooks/mention-context/src/hook.test.ts +68 -0
- package/hooks/mention-context/src/test_run_capture.py +345 -0
- package/hooks/pre-bash/README.md +72 -2
- package/hooks/worktree-guard/README.md +9 -2
- package/package.json +9 -5
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom hook install — sources outside the bundled registry.
|
|
3
|
+
*
|
|
4
|
+
* git URL -> shallow clone, read manifest, copy into the custom dir
|
|
5
|
+
* local -> copy a directory containing manifest.json
|
|
6
|
+
* https -> fetch manifest.json (+ relative script file when needed)
|
|
7
|
+
*/
|
|
8
|
+
export type CustomSourceKind = "git" | "local" | "url";
|
|
9
|
+
export interface CustomInstallResult {
|
|
10
|
+
name: string;
|
|
11
|
+
version: string;
|
|
12
|
+
kind: CustomSourceKind;
|
|
13
|
+
source: string;
|
|
14
|
+
dir: string;
|
|
15
|
+
scriptPath: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function isCustomSource(value: string): boolean;
|
|
18
|
+
export declare function installCustomSource(source: string): Promise<CustomInstallResult>;
|
|
19
|
+
export declare function ensureCustomDir(): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook manifest model — the contract for custom/remote hooks.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
export declare const HOOK_NAME_RE: RegExp;
|
|
6
|
+
export declare const manifestSchema: z.ZodObject<{
|
|
7
|
+
name: z.ZodString;
|
|
8
|
+
version: z.ZodString;
|
|
9
|
+
description: z.ZodOptional<z.ZodString>;
|
|
10
|
+
events: z.ZodArray<z.ZodString, "many">;
|
|
11
|
+
script: z.ZodString;
|
|
12
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
13
|
+
timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
version: string;
|
|
16
|
+
events: string[];
|
|
17
|
+
name: string;
|
|
18
|
+
script: string;
|
|
19
|
+
description?: string | undefined;
|
|
20
|
+
args?: string[] | undefined;
|
|
21
|
+
timeout_ms?: number | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
version: string;
|
|
24
|
+
events: string[];
|
|
25
|
+
name: string;
|
|
26
|
+
script: string;
|
|
27
|
+
description?: string | undefined;
|
|
28
|
+
args?: string[] | undefined;
|
|
29
|
+
timeout_ms?: number | undefined;
|
|
30
|
+
}>;
|
|
31
|
+
export type HookManifest = z.infer<typeof manifestSchema>;
|
|
32
|
+
export interface ParsedManifest {
|
|
33
|
+
manifest: HookManifest;
|
|
34
|
+
scriptPath: string;
|
|
35
|
+
scriptContent: string;
|
|
36
|
+
scriptIsInline: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare function parseManifest(json: string): HookManifest;
|
|
39
|
+
export declare function normalizeManifestName(name: string): string;
|
|
40
|
+
export declare function shortManifestName(name: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* A manifest script path resolved outside its hook's own directory.
|
|
43
|
+
* Carries the offending path so callers can surface it verbatim.
|
|
44
|
+
*/
|
|
45
|
+
export declare class ScriptContainmentError extends Error {
|
|
46
|
+
constructor(path: string, hookDir: string, raw?: string);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Full containment check (lexical + symlink resolution). Requires `root` to
|
|
50
|
+
* exist. For write targets that do not exist yet, call
|
|
51
|
+
* assertContainedLexical first (before creating anything), then create the
|
|
52
|
+
* root, then call assertContainedReal.
|
|
53
|
+
*/
|
|
54
|
+
export declare function assertContained(target: string, root: string, raw?: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a manifest's script to (relative path, content).
|
|
57
|
+
* A script value containing a newline is inline content; anything else is a
|
|
58
|
+
* relative path resolved against the manifest directory.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveScript(manifest: HookManifest, manifestDir: string): {
|
|
61
|
+
path: string;
|
|
62
|
+
content: string;
|
|
63
|
+
};
|
|
64
|
+
export declare function customHookDir(name: string): string;
|
|
65
|
+
export declare function readCustomManifest(name: string): ParsedManifest | undefined;
|
|
66
|
+
export declare function listCustomHooks(): ParsedManifest[];
|
|
67
|
+
export declare function writeCustomHook(name: string, manifest: HookManifest, scriptContent: string, scriptRelPath: string): {
|
|
68
|
+
dir: string;
|
|
69
|
+
scriptPath: string;
|
|
70
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook resolution — custom directory first, bundled registry second.
|
|
3
|
+
* A custom hook that collides with a bundled name shadows it.
|
|
4
|
+
*/
|
|
5
|
+
import { type HookMeta } from "./registry.js";
|
|
6
|
+
export type HookSource = "custom" | "bundled";
|
|
7
|
+
export interface ResolvedHook {
|
|
8
|
+
name: string;
|
|
9
|
+
source: HookSource;
|
|
10
|
+
version: string;
|
|
11
|
+
events: string[];
|
|
12
|
+
description: string;
|
|
13
|
+
scriptPath: string;
|
|
14
|
+
meta: HookMeta;
|
|
15
|
+
}
|
|
16
|
+
export declare function resolveHookDir(name: string): string | undefined;
|
|
17
|
+
export declare function resolveScriptPath(name: string): string | undefined;
|
|
18
|
+
export declare function resolveHookMeta(name: string): HookMeta | undefined;
|
|
19
|
+
export declare function resolveHook(name: string): ResolvedHook | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verified hook execution — the verified bytes are the executed bytes.
|
|
3
|
+
*
|
|
4
|
+
* The trust check hashes the script content once; executing a PATH afterwards
|
|
5
|
+
* would re-open the file and could run different bytes (TOCTOU). So the
|
|
6
|
+
* verified content is written to a runner-owned temp file next to the original
|
|
7
|
+
* script and that file is executed. The original path is never opened for
|
|
8
|
+
* execution after verification.
|
|
9
|
+
*
|
|
10
|
+
* The temp file sits in the script's own directory so the hook's relative
|
|
11
|
+
* imports resolve exactly as they do today (bundled hooks import shared
|
|
12
|
+
* modules like ../../../src/lib/db-writer). A temp file in the system tmp dir
|
|
13
|
+
* would break that resolution.
|
|
14
|
+
*/
|
|
15
|
+
export interface VerifiedRunOptions {
|
|
16
|
+
/** Registered hook name (for error messages) */
|
|
17
|
+
name: string;
|
|
18
|
+
/**
|
|
19
|
+
* The hook's on-disk script path. Only its directory and basename are used:
|
|
20
|
+
* the path itself is never re-opened for execution.
|
|
21
|
+
*/
|
|
22
|
+
scriptPath: string;
|
|
23
|
+
/** The verified bytes to execute — exactly what was hashed and trusted */
|
|
24
|
+
content: Buffer;
|
|
25
|
+
/** Args passed through to the hook script */
|
|
26
|
+
args?: string[];
|
|
27
|
+
/** Input passed to the hook on stdin */
|
|
28
|
+
stdin: string;
|
|
29
|
+
env?: Record<string, string | undefined>;
|
|
30
|
+
timeout?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface VerifiedRunResult {
|
|
33
|
+
stdout: string;
|
|
34
|
+
stderr: string;
|
|
35
|
+
exitCode: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function executeVerifiedScript(options: VerifiedRunOptions): Promise<VerifiedRunResult>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook store — SQLite hooks table + hooks.lock pin file.
|
|
3
|
+
*
|
|
4
|
+
* The DB record and the lock pin are written together so a verified hash is
|
|
5
|
+
* never trusted on one surface only. The lock file is the portable pin that a
|
|
6
|
+
* remote registry sync compares against.
|
|
7
|
+
*/
|
|
8
|
+
import type { Database } from "bun:sqlite";
|
|
9
|
+
export interface HookRecord {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
sha256: string;
|
|
14
|
+
source_type: string;
|
|
15
|
+
source_ref: string | null;
|
|
16
|
+
installed_at: string;
|
|
17
|
+
enabled: number;
|
|
18
|
+
last_verified_at: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface LockEntry {
|
|
21
|
+
version: string;
|
|
22
|
+
sha256: string;
|
|
23
|
+
source: string;
|
|
24
|
+
}
|
|
25
|
+
export interface LockFile {
|
|
26
|
+
hooks: Record<string, LockEntry>;
|
|
27
|
+
}
|
|
28
|
+
export declare function sha256Of(input: Buffer | string): string;
|
|
29
|
+
export declare function sha256File(path: string): Promise<string>;
|
|
30
|
+
export declare function getHookRecord(db: Database, name: string): HookRecord | null;
|
|
31
|
+
export declare function listHookRecords(db: Database): HookRecord[];
|
|
32
|
+
export declare function upsertHookRecord(db: Database, record: {
|
|
33
|
+
name: string;
|
|
34
|
+
version: string;
|
|
35
|
+
sha256: string;
|
|
36
|
+
source_type: string;
|
|
37
|
+
source_ref?: string | null;
|
|
38
|
+
enabled?: number;
|
|
39
|
+
last_verified_at?: string | null;
|
|
40
|
+
}): void;
|
|
41
|
+
export declare function removeHookRecord(db: Database, name: string): boolean;
|
|
42
|
+
export declare function readLock(): LockFile;
|
|
43
|
+
export declare function writeLock(lock: LockFile): string;
|
|
44
|
+
export declare function setPinnedHook(name: string, entry: LockEntry): string;
|
|
45
|
+
export declare function getPinnedHook(name: string): LockEntry | undefined;
|
|
46
|
+
export declare function removePinnedHook(name: string): boolean;
|
|
47
|
+
export interface TrustCheck {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
pinned: boolean;
|
|
50
|
+
expected: string | null;
|
|
51
|
+
actual: string;
|
|
52
|
+
name: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check a hook script's content hash against the trusted record.
|
|
56
|
+
* No record exists (first run) => pin the current hash and pass.
|
|
57
|
+
* Record or pin exists and differs => refuse.
|
|
58
|
+
*
|
|
59
|
+
* Takes a precomputed hash so callers can verify the exact bytes they are
|
|
60
|
+
* about to execute (content-based verification), instead of re-reading a path.
|
|
61
|
+
*/
|
|
62
|
+
export declare function checkScriptHash(name: string, actual: string): TrustCheck;
|
|
63
|
+
/**
|
|
64
|
+
* Verify a hook script's content hash against the trusted record, reading the
|
|
65
|
+
* script from disk. Execution paths should prefer checkScriptHash with the
|
|
66
|
+
* bytes they already hold so the verified bytes are the executed bytes.
|
|
67
|
+
*/
|
|
68
|
+
export declare function verifyScriptHash(name: string, scriptPath: string): Promise<TrustCheck>;
|
|
69
|
+
export declare function retrustHook(name: string, scriptPath: string, version: string, source: string): TrustCheck;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hooks sync — reconcile the local store against the remote registry (when an
|
|
3
|
+
* API URL is configured) or the bundled registry (otherwise).
|
|
4
|
+
*
|
|
5
|
+
* Fail-closed: every network read happens before anything is written, so a
|
|
6
|
+
* failure mid-sync leaves the local store untouched. Local-only hooks are
|
|
7
|
+
* never deleted by a remote sync.
|
|
8
|
+
*/
|
|
9
|
+
export interface SyncDiff {
|
|
10
|
+
added: string[];
|
|
11
|
+
updated: string[];
|
|
12
|
+
unchanged: string[];
|
|
13
|
+
skipped: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface SyncPlan {
|
|
16
|
+
apiUrl: string | null;
|
|
17
|
+
dryRun: boolean;
|
|
18
|
+
diff: SyncDiff;
|
|
19
|
+
}
|
|
20
|
+
export interface ArtifactResponse {
|
|
21
|
+
manifest: {
|
|
22
|
+
name: string;
|
|
23
|
+
version: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
events: string[];
|
|
26
|
+
script: string;
|
|
27
|
+
args?: string[];
|
|
28
|
+
timeout_ms?: number;
|
|
29
|
+
};
|
|
30
|
+
script: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function planSync(): Promise<SyncPlan>;
|
|
33
|
+
export declare function syncHooks(options?: {
|
|
34
|
+
dryRun?: boolean;
|
|
35
|
+
}): Promise<SyncPlan>;
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* hooks serve — the local registry HTTP API.
|
|
4
|
+
*
|
|
5
|
+
* Catalog and artifact reads are open; publishing (PUT) requires the API key.
|
|
6
|
+
* The published set is hooks.lock, the same pin file the client syncs from.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_SERVE_PORT = 39428;
|
|
9
|
+
export declare const SERVE_HOST = "127.0.0.1";
|
|
10
|
+
export declare const SERVE_SERVICE_NAME = "hooks-registry";
|
|
11
|
+
export interface CatalogEntry {
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
sha256: string;
|
|
15
|
+
events: string[];
|
|
16
|
+
description: string;
|
|
17
|
+
source: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ArtifactPayload {
|
|
20
|
+
manifest: {
|
|
21
|
+
name: string;
|
|
22
|
+
version: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
events: string[];
|
|
25
|
+
script: string;
|
|
26
|
+
args?: string[];
|
|
27
|
+
timeout_ms?: number;
|
|
28
|
+
};
|
|
29
|
+
script: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function handleServeRequest(req: Request, apiKey: string | undefined): Promise<Response>;
|
|
32
|
+
export declare function startServeServer(options: {
|
|
33
|
+
port?: number;
|
|
34
|
+
host?: string;
|
|
35
|
+
apiKey?: string;
|
|
36
|
+
}): ReturnType<typeof Bun.serve>;
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { HOOKS_STORAGE_ENV, HOOKS_STORAGE_FALLBACK_ENV,
|
|
2
|
-
export type {
|
|
1
|
+
export { HOOKS_STORAGE_ENV, HOOKS_STORAGE_FALLBACK_ENV, HOOKS_STORAGE_BACKEND_ENV, HOOKS_STORAGE_BACKEND_FALLBACK_ENV, HOOKS_STORAGE_TABLES, RETIRED_STORAGE_MODE_ENV, STORAGE_BACKENDS, STORAGE_BACKEND_ENV, STORAGE_DATABASE_ENV, STORAGE_TABLES, getStorageBackend, getStorageDatabaseEnv, getStorageDatabaseEnvName, getStorageDatabaseUrl, getStoragePg, getStorageStatus, getSyncMetaAll, parseStorageTables, resolveTables, runStorageMigrations, storagePull, storagePush, storageSync, } from "./db/storage-sync.js";
|
|
2
|
+
export type { StorageBackend, StorageEnv, StorageStatus, SyncMeta, SyncResult } from "./db/storage-sync.js";
|
|
3
3
|
export { PgAdapterAsync } from "./db/remote-storage.js";
|
|
4
4
|
export { PG_MIGRATIONS } from "./db/pg-migrations.js";
|
package/dist/storage.js
CHANGED
|
@@ -15,12 +15,6 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
|
-
// src/db/index.ts
|
|
19
|
-
import { Database } from "bun:sqlite";
|
|
20
|
-
import { existsSync as existsSync2, mkdirSync, cpSync } from "fs";
|
|
21
|
-
import { join as join2 } from "path";
|
|
22
|
-
import { homedir as homedir2 } from "os";
|
|
23
|
-
|
|
24
18
|
// src/db/schema.ts
|
|
25
19
|
var CREATE_HOOK_EVENTS_TABLE = `
|
|
26
20
|
CREATE TABLE IF NOT EXISTS hook_events (
|
|
@@ -37,14 +31,16 @@ var CREATE_HOOK_EVENTS_TABLE = `
|
|
|
37
31
|
project_dir TEXT,
|
|
38
32
|
metadata TEXT
|
|
39
33
|
)
|
|
40
|
-
|
|
41
|
-
var
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
`, CREATE_INDEXES;
|
|
35
|
+
var init_schema = __esm(() => {
|
|
36
|
+
CREATE_INDEXES = [
|
|
37
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_timestamp ON hook_events (timestamp)`,
|
|
38
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_session_id ON hook_events (session_id)`,
|
|
39
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_hook_name ON hook_events (hook_name)`,
|
|
40
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_event_type ON hook_events (event_type)`,
|
|
41
|
+
`CREATE INDEX IF NOT EXISTS idx_hook_events_errors ON hook_events (timestamp) WHERE error IS NOT NULL`
|
|
42
|
+
];
|
|
43
|
+
});
|
|
48
44
|
|
|
49
45
|
// src/db/migrations/001_initial.ts
|
|
50
46
|
function up(db) {
|
|
@@ -53,6 +49,9 @@ function up(db) {
|
|
|
53
49
|
db.exec(idx);
|
|
54
50
|
}
|
|
55
51
|
}
|
|
52
|
+
var init_001_initial = __esm(() => {
|
|
53
|
+
init_schema();
|
|
54
|
+
});
|
|
56
55
|
|
|
57
56
|
// src/db/migrations/002_session_events.ts
|
|
58
57
|
function up2(db) {
|
|
@@ -79,6 +78,9 @@ function up2(db) {
|
|
|
79
78
|
throw err;
|
|
80
79
|
}
|
|
81
80
|
}
|
|
81
|
+
var init_002_session_events = __esm(() => {
|
|
82
|
+
init_schema();
|
|
83
|
+
});
|
|
82
84
|
|
|
83
85
|
// src/db/migrations/003_user_prompt_submit_event.ts
|
|
84
86
|
function up3(db) {
|
|
@@ -105,13 +107,37 @@ function up3(db) {
|
|
|
105
107
|
throw err;
|
|
106
108
|
}
|
|
107
109
|
}
|
|
110
|
+
var init_003_user_prompt_submit_event = __esm(() => {
|
|
111
|
+
init_schema();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// src/db/migrations/004_hooks_table.ts
|
|
115
|
+
function up4(db) {
|
|
116
|
+
db.exec(CREATE_HOOKS_TABLE);
|
|
117
|
+
for (const idx of CREATE_HOOKS_INDEXES) {
|
|
118
|
+
db.exec(idx);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
var CREATE_HOOKS_TABLE = `
|
|
122
|
+
CREATE TABLE IF NOT EXISTS hooks (
|
|
123
|
+
id TEXT PRIMARY KEY,
|
|
124
|
+
name TEXT NOT NULL,
|
|
125
|
+
version TEXT NOT NULL,
|
|
126
|
+
sha256 TEXT NOT NULL,
|
|
127
|
+
source_type TEXT NOT NULL,
|
|
128
|
+
source_ref TEXT,
|
|
129
|
+
installed_at TEXT NOT NULL,
|
|
130
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
131
|
+
last_verified_at TEXT
|
|
132
|
+
)
|
|
133
|
+
`, CREATE_HOOKS_INDEXES;
|
|
134
|
+
var init_004_hooks_table = __esm(() => {
|
|
135
|
+
CREATE_HOOKS_INDEXES = [
|
|
136
|
+
`CREATE INDEX IF NOT EXISTS idx_hooks_name ON hooks (name)`
|
|
137
|
+
];
|
|
138
|
+
});
|
|
108
139
|
|
|
109
140
|
// src/db/migrations/index.ts
|
|
110
|
-
var MIGRATIONS = [
|
|
111
|
-
{ version: "001_initial", up },
|
|
112
|
-
{ version: "002_session_events", up: up2 },
|
|
113
|
-
{ version: "003_user_prompt_submit_event", up: up3 }
|
|
114
|
-
];
|
|
115
141
|
function ensureMigrationsTable(db) {
|
|
116
142
|
db.exec(`
|
|
117
143
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
@@ -137,12 +163,24 @@ function runMigrations(db) {
|
|
|
137
163
|
]);
|
|
138
164
|
}
|
|
139
165
|
}
|
|
166
|
+
var MIGRATIONS;
|
|
167
|
+
var init_migrations = __esm(() => {
|
|
168
|
+
init_001_initial();
|
|
169
|
+
init_002_session_events();
|
|
170
|
+
init_003_user_prompt_submit_event();
|
|
171
|
+
init_004_hooks_table();
|
|
172
|
+
MIGRATIONS = [
|
|
173
|
+
{ version: "001_initial", up },
|
|
174
|
+
{ version: "002_session_events", up: up2 },
|
|
175
|
+
{ version: "003_user_prompt_submit_event", up: up3 },
|
|
176
|
+
{ version: "004_hooks_table", up: up4 }
|
|
177
|
+
];
|
|
178
|
+
});
|
|
140
179
|
|
|
141
180
|
// src/db/legacy-import.ts
|
|
142
181
|
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
143
182
|
import { join } from "path";
|
|
144
183
|
import { homedir } from "os";
|
|
145
|
-
var META_KEY = "legacy_import_done";
|
|
146
184
|
function ensureMetaTable(db) {
|
|
147
185
|
db.exec(`
|
|
148
186
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
@@ -216,12 +254,12 @@ function importErrorsLog(db, filePath) {
|
|
|
216
254
|
} catch {}
|
|
217
255
|
return count;
|
|
218
256
|
}
|
|
219
|
-
function runLegacyImport(db) {
|
|
257
|
+
function runLegacyImport(db, homeDir = homedir()) {
|
|
220
258
|
try {
|
|
221
259
|
if (isAlreadyDone(db))
|
|
222
260
|
return;
|
|
223
261
|
let total = 0;
|
|
224
|
-
const claudeProjectsDir = join(
|
|
262
|
+
const claudeProjectsDir = join(homeDir, ".claude", "projects");
|
|
225
263
|
if (existsSync(claudeProjectsDir)) {
|
|
226
264
|
try {
|
|
227
265
|
const projectDirs = readdirSync(claudeProjectsDir);
|
|
@@ -251,6 +289,8 @@ function runLegacyImport(db) {
|
|
|
251
289
|
`);
|
|
252
290
|
}
|
|
253
291
|
}
|
|
292
|
+
var META_KEY = "legacy_import_done";
|
|
293
|
+
var init_legacy_import = () => {};
|
|
254
294
|
|
|
255
295
|
// src/db/retention.ts
|
|
256
296
|
function runRetention(db, days) {
|
|
@@ -267,7 +307,10 @@ function runRetention(db, days) {
|
|
|
267
307
|
}
|
|
268
308
|
|
|
269
309
|
// src/db/index.ts
|
|
270
|
-
|
|
310
|
+
import { Database } from "bun:sqlite";
|
|
311
|
+
import { existsSync as existsSync2, mkdirSync, cpSync } from "fs";
|
|
312
|
+
import { join as join2 } from "path";
|
|
313
|
+
import { homedir as homedir2 } from "os";
|
|
271
314
|
function resolveDataDir() {
|
|
272
315
|
const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
|
|
273
316
|
if (explicit)
|
|
@@ -318,6 +361,14 @@ function getDb() {
|
|
|
318
361
|
}
|
|
319
362
|
return instance;
|
|
320
363
|
}
|
|
364
|
+
var instance = null;
|
|
365
|
+
var init_db = __esm(() => {
|
|
366
|
+
init_migrations();
|
|
367
|
+
init_legacy_import();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// src/db/storage-sync.ts
|
|
371
|
+
init_db();
|
|
321
372
|
|
|
322
373
|
// src/db/pg-migrations.ts
|
|
323
374
|
var PG_MIGRATIONS = [
|
|
@@ -402,12 +453,14 @@ var STORAGE_TABLES = [
|
|
|
402
453
|
"feedback"
|
|
403
454
|
];
|
|
404
455
|
var HOOKS_STORAGE_TABLES = STORAGE_TABLES;
|
|
456
|
+
var STORAGE_BACKENDS = ["sqlite", "postgresql"];
|
|
405
457
|
var HOOKS_STORAGE_ENV = "HASNA_HOOKS_DATABASE_URL";
|
|
406
458
|
var HOOKS_STORAGE_FALLBACK_ENV = "HOOKS_DATABASE_URL";
|
|
407
|
-
var
|
|
408
|
-
var
|
|
459
|
+
var HOOKS_STORAGE_BACKEND_ENV = "HASNA_HOOKS_STORAGE_BACKEND";
|
|
460
|
+
var HOOKS_STORAGE_BACKEND_FALLBACK_ENV = "HOOKS_STORAGE_BACKEND";
|
|
409
461
|
var STORAGE_DATABASE_ENV = [HOOKS_STORAGE_ENV, HOOKS_STORAGE_FALLBACK_ENV];
|
|
410
|
-
var
|
|
462
|
+
var STORAGE_BACKEND_ENV = [HOOKS_STORAGE_BACKEND_ENV, HOOKS_STORAGE_BACKEND_FALLBACK_ENV];
|
|
463
|
+
var RETIRED_STORAGE_MODE_ENV = ["HASNA_HOOKS_STORAGE_MODE", "HOOKS_STORAGE_MODE"];
|
|
411
464
|
var PRIMARY_KEYS = {
|
|
412
465
|
hook_events: ["id"],
|
|
413
466
|
schema_migrations: ["version"],
|
|
@@ -418,11 +471,46 @@ function readEnv(name) {
|
|
|
418
471
|
const value = process.env[name]?.trim();
|
|
419
472
|
return value || undefined;
|
|
420
473
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
474
|
+
var RETIRED_MODE_REPLACEMENT = {
|
|
475
|
+
local: "sqlite",
|
|
476
|
+
hybrid: "postgresql",
|
|
477
|
+
remote: "postgresql",
|
|
478
|
+
self_hosted: "postgresql",
|
|
479
|
+
"self-hosted": "postgresql",
|
|
480
|
+
selfhosted: "postgresql",
|
|
481
|
+
cloud: "postgresql"
|
|
482
|
+
};
|
|
483
|
+
var BACKEND_ALIASES = {
|
|
484
|
+
sqlite: "sqlite",
|
|
485
|
+
sqlite3: "sqlite",
|
|
486
|
+
postgresql: "postgresql",
|
|
487
|
+
postgres: "postgresql",
|
|
488
|
+
pg: "postgresql"
|
|
489
|
+
};
|
|
490
|
+
function assertNoRetiredModeEnv() {
|
|
491
|
+
for (const name of RETIRED_STORAGE_MODE_ENV) {
|
|
492
|
+
const value = readEnv(name);
|
|
493
|
+
if (!value)
|
|
494
|
+
continue;
|
|
495
|
+
const replacement = RETIRED_MODE_REPLACEMENT[value.trim().toLowerCase()];
|
|
496
|
+
const mapping = replacement ? `${value} maps to ${replacement}` : `use one of ${STORAGE_BACKENDS.join(", ")}`;
|
|
497
|
+
throw new Error(`${name} is a retired deployment-mode variable and is no longer read. ` + `Hooks storage is a data-backend switch, not a deployment mode: ` + `set ${HOOKS_STORAGE_BACKEND_ENV} to ${STORAGE_BACKENDS.join(" or ")} instead (${mapping}), ` + `then unset ${name}.`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function normalizeStorageBackend(value, envName) {
|
|
501
|
+
if (value === undefined)
|
|
502
|
+
return;
|
|
503
|
+
const normalized = value.trim().toLowerCase();
|
|
504
|
+
if (normalized === "")
|
|
505
|
+
return;
|
|
506
|
+
const backend = BACKEND_ALIASES[normalized];
|
|
507
|
+
if (backend)
|
|
508
|
+
return backend;
|
|
509
|
+
const replacement = RETIRED_MODE_REPLACEMENT[normalized];
|
|
510
|
+
if (replacement) {
|
|
511
|
+
throw new Error(`${envName}=${value} names a retired deployment mode. ` + `local/hybrid/remote/self_hosted/cloud were removed: hooks storage now selects a data ` + `backend only. Set ${envName}=${replacement} instead.`);
|
|
512
|
+
}
|
|
513
|
+
throw new Error(`${envName}=${value} is not a known hooks storage backend. ` + `Set ${HOOKS_STORAGE_BACKEND_ENV} to one of ${STORAGE_BACKENDS.join(", ")}.`);
|
|
426
514
|
}
|
|
427
515
|
function getStorageDatabaseEnvName() {
|
|
428
516
|
for (const name of STORAGE_DATABASE_ENV) {
|
|
@@ -439,11 +527,12 @@ function getStorageDatabaseUrl() {
|
|
|
439
527
|
const env = getStorageDatabaseEnv();
|
|
440
528
|
return env ? readEnv(env.name) ?? null : null;
|
|
441
529
|
}
|
|
442
|
-
function
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
530
|
+
function getStorageBackend() {
|
|
531
|
+
assertNoRetiredModeEnv();
|
|
532
|
+
const backend = normalizeStorageBackend(readEnv(HOOKS_STORAGE_BACKEND_ENV), HOOKS_STORAGE_BACKEND_ENV) ?? normalizeStorageBackend(readEnv(HOOKS_STORAGE_BACKEND_FALLBACK_ENV), HOOKS_STORAGE_BACKEND_FALLBACK_ENV);
|
|
533
|
+
if (backend)
|
|
534
|
+
return backend;
|
|
535
|
+
return getStorageDatabaseUrl() ? "postgresql" : "sqlite";
|
|
447
536
|
}
|
|
448
537
|
async function getStoragePg() {
|
|
449
538
|
const url = getStorageDatabaseUrl();
|
|
@@ -501,7 +590,7 @@ function getStorageStatus() {
|
|
|
501
590
|
const activeEnv = getStorageDatabaseEnv();
|
|
502
591
|
return {
|
|
503
592
|
configured: Boolean(activeEnv),
|
|
504
|
-
|
|
593
|
+
backend: getStorageBackend(),
|
|
505
594
|
env: STORAGE_DATABASE_ENV,
|
|
506
595
|
activeEnv: activeEnv?.name ?? null,
|
|
507
596
|
service: "hooks",
|
|
@@ -679,18 +768,20 @@ export {
|
|
|
679
768
|
getSyncMetaAll,
|
|
680
769
|
getStorageStatus,
|
|
681
770
|
getStoragePg,
|
|
682
|
-
getStorageMode,
|
|
683
771
|
getStorageDatabaseUrl,
|
|
684
772
|
getStorageDatabaseEnvName,
|
|
685
773
|
getStorageDatabaseEnv,
|
|
774
|
+
getStorageBackend,
|
|
686
775
|
STORAGE_TABLES,
|
|
687
|
-
STORAGE_MODE_ENV,
|
|
688
776
|
STORAGE_DATABASE_ENV,
|
|
777
|
+
STORAGE_BACKEND_ENV,
|
|
778
|
+
STORAGE_BACKENDS,
|
|
779
|
+
RETIRED_STORAGE_MODE_ENV,
|
|
689
780
|
PgAdapterAsync,
|
|
690
781
|
PG_MIGRATIONS,
|
|
691
782
|
HOOKS_STORAGE_TABLES,
|
|
692
|
-
HOOKS_STORAGE_MODE_FALLBACK_ENV,
|
|
693
|
-
HOOKS_STORAGE_MODE_ENV,
|
|
694
783
|
HOOKS_STORAGE_FALLBACK_ENV,
|
|
695
|
-
HOOKS_STORAGE_ENV
|
|
784
|
+
HOOKS_STORAGE_ENV,
|
|
785
|
+
HOOKS_STORAGE_BACKEND_FALLBACK_ENV,
|
|
786
|
+
HOOKS_STORAGE_BACKEND_ENV
|
|
696
787
|
};
|