@hasna/hooks 0.7.9 → 0.7.10
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/bin/index.js +483 -177
- package/bin/serve.js +281 -82
- package/dist/db/index.d.ts +3 -1
- package/dist/index.js +334 -131
- package/dist/lib/app-home.d.ts +65 -0
- package/dist/lib/profiles.d.ts +4 -1
- package/dist/serve.d.ts +7 -0
- package/dist/storage.js +148 -25
- package/package.json +6 -4
- package/scripts/artifact-scan.ts +49 -0
- package/scripts/ensure-profiles-dir.mjs +48 -0
- package/scripts/validate-package.ts +280 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-home resolution for @hasna/hooks — routes the local data root through
|
|
3
|
+
* the @hasna/paths resolver (XDG / macOS home layout) with gated legacy
|
|
4
|
+
* adoption.
|
|
5
|
+
*
|
|
6
|
+
* The XDG home migration (hotfixes plan 0f49f56a, task P3.3) moves the store
|
|
7
|
+
* from `~/.hasna/hooks` toward `~/.local/share/hasna/hooks` on Linux and
|
|
8
|
+
* `~/Library/Application Support/Hasna/hooks` on macOS. Nothing moves on disk
|
|
9
|
+
* in this phase — the resolver root is adopted only when the operator has
|
|
10
|
+
* deliberately opted in (`HASNA_DATA_HOME`) or the store has already been
|
|
11
|
+
* physically migrated there, so a live store at the legacy home never becomes
|
|
12
|
+
* invisible on upgrade.
|
|
13
|
+
*/
|
|
14
|
+
/** Resolve the user's home directory: $HOME, then $USERPROFILE, then the OS user database. */
|
|
15
|
+
export declare function getHomeDir(env?: NodeJS.ProcessEnv): string;
|
|
16
|
+
/**
|
|
17
|
+
* The @hasna/paths-resolved (XDG / macOS layout) data root for hooks: the
|
|
18
|
+
* forward-looking home the XDG migration moves the store toward. The home
|
|
19
|
+
* override mirrors the pre-existing $HOME-first resolution so the resolver
|
|
20
|
+
* follows the same home the legacy path does.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getResolverDataRoot(env?: NodeJS.ProcessEnv): string;
|
|
23
|
+
/** The legacy (pre-XDG) data root: ~/.hasna/hooks */
|
|
24
|
+
export declare function getLegacyDataRoot(env?: NodeJS.ProcessEnv): string;
|
|
25
|
+
/**
|
|
26
|
+
* Whether the resolver (XDG) data root should be adopted as the effective data
|
|
27
|
+
* root. The resolver root is adopted only when the operator has set
|
|
28
|
+
* `HASNA_DATA_HOME` (the data-kind override — a deliberate opt-in to the XDG
|
|
29
|
+
* layout) or the store has already been physically migrated there (`hooks.db`
|
|
30
|
+
* exists). A machine that only redirects another kind (e.g. cache to tmpfs)
|
|
31
|
+
* must NOT have its data home moved, and a live store at the legacy home must
|
|
32
|
+
* never become invisible on upgrade.
|
|
33
|
+
*/
|
|
34
|
+
export declare function adoptResolverDataRoot(resolved: string, env?: NodeJS.ProcessEnv): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* The granular data-dir override root, when set: `HASNA_HOOKS_DATA_DIR`, then
|
|
37
|
+
* `HOOKS_DATA_DIR`. This preserves the pre-existing exact-data-dir override
|
|
38
|
+
* behavior verbatim (first-nonblank; a set-but-empty value falls through).
|
|
39
|
+
*/
|
|
40
|
+
export declare function getExplicitDataDir(env?: NodeJS.ProcessEnv): string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The exact-app override root, when set: `HASNA_HOOKS_HOME`, then `HOOKS_HOME`.
|
|
43
|
+
* First-nonblank selection: a set-but-whitespace override must not suppress a
|
|
44
|
+
* valid fallback (the same `?.trim() ||` semantics the emails lane settled as
|
|
45
|
+
* release-review P1).
|
|
46
|
+
*/
|
|
47
|
+
export declare function getExactDataRoot(env?: NodeJS.ProcessEnv): string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* The effective data root for hooks. Precedence: the granular data-dir
|
|
50
|
+
* override (`HASNA_HOOKS_DATA_DIR`/`HOOKS_DATA_DIR`) wins; then the exact-app
|
|
51
|
+
* override (`HASNA_HOOKS_HOME`/`HOOKS_HOME`); then the resolver (XDG) data
|
|
52
|
+
* root once adopted; then the legacy `~/.hasna/hooks` default — a live store
|
|
53
|
+
* never becomes invisible on upgrade. Store/lock/config paths are layered on
|
|
54
|
+
* top of this by their own overrides, so an explicit path always wins
|
|
55
|
+
* regardless.
|
|
56
|
+
*/
|
|
57
|
+
export declare function getEffectiveDataRoot(env?: NodeJS.ProcessEnv): string;
|
|
58
|
+
/**
|
|
59
|
+
* The SQLite store path surfaced by help/status surfaces (e.g. `hooks log`,
|
|
60
|
+
* registry hook descriptions). The explicit `HASNA_HOOKS_DB_PATH` /
|
|
61
|
+
* `HOOKS_DB_PATH` override wins; otherwise `hooks.db` under the effective data
|
|
62
|
+
* root. A status surface must never hardcode the legacy literal — the store can
|
|
63
|
+
* live at the resolver home once adopted.
|
|
64
|
+
*/
|
|
65
|
+
export declare function getReportedDbPath(env?: NodeJS.ProcessEnv): string;
|
package/dist/lib/profiles.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent profile management — identity system for hooks
|
|
3
3
|
*
|
|
4
|
-
* Each agent instance gets a unique 8-char UUID stored at
|
|
4
|
+
* Each agent instance gets a unique 8-char UUID stored at
|
|
5
|
+
* <effective-data-root>/profiles/<id>.json, where the effective data root is
|
|
6
|
+
* resolved through @hasna/paths (legacy ~/.hasna/hooks until adopted, then
|
|
7
|
+
* the XDG data home).
|
|
5
8
|
* Profiles are injected into HookInput when hooks are run with --profile <id>,
|
|
6
9
|
* allowing hooks to identify which agent is calling them.
|
|
7
10
|
*/
|
package/dist/serve.d.ts
CHANGED
|
@@ -31,6 +31,13 @@ export interface ArtifactPayload {
|
|
|
31
31
|
script: string;
|
|
32
32
|
}
|
|
33
33
|
export declare function handleServeRequest(req: Request, apiKey: string | undefined): Promise<Response>;
|
|
34
|
+
export declare function resolveServeOptions(options: {
|
|
35
|
+
port?: number;
|
|
36
|
+
host?: string;
|
|
37
|
+
}): {
|
|
38
|
+
port: number;
|
|
39
|
+
host: string;
|
|
40
|
+
};
|
|
34
41
|
export declare function startServeServer(options: {
|
|
35
42
|
port?: number;
|
|
36
43
|
host?: string;
|
package/dist/storage.js
CHANGED
|
@@ -15,6 +15,132 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
|
+
// ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
function assertApp(app) {
|
|
22
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
23
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
24
|
+
}
|
|
25
|
+
if (!APP_SLUG_RE.test(app)) {
|
|
26
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function envOf(options) {
|
|
30
|
+
return options.env ?? process.env;
|
|
31
|
+
}
|
|
32
|
+
function envValue(options, kind) {
|
|
33
|
+
const value = envOf(options)[KIND_ENV[kind]];
|
|
34
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
35
|
+
}
|
|
36
|
+
function isMacOS(platform) {
|
|
37
|
+
return platform === "darwin";
|
|
38
|
+
}
|
|
39
|
+
function baseDir(kind, options) {
|
|
40
|
+
const override = envValue(options, kind);
|
|
41
|
+
if (override)
|
|
42
|
+
return override;
|
|
43
|
+
const home = options.home ?? homedir();
|
|
44
|
+
const platform = options.platform ?? process.platform;
|
|
45
|
+
if (isMacOS(platform)) {
|
|
46
|
+
switch (kind) {
|
|
47
|
+
case "config":
|
|
48
|
+
case "data":
|
|
49
|
+
return join(home, "Library", "Application Support", "Hasna");
|
|
50
|
+
case "cache":
|
|
51
|
+
return join(home, "Library", "Caches", "Hasna");
|
|
52
|
+
case "state":
|
|
53
|
+
return join(home, "Library", "Logs", "Hasna");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
switch (kind) {
|
|
57
|
+
case "config":
|
|
58
|
+
return join(home, ".config", "hasna");
|
|
59
|
+
case "data":
|
|
60
|
+
return join(home, ".local", "share", "hasna");
|
|
61
|
+
case "state":
|
|
62
|
+
return join(home, ".local", "state", "hasna");
|
|
63
|
+
case "cache":
|
|
64
|
+
return join(home, ".cache", "hasna");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function resolvePath(kind, options) {
|
|
68
|
+
assertApp(options.app);
|
|
69
|
+
const appSegment = options.internal === true ? join("internal", options.app) : options.app;
|
|
70
|
+
return join(baseDir(kind, options), appSegment);
|
|
71
|
+
}
|
|
72
|
+
function dataDir(options) {
|
|
73
|
+
return resolvePath("data", options);
|
|
74
|
+
}
|
|
75
|
+
var KIND_ENV, APP_SLUG_RE;
|
|
76
|
+
var init_dist = __esm(() => {
|
|
77
|
+
KIND_ENV = {
|
|
78
|
+
config: "HASNA_CONFIG_HOME",
|
|
79
|
+
data: "HASNA_DATA_HOME",
|
|
80
|
+
state: "HASNA_STATE_HOME",
|
|
81
|
+
cache: "HASNA_CACHE_HOME"
|
|
82
|
+
};
|
|
83
|
+
APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// src/lib/app-home.ts
|
|
87
|
+
import { existsSync } from "fs";
|
|
88
|
+
import { homedir as homedir2 } from "os";
|
|
89
|
+
import { join as join2, resolve } from "path";
|
|
90
|
+
function getHomeDir(env = process.env) {
|
|
91
|
+
return env.HOME || env.USERPROFILE || homedir2();
|
|
92
|
+
}
|
|
93
|
+
function getResolverDataRoot(env = process.env) {
|
|
94
|
+
return dataDir({ app: "hooks", home: getHomeDir(env), env });
|
|
95
|
+
}
|
|
96
|
+
function getLegacyDataRoot(env = process.env) {
|
|
97
|
+
return join2(getHomeDir(env), ".hasna", "hooks");
|
|
98
|
+
}
|
|
99
|
+
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
100
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
101
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
102
|
+
return true;
|
|
103
|
+
return existsSync(join2(resolved, STORE_MARKER));
|
|
104
|
+
}
|
|
105
|
+
function getExplicitDataDir(env = process.env) {
|
|
106
|
+
const dir = env.HASNA_HOOKS_DATA_DIR ?? env.HOOKS_DATA_DIR;
|
|
107
|
+
if (typeof dir === "string" && dir.trim().length > 0)
|
|
108
|
+
return dir.trim();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
function getExactDataRoot(env = process.env) {
|
|
112
|
+
const dir = env.HASNA_HOOKS_HOME?.trim() || env.HOOKS_HOME?.trim();
|
|
113
|
+
if (dir)
|
|
114
|
+
return resolve(dir);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
function getEffectiveDataRoot(env = process.env) {
|
|
118
|
+
const explicit = getExplicitDataDir(env);
|
|
119
|
+
if (explicit)
|
|
120
|
+
return explicit;
|
|
121
|
+
const exact = getExactDataRoot(env);
|
|
122
|
+
if (exact)
|
|
123
|
+
return exact;
|
|
124
|
+
const resolved = getResolverDataRoot(env);
|
|
125
|
+
return adoptResolverDataRoot(resolved, env) ? resolve(resolved) : getLegacyDataRoot(env);
|
|
126
|
+
}
|
|
127
|
+
function getReportedDbPath(env = process.env) {
|
|
128
|
+
const explicit = getExplicitDbPath(env);
|
|
129
|
+
if (explicit)
|
|
130
|
+
return explicit;
|
|
131
|
+
return join2(getEffectiveDataRoot(env), "hooks.db");
|
|
132
|
+
}
|
|
133
|
+
function getExplicitDbPath(env = process.env) {
|
|
134
|
+
const db = env.HASNA_HOOKS_DB_PATH ?? env.HOOKS_DB_PATH;
|
|
135
|
+
if (typeof db === "string" && db.trim().length > 0)
|
|
136
|
+
return db.trim();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
var STORE_MARKER = "hooks.db";
|
|
140
|
+
var init_app_home = __esm(() => {
|
|
141
|
+
init_dist();
|
|
142
|
+
});
|
|
143
|
+
|
|
18
144
|
// src/db/schema.ts
|
|
19
145
|
var CREATE_HOOK_EVENTS_TABLE = `
|
|
20
146
|
CREATE TABLE IF NOT EXISTS hook_events (
|
|
@@ -209,9 +335,9 @@ var init_migrations = __esm(() => {
|
|
|
209
335
|
});
|
|
210
336
|
|
|
211
337
|
// src/db/legacy-import.ts
|
|
212
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
213
|
-
import { join } from "path";
|
|
214
|
-
import { homedir } from "os";
|
|
338
|
+
import { existsSync as existsSync2, readdirSync, readFileSync } from "fs";
|
|
339
|
+
import { join as join3 } from "path";
|
|
340
|
+
import { homedir as homedir3 } from "os";
|
|
215
341
|
function ensureMetaTable(db) {
|
|
216
342
|
db.exec(`
|
|
217
343
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
@@ -285,25 +411,25 @@ function importErrorsLog(db, filePath) {
|
|
|
285
411
|
} catch {}
|
|
286
412
|
return count;
|
|
287
413
|
}
|
|
288
|
-
function runLegacyImport(db, homeDir =
|
|
414
|
+
function runLegacyImport(db, homeDir = homedir3()) {
|
|
289
415
|
try {
|
|
290
416
|
if (isAlreadyDone(db))
|
|
291
417
|
return;
|
|
292
418
|
let total = 0;
|
|
293
|
-
const claudeProjectsDir =
|
|
294
|
-
if (
|
|
419
|
+
const claudeProjectsDir = join3(homeDir, ".claude", "projects");
|
|
420
|
+
if (existsSync2(claudeProjectsDir)) {
|
|
295
421
|
try {
|
|
296
422
|
const projectDirs = readdirSync(claudeProjectsDir);
|
|
297
423
|
for (const dir of projectDirs) {
|
|
298
|
-
const projectDir =
|
|
424
|
+
const projectDir = join3(claudeProjectsDir, dir);
|
|
299
425
|
try {
|
|
300
426
|
const files = readdirSync(projectDir);
|
|
301
427
|
for (const file of files) {
|
|
302
428
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
303
|
-
total += importJsonlFile(db,
|
|
429
|
+
total += importJsonlFile(db, join3(projectDir, file));
|
|
304
430
|
}
|
|
305
431
|
if (file === "errors.log") {
|
|
306
|
-
total += importErrorsLog(db,
|
|
432
|
+
total += importErrorsLog(db, join3(projectDir, file));
|
|
307
433
|
}
|
|
308
434
|
}
|
|
309
435
|
} catch {}
|
|
@@ -339,31 +465,27 @@ function runRetention(db, days) {
|
|
|
339
465
|
|
|
340
466
|
// src/db/index.ts
|
|
341
467
|
import { Database } from "bun:sqlite";
|
|
342
|
-
import { existsSync as
|
|
343
|
-
import { join as
|
|
344
|
-
import { homedir as homedir2 } from "os";
|
|
468
|
+
import { existsSync as existsSync3, mkdirSync, cpSync } from "fs";
|
|
469
|
+
import { join as join4 } from "path";
|
|
345
470
|
function resolveDataDir() {
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (!existsSync2(newDir) && existsSync2(oldDir)) {
|
|
352
|
-
mkdirSync(join2(homedir2(), ".hasna"), { recursive: true });
|
|
353
|
-
cpSync(oldDir, newDir, { recursive: true });
|
|
471
|
+
const effective = getEffectiveDataRoot();
|
|
472
|
+
const oldDir = join4(getHomeDir(), ".hooks");
|
|
473
|
+
if (!existsSync3(effective) && existsSync3(oldDir)) {
|
|
474
|
+
mkdirSync(effective, { recursive: true });
|
|
475
|
+
cpSync(oldDir, effective, { recursive: true });
|
|
354
476
|
}
|
|
355
|
-
return
|
|
477
|
+
return effective;
|
|
356
478
|
}
|
|
357
479
|
function getDbPath() {
|
|
358
480
|
const explicitDb = process.env.HASNA_HOOKS_DB_PATH ?? process.env.HOOKS_DB_PATH;
|
|
359
481
|
if (explicitDb)
|
|
360
482
|
return explicitDb;
|
|
361
|
-
const
|
|
362
|
-
return
|
|
483
|
+
const dataDir2 = resolveDataDir();
|
|
484
|
+
return join4(dataDir2, "hooks.db");
|
|
363
485
|
}
|
|
364
486
|
function ensureDir(dbPath) {
|
|
365
487
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
366
|
-
if (dir && !
|
|
488
|
+
if (dir && !existsSync3(dir)) {
|
|
367
489
|
mkdirSync(dir, { recursive: true });
|
|
368
490
|
}
|
|
369
491
|
}
|
|
@@ -371,7 +493,7 @@ function getDb() {
|
|
|
371
493
|
if (instance)
|
|
372
494
|
return instance;
|
|
373
495
|
const dbPath = getDbPath();
|
|
374
|
-
const isNew = dbPath === ":memory:" || !
|
|
496
|
+
const isNew = dbPath === ":memory:" || !existsSync3(dbPath);
|
|
375
497
|
ensureDir(dbPath);
|
|
376
498
|
instance = new Database(dbPath);
|
|
377
499
|
instance.exec("PRAGMA busy_timeout=5000");
|
|
@@ -395,6 +517,7 @@ function getDb() {
|
|
|
395
517
|
}
|
|
396
518
|
var instance = null;
|
|
397
519
|
var init_db = __esm(() => {
|
|
520
|
+
init_app_home();
|
|
398
521
|
init_migrations();
|
|
399
522
|
init_legacy_import();
|
|
400
523
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/hooks",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
4
4
|
"description": "Open source hooks library for AI coding agents - Install safety, quality, and automation hooks with a single command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dashboard:dev": "cd dashboard && bun run dev",
|
|
31
31
|
"dashboard:build": "cd dashboard && bun run build",
|
|
32
32
|
"dashboard:preview": "cd dashboard && bun run preview",
|
|
33
|
-
"postinstall": "
|
|
33
|
+
"postinstall": "node scripts/ensure-profiles-dir.mjs",
|
|
34
34
|
"scan:artifact": "bun scripts/artifact-scan.ts",
|
|
35
35
|
"prepack": "bun run build && bun run scan:artifact"
|
|
36
36
|
},
|
|
@@ -52,10 +52,11 @@
|
|
|
52
52
|
"@types/react": "^18.2.0",
|
|
53
53
|
"typescript": "^5",
|
|
54
54
|
"bun-types": "1.3.14",
|
|
55
|
-
"@hasna/contracts": "0.14.
|
|
55
|
+
"@hasna/contracts": "0.14.1"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@hasna/events": "^0.1.16",
|
|
59
|
+
"@hasna/paths": "0.1.0",
|
|
59
60
|
"@hasna/secrets": "0.3.10",
|
|
60
61
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
61
62
|
"chalk": "^5.3.0",
|
|
@@ -68,7 +69,7 @@
|
|
|
68
69
|
"pg": "^8.16.3",
|
|
69
70
|
"react": "^18.2.0",
|
|
70
71
|
"zod": "3.25.76",
|
|
71
|
-
"@hasna/contracts": "0.14.
|
|
72
|
+
"@hasna/contracts": "0.14.1"
|
|
72
73
|
},
|
|
73
74
|
"engines": {
|
|
74
75
|
"bun": ">=1.0.0"
|
|
@@ -82,6 +83,7 @@
|
|
|
82
83
|
"dist/",
|
|
83
84
|
"hooks/",
|
|
84
85
|
"!hooks/*/dist/",
|
|
86
|
+
"scripts/",
|
|
85
87
|
"README.md",
|
|
86
88
|
"LICENSE"
|
|
87
89
|
],
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Pack this repo the way `npm publish` would, then scan what actually shipped.
|
|
2
|
+
//
|
|
3
|
+
// `contracts artifact-scan` takes a PACKED .tgz — running it against `src/` is
|
|
4
|
+
// explicitly wrong, and running it with no target at all just exits 1 and takes
|
|
5
|
+
// `npm pack` / `bun publish` down with it. So this script produces the tarball
|
|
6
|
+
// first and hands the real artifact to the scanner.
|
|
7
|
+
//
|
|
8
|
+
// Packing uses `--ignore-scripts`: without it, packing from inside `prepack`
|
|
9
|
+
// would re-enter `prepack` forever.
|
|
10
|
+
//
|
|
11
|
+
// The scanner comes from the pinned `@hasna/contracts` devDependency
|
|
12
|
+
// (node_modules/.bin), never `bunx` — an unpinned package runner makes the
|
|
13
|
+
// release gate depend on whatever the registry serves that day.
|
|
14
|
+
|
|
15
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { isAbsolute, join } from "node:path";
|
|
18
|
+
|
|
19
|
+
function run(command: string[], cwd: string): string {
|
|
20
|
+
const result = Bun.spawnSync(command, { cwd, stdout: "pipe", stderr: "pipe" });
|
|
21
|
+
const stdout = new TextDecoder().decode(result.stdout).trim();
|
|
22
|
+
if (result.exitCode !== 0) {
|
|
23
|
+
const stderr = new TextDecoder().decode(result.stderr).trim();
|
|
24
|
+
throw new Error(`${command.join(" ")} exited ${result.exitCode}\n${stdout}\n${stderr}`);
|
|
25
|
+
}
|
|
26
|
+
return stdout;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const repoRoot = join(import.meta.dir, "..");
|
|
30
|
+
const scanner = join(repoRoot, "node_modules", "@hasna", "contracts", "dist", "cli", "index.js");
|
|
31
|
+
const workspace = mkdtempSync(join(tmpdir(), "hooks-artifact-scan-"));
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const packed = run(["bun", "pm", "pack", "--destination", workspace, "--ignore-scripts", "--quiet"], repoRoot);
|
|
35
|
+
const archive = isAbsolute(packed) ? packed : join(workspace, packed);
|
|
36
|
+
// The manifest is where a reviewed waiver would be declared, so the scanner
|
|
37
|
+
// has to read it; a waiver the enforcement never loads is a dead end.
|
|
38
|
+
const scan = Bun.spawnSync([scanner, "artifact-scan", archive, "--manifest", join(repoRoot, "hasna.contract.json")], {
|
|
39
|
+
cwd: repoRoot,
|
|
40
|
+
stdout: "inherit",
|
|
41
|
+
stderr: "inherit",
|
|
42
|
+
});
|
|
43
|
+
if (scan.exitCode !== 0) {
|
|
44
|
+
console.error("\nA published artifact must not carry a bulk asset inventory.");
|
|
45
|
+
process.exit(scan.exitCode ?? 1);
|
|
46
|
+
}
|
|
47
|
+
} finally {
|
|
48
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Ensure the hooks agent-profile directory exists at the effective data root.
|
|
2
|
+
// Runs at install time (postinstall). Mirrors src/lib/app-home.ts resolution:
|
|
3
|
+
// granular data-dir override (HASNA_HOOKS_DATA_DIR/HOOKS_DATA_DIR), then
|
|
4
|
+
// exact-app override (HASNA_HOOKS_HOME/HOOKS_HOME), then the @hasna/paths
|
|
5
|
+
// resolver data root once adopted (HASNA_DATA_HOME set or hooks.db already
|
|
6
|
+
// present there), then the legacy ~/.hasna/hooks default. Best-effort — the
|
|
7
|
+
// package must never fail to install because this script cannot run.
|
|
8
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
13
|
+
|
|
14
|
+
function legacyDataRoot() {
|
|
15
|
+
return join(HOME, ".hasna", "hooks");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function adoptResolverRoot(resolved) {
|
|
19
|
+
const override = process.env.HASNA_DATA_HOME;
|
|
20
|
+
if (typeof override === "string" && override.trim().length > 0) return true;
|
|
21
|
+
return existsSync(join(resolved, "hooks.db"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function effectiveDataRoot() {
|
|
25
|
+
const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
|
|
26
|
+
if (typeof explicit === "string" && explicit.trim().length > 0) return explicit.trim();
|
|
27
|
+
|
|
28
|
+
const exact = process.env.HASNA_HOOKS_HOME?.trim() || process.env.HOOKS_HOME?.trim();
|
|
29
|
+
if (exact) return resolve(exact);
|
|
30
|
+
|
|
31
|
+
let resolved = null;
|
|
32
|
+
try {
|
|
33
|
+
const { dataDir } = await import("@hasna/paths");
|
|
34
|
+
resolved = dataDir({ app: "hooks", home: HOME });
|
|
35
|
+
} catch {
|
|
36
|
+
// @hasna/paths unavailable — fall back to the legacy root.
|
|
37
|
+
}
|
|
38
|
+
if (resolved && adoptResolverRoot(resolved)) return resolve(resolved);
|
|
39
|
+
return legacyDataRoot();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Best-effort: a profile-directory creation failure must never block install.
|
|
43
|
+
try {
|
|
44
|
+
const root = await effectiveDataRoot();
|
|
45
|
+
mkdirSync(join(root, "profiles"), { recursive: true });
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore
|
|
48
|
+
}
|