@ekkos/cli 0.2.9 → 0.2.11
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/agent/daemon.d.ts +86 -0
- package/dist/agent/daemon.js +297 -0
- package/dist/agent/pty-runner.d.ts +51 -0
- package/dist/agent/pty-runner.js +184 -0
- package/dist/cache/LocalSessionStore.d.ts +34 -21
- package/dist/cache/LocalSessionStore.js +169 -53
- package/dist/cache/capture.d.ts +19 -11
- package/dist/cache/capture.js +243 -76
- package/dist/cache/types.d.ts +14 -1
- package/dist/commands/agent.d.ts +44 -0
- package/dist/commands/agent.js +300 -0
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +175 -87
- package/dist/commands/hooks.d.ts +109 -0
- package/dist/commands/hooks.js +668 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +357 -85
- package/dist/commands/setup-remote.d.ts +20 -0
- package/dist/commands/setup-remote.js +467 -0
- package/dist/index.js +116 -1
- package/dist/restore/RestoreOrchestrator.d.ts +17 -3
- package/dist/restore/RestoreOrchestrator.js +64 -22
- package/dist/utils/paths.d.ts +125 -0
- package/dist/utils/paths.js +283 -0
- package/dist/utils/state.d.ts +2 -0
- package/package.json +1 -1
- package/templates/ekkos-manifest.json +223 -0
- package/templates/helpers/json-parse.cjs +101 -0
- package/templates/hooks/assistant-response.ps1 +256 -0
- package/templates/hooks/assistant-response.sh +124 -64
- package/templates/hooks/session-start.ps1 +107 -2
- package/templates/hooks/session-start.sh +201 -166
- package/templates/hooks/stop.ps1 +124 -3
- package/templates/hooks/stop.sh +470 -843
- package/templates/hooks/user-prompt-submit.ps1 +107 -22
- package/templates/hooks/user-prompt-submit.sh +403 -393
- package/templates/project-stubs/session-start.ps1 +63 -0
- package/templates/project-stubs/session-start.sh +55 -0
- package/templates/project-stubs/stop.ps1 +63 -0
- package/templates/project-stubs/stop.sh +55 -0
- package/templates/project-stubs/user-prompt-submit.ps1 +63 -0
- package/templates/project-stubs/user-prompt-submit.sh +55 -0
- package/templates/shared/hooks-enabled.json +22 -0
- package/templates/shared/session-words.json +45 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ekkOS CLI: hooks subcommand
|
|
4
|
+
* Implements: ekkos hooks install | verify | status
|
|
5
|
+
*
|
|
6
|
+
* Per ekkOS Onboarding Spec v1.2 + Addendum:
|
|
7
|
+
* - Manifest-driven deployment
|
|
8
|
+
* - SHA256 checksum verification
|
|
9
|
+
* - Installed-state manifest tracking
|
|
10
|
+
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.findManifest = findManifest;
|
|
16
|
+
exports.loadManifest = loadManifest;
|
|
17
|
+
exports.hooksInstall = hooksInstall;
|
|
18
|
+
exports.hooksVerify = hooksVerify;
|
|
19
|
+
exports.hooksStatus = hooksStatus;
|
|
20
|
+
exports.findProjectRoot = findProjectRoot;
|
|
21
|
+
exports.expandPath = expandPath;
|
|
22
|
+
const fs_1 = require("fs");
|
|
23
|
+
const path_1 = require("path");
|
|
24
|
+
const os_1 = require("os");
|
|
25
|
+
const crypto_1 = require("crypto");
|
|
26
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
28
|
+
// CONSTANTS
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
+
const isWindows = (0, os_1.platform)() === 'win32';
|
|
31
|
+
const HOME_DIR = (0, os_1.homedir)();
|
|
32
|
+
// Resolved paths
|
|
33
|
+
function expandPath(p) {
|
|
34
|
+
return p
|
|
35
|
+
.replace(/^~/, HOME_DIR)
|
|
36
|
+
.replace(/%USERPROFILE%/g, HOME_DIR);
|
|
37
|
+
}
|
|
38
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
+
// MANIFEST DISCOVERY
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
41
|
+
/**
|
|
42
|
+
* Find the ekkos-manifest.json file
|
|
43
|
+
* Search order per spec:
|
|
44
|
+
* 1. <packageRoot>/templates/ekkos-manifest.json
|
|
45
|
+
* 2. <distRoot>/../templates/ekkos-manifest.json
|
|
46
|
+
* 3. In monorepo dev: <repoRoot>/templates/ekkos-manifest.json
|
|
47
|
+
*/
|
|
48
|
+
function findManifest() {
|
|
49
|
+
const candidates = [];
|
|
50
|
+
// 1. Package root (npm install location)
|
|
51
|
+
const packageRoot = (0, path_1.join)((0, path_1.dirname)((0, path_1.dirname)(__dirname)), 'templates', 'ekkos-manifest.json');
|
|
52
|
+
candidates.push(packageRoot);
|
|
53
|
+
// 2. Dist root (compiled location)
|
|
54
|
+
const distRoot = (0, path_1.join)((0, path_1.dirname)((0, path_1.dirname)((0, path_1.dirname)(__dirname))), 'templates', 'ekkos-manifest.json');
|
|
55
|
+
candidates.push(distRoot);
|
|
56
|
+
// 3. Monorepo dev (presence of templates/ at repo root)
|
|
57
|
+
const repoRoot = (0, path_1.join)((0, path_1.dirname)((0, path_1.dirname)((0, path_1.dirname)((0, path_1.dirname)((0, path_1.dirname)(__dirname))))), 'templates', 'ekkos-manifest.json');
|
|
58
|
+
candidates.push(repoRoot);
|
|
59
|
+
// Also check from current __dirname variations
|
|
60
|
+
const fromDirname = (0, path_1.join)(__dirname, '..', '..', 'templates', 'ekkos-manifest.json');
|
|
61
|
+
candidates.push(fromDirname);
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
if ((0, fs_1.existsSync)(candidate)) {
|
|
64
|
+
return {
|
|
65
|
+
path: candidate,
|
|
66
|
+
templatesDir: (0, path_1.dirname)(candidate)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Load manifest from disk
|
|
74
|
+
*/
|
|
75
|
+
function loadManifest() {
|
|
76
|
+
const found = findManifest();
|
|
77
|
+
if (!found)
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
const content = (0, fs_1.readFileSync)(found.path, 'utf-8');
|
|
81
|
+
const manifest = JSON.parse(content);
|
|
82
|
+
return { manifest, ...found };
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
89
|
+
// CHECKSUM UTILITIES
|
|
90
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
91
|
+
function sha256File(filePath) {
|
|
92
|
+
const content = (0, fs_1.readFileSync)(filePath);
|
|
93
|
+
return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
94
|
+
}
|
|
95
|
+
function sha256String(content) {
|
|
96
|
+
return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
97
|
+
}
|
|
98
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
99
|
+
// PROJECT ROOT DETECTION
|
|
100
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
101
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
102
|
+
let dir = startDir;
|
|
103
|
+
// Walk up looking for .git or package.json
|
|
104
|
+
while (dir !== (0, path_1.dirname)(dir)) {
|
|
105
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dir, '.git')) || (0, fs_1.existsSync)((0, path_1.join)(dir, 'package.json'))) {
|
|
106
|
+
return dir;
|
|
107
|
+
}
|
|
108
|
+
dir = (0, path_1.dirname)(dir);
|
|
109
|
+
}
|
|
110
|
+
// Fallback to cwd
|
|
111
|
+
return startDir;
|
|
112
|
+
}
|
|
113
|
+
async function hooksInstall(options) {
|
|
114
|
+
const verbose = options.verbose || false;
|
|
115
|
+
const isGlobal = options.global !== false; // Default to global
|
|
116
|
+
const isProject = options.project === true;
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(chalk_1.default.cyan.bold('ekkOS Hooks Installer'));
|
|
119
|
+
console.log(chalk_1.default.gray('-'.repeat(50)));
|
|
120
|
+
console.log('');
|
|
121
|
+
// Load manifest
|
|
122
|
+
const manifestData = loadManifest();
|
|
123
|
+
if (!manifestData) {
|
|
124
|
+
console.log(chalk_1.default.red('Error: Could not find ekkos-manifest.json'));
|
|
125
|
+
console.log(chalk_1.default.gray('Searched in:'));
|
|
126
|
+
console.log(chalk_1.default.gray(' - <packageRoot>/templates/'));
|
|
127
|
+
console.log(chalk_1.default.gray(' - <distRoot>/../templates/'));
|
|
128
|
+
console.log(chalk_1.default.gray(' - <repoRoot>/templates/ (monorepo dev)'));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const { manifest, templatesDir } = manifestData;
|
|
132
|
+
const platformConfig = manifest.platforms[(0, os_1.platform)()];
|
|
133
|
+
if (verbose) {
|
|
134
|
+
console.log(chalk_1.default.gray(`Manifest: ${manifestData.path}`));
|
|
135
|
+
console.log(chalk_1.default.gray(`Templates: ${templatesDir}`));
|
|
136
|
+
console.log(chalk_1.default.gray(`Platform: ${(0, os_1.platform)()}`));
|
|
137
|
+
console.log('');
|
|
138
|
+
}
|
|
139
|
+
const installedFiles = [];
|
|
140
|
+
let errorCount = 0;
|
|
141
|
+
// Resolve config and hooks directories
|
|
142
|
+
const configDir = expandPath(platformConfig.configDir);
|
|
143
|
+
const globalHooksDir = expandPath(platformConfig.globalHooksDir);
|
|
144
|
+
// Create directories
|
|
145
|
+
const defaultsDir = (0, path_1.join)(configDir, '.defaults');
|
|
146
|
+
const helpersDir = (0, path_1.join)(configDir, '.helpers');
|
|
147
|
+
(0, fs_1.mkdirSync)(configDir, { recursive: true });
|
|
148
|
+
(0, fs_1.mkdirSync)(defaultsDir, { recursive: true });
|
|
149
|
+
(0, fs_1.mkdirSync)(helpersDir, { recursive: true });
|
|
150
|
+
(0, fs_1.mkdirSync)(globalHooksDir, { recursive: true });
|
|
151
|
+
(0, fs_1.mkdirSync)((0, path_1.join)(globalHooksDir, 'lib'), { recursive: true });
|
|
152
|
+
if (verbose) {
|
|
153
|
+
console.log(chalk_1.default.gray(`Config dir: ${configDir}`));
|
|
154
|
+
console.log(chalk_1.default.gray(`Hooks dir: ${globalHooksDir}`));
|
|
155
|
+
console.log('');
|
|
156
|
+
}
|
|
157
|
+
// Helper to copy a file
|
|
158
|
+
const copyFile = (source, dest, description, options = {}) => {
|
|
159
|
+
const sourcePath = (0, path_1.join)(templatesDir, source);
|
|
160
|
+
if (!(0, fs_1.existsSync)(sourcePath)) {
|
|
161
|
+
console.log(chalk_1.default.yellow(` SKIP ${(0, path_1.basename)(dest)} - source not found`));
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
// Check overwrite policy
|
|
165
|
+
if (options.overwrite === 'createOnly' && (0, fs_1.existsSync)(dest)) {
|
|
166
|
+
if (verbose) {
|
|
167
|
+
console.log(chalk_1.default.gray(` SKIP ${(0, path_1.basename)(dest)} - already exists (user-editable)`));
|
|
168
|
+
}
|
|
169
|
+
return true; // Not an error
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(dest), { recursive: true });
|
|
173
|
+
(0, fs_1.copyFileSync)(sourcePath, dest);
|
|
174
|
+
// Make executable on Unix
|
|
175
|
+
if (options.executable && !isWindows) {
|
|
176
|
+
(0, fs_1.chmodSync)(dest, '755');
|
|
177
|
+
}
|
|
178
|
+
const checksum = sha256File(dest);
|
|
179
|
+
installedFiles.push({
|
|
180
|
+
path: dest,
|
|
181
|
+
checksum,
|
|
182
|
+
installedAt: new Date().toISOString()
|
|
183
|
+
});
|
|
184
|
+
console.log(chalk_1.default.green(` ✓ ${(0, path_1.basename)(dest)}`));
|
|
185
|
+
if (verbose) {
|
|
186
|
+
console.log(chalk_1.default.gray(` → ${dest}`));
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
console.log(chalk_1.default.red(` ✗ ${(0, path_1.basename)(dest)} - ${err.message}`));
|
|
192
|
+
errorCount++;
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
197
|
+
// GLOBAL INSTALL
|
|
198
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
199
|
+
if (isGlobal) {
|
|
200
|
+
console.log(chalk_1.default.cyan('Installing global hooks...'));
|
|
201
|
+
console.log('');
|
|
202
|
+
// 1. Managed defaults (always overwrite)
|
|
203
|
+
console.log(chalk_1.default.gray('Managed defaults:'));
|
|
204
|
+
for (const file of manifest.files.managed) {
|
|
205
|
+
const dest = (0, path_1.join)(configDir, file.destination);
|
|
206
|
+
copyFile(file.source, dest, file.description, {
|
|
207
|
+
overwrite: 'always'
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
console.log('');
|
|
211
|
+
// 2. Helpers (always overwrite, executable)
|
|
212
|
+
console.log(chalk_1.default.gray('Helpers:'));
|
|
213
|
+
for (const file of manifest.files.helpers) {
|
|
214
|
+
const dest = (0, path_1.join)(configDir, file.destination);
|
|
215
|
+
copyFile(file.source, dest, file.description, {
|
|
216
|
+
executable: file.executable,
|
|
217
|
+
overwrite: 'always'
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
console.log('');
|
|
221
|
+
// 3. User-editable files (create if missing only)
|
|
222
|
+
console.log(chalk_1.default.gray('User-editable (create if missing):'));
|
|
223
|
+
for (const file of manifest.files.userEditable) {
|
|
224
|
+
const dest = (0, path_1.join)(configDir, file.destination);
|
|
225
|
+
copyFile(file.source, dest, file.description, {
|
|
226
|
+
overwrite: 'createOnly'
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
console.log('');
|
|
230
|
+
// 4. Hooks (platform-specific)
|
|
231
|
+
console.log(chalk_1.default.gray('Hooks:'));
|
|
232
|
+
const hookFiles = isWindows ? manifest.files.hooks.powershell : manifest.files.hooks.bash;
|
|
233
|
+
for (const file of hookFiles) {
|
|
234
|
+
const dest = (0, path_1.join)(globalHooksDir, file.destination);
|
|
235
|
+
copyFile(file.source, dest, file.description, {
|
|
236
|
+
executable: file.executable,
|
|
237
|
+
overwrite: 'always'
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
console.log('');
|
|
241
|
+
// 5. Lib files (Unix only)
|
|
242
|
+
if (!isWindows) {
|
|
243
|
+
console.log(chalk_1.default.gray('Hook libraries:'));
|
|
244
|
+
for (const file of manifest.files.hooks.lib) {
|
|
245
|
+
const dest = (0, path_1.join)(globalHooksDir, file.destination);
|
|
246
|
+
copyFile(file.source, dest, file.description, {
|
|
247
|
+
executable: file.executable,
|
|
248
|
+
overwrite: 'always'
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
console.log('');
|
|
252
|
+
}
|
|
253
|
+
// Write installed-state manifest (only on success)
|
|
254
|
+
if (errorCount === 0) {
|
|
255
|
+
const installedManifest = {
|
|
256
|
+
version: manifest.manifestVersion,
|
|
257
|
+
installedAt: new Date().toISOString(),
|
|
258
|
+
installedBy: `@ekkos/cli@${require('../../package.json').version}`,
|
|
259
|
+
sourceManifestChecksum: sha256File(manifestData.path),
|
|
260
|
+
installedFiles
|
|
261
|
+
};
|
|
262
|
+
const installedManifestPath = (0, path_1.join)(globalHooksDir, '.ekkos-manifest.json');
|
|
263
|
+
(0, fs_1.writeFileSync)(installedManifestPath, JSON.stringify(installedManifest, null, 2));
|
|
264
|
+
if (verbose) {
|
|
265
|
+
console.log(chalk_1.default.gray(`Installed manifest: ${installedManifestPath}`));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
270
|
+
// PROJECT INSTALL
|
|
271
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
272
|
+
if (isProject) {
|
|
273
|
+
const projectRoot = findProjectRoot();
|
|
274
|
+
const projectHooksDir = (0, path_1.join)(projectRoot, '.claude', 'hooks');
|
|
275
|
+
const projectEkkosDir = (0, path_1.join)(projectRoot, '.ekkos');
|
|
276
|
+
console.log(chalk_1.default.cyan(`Installing project hooks for: ${projectRoot}`));
|
|
277
|
+
console.log('');
|
|
278
|
+
(0, fs_1.mkdirSync)(projectHooksDir, { recursive: true });
|
|
279
|
+
(0, fs_1.mkdirSync)(projectEkkosDir, { recursive: true });
|
|
280
|
+
// Use manifest.projectStubs if available, otherwise fallback to scanning directory
|
|
281
|
+
console.log(chalk_1.default.gray('Project stubs:'));
|
|
282
|
+
if (manifest.projectStubs) {
|
|
283
|
+
// Use manifest-driven installation
|
|
284
|
+
const stubFiles = isWindows ? manifest.projectStubs.powershell : manifest.projectStubs.bash;
|
|
285
|
+
for (const file of stubFiles) {
|
|
286
|
+
const sourcePath = (0, path_1.join)(templatesDir, file.source);
|
|
287
|
+
const destPath = (0, path_1.join)(projectHooksDir, file.destination);
|
|
288
|
+
if (!(0, fs_1.existsSync)(sourcePath)) {
|
|
289
|
+
console.log(chalk_1.default.yellow(` SKIP ${file.destination} - source not found`));
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
(0, fs_1.copyFileSync)(sourcePath, destPath);
|
|
294
|
+
if (file.executable && !isWindows) {
|
|
295
|
+
(0, fs_1.chmodSync)(destPath, '755');
|
|
296
|
+
}
|
|
297
|
+
console.log(chalk_1.default.green(` ✓ ${file.destination}`));
|
|
298
|
+
installedFiles.push({
|
|
299
|
+
path: destPath,
|
|
300
|
+
checksum: sha256File(destPath),
|
|
301
|
+
installedAt: new Date().toISOString()
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
console.log(chalk_1.default.red(` ✗ ${file.destination} - ${err.message}`));
|
|
306
|
+
errorCount++;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// Fallback: scan project-stubs directory
|
|
312
|
+
const stubsDir = (0, path_1.join)(templatesDir, 'project-stubs');
|
|
313
|
+
if ((0, fs_1.existsSync)(stubsDir)) {
|
|
314
|
+
const stubExt = isWindows ? '.ps1' : '.sh';
|
|
315
|
+
const stubFileNames = (0, fs_1.readdirSync)(stubsDir).filter(f => f.endsWith(stubExt));
|
|
316
|
+
for (const stubFile of stubFileNames) {
|
|
317
|
+
const sourcePath = (0, path_1.join)(stubsDir, stubFile);
|
|
318
|
+
const destPath = (0, path_1.join)(projectHooksDir, stubFile);
|
|
319
|
+
try {
|
|
320
|
+
(0, fs_1.copyFileSync)(sourcePath, destPath);
|
|
321
|
+
if (!isWindows) {
|
|
322
|
+
(0, fs_1.chmodSync)(destPath, '755');
|
|
323
|
+
}
|
|
324
|
+
console.log(chalk_1.default.green(` ✓ ${stubFile}`));
|
|
325
|
+
installedFiles.push({
|
|
326
|
+
path: destPath,
|
|
327
|
+
checksum: sha256File(destPath),
|
|
328
|
+
installedAt: new Date().toISOString()
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
console.log(chalk_1.default.yellow(` SKIP ${stubFile} - ${err.message}`));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
console.log(chalk_1.default.yellow(' No project stubs found - using global hooks'));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
console.log('');
|
|
341
|
+
// Write project installed manifest
|
|
342
|
+
if (errorCount === 0) {
|
|
343
|
+
const projectInstalledManifest = {
|
|
344
|
+
version: manifest.manifestVersion,
|
|
345
|
+
installedAt: new Date().toISOString(),
|
|
346
|
+
installedBy: `@ekkos/cli@${require('../../package.json').version}`,
|
|
347
|
+
sourceManifestChecksum: sha256File(manifestData.path),
|
|
348
|
+
installedFiles: installedFiles.filter(f => f.path.startsWith(projectRoot))
|
|
349
|
+
};
|
|
350
|
+
const projectManifestPath = (0, path_1.join)(projectEkkosDir, '.ekkos-project-manifest.json');
|
|
351
|
+
(0, fs_1.writeFileSync)(projectManifestPath, JSON.stringify(projectInstalledManifest, null, 2));
|
|
352
|
+
if (verbose) {
|
|
353
|
+
console.log(chalk_1.default.gray(`Project manifest: ${projectManifestPath}`));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Summary
|
|
358
|
+
console.log(chalk_1.default.gray('-'.repeat(50)));
|
|
359
|
+
if (errorCount === 0) {
|
|
360
|
+
console.log(chalk_1.default.green.bold(`✓ ${installedFiles.length} files installed successfully`));
|
|
361
|
+
console.log('');
|
|
362
|
+
console.log(chalk_1.default.gray('Run `ekkos hooks verify` to confirm installation.'));
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
console.log(chalk_1.default.red.bold(`✗ ${errorCount} errors during installation`));
|
|
366
|
+
console.log('');
|
|
367
|
+
console.log(chalk_1.default.yellow('Some files may not have been installed correctly.'));
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
console.log('');
|
|
371
|
+
}
|
|
372
|
+
async function hooksVerify(options) {
|
|
373
|
+
const verbose = options.verbose || false;
|
|
374
|
+
const checkGlobal = options.global !== false;
|
|
375
|
+
const checkProject = options.project === true;
|
|
376
|
+
console.log('');
|
|
377
|
+
console.log(chalk_1.default.cyan.bold('ekkOS Hooks Verification'));
|
|
378
|
+
console.log(chalk_1.default.gray('-'.repeat(50)));
|
|
379
|
+
console.log('');
|
|
380
|
+
const issues = [];
|
|
381
|
+
// Load source manifest for reference
|
|
382
|
+
const manifestData = loadManifest();
|
|
383
|
+
if (!manifestData) {
|
|
384
|
+
issues.push({
|
|
385
|
+
severity: 'error',
|
|
386
|
+
message: 'Source manifest not found - cannot verify'
|
|
387
|
+
});
|
|
388
|
+
return { status: 'FAIL', issues };
|
|
389
|
+
}
|
|
390
|
+
const { manifest } = manifestData;
|
|
391
|
+
const platformConfig = manifest.platforms[(0, os_1.platform)()];
|
|
392
|
+
const configDir = expandPath(platformConfig.configDir);
|
|
393
|
+
const globalHooksDir = expandPath(platformConfig.globalHooksDir);
|
|
394
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
395
|
+
// GLOBAL VERIFICATION
|
|
396
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
397
|
+
if (checkGlobal) {
|
|
398
|
+
console.log(chalk_1.default.cyan('Verifying global installation...'));
|
|
399
|
+
console.log('');
|
|
400
|
+
// Check installed-state manifest
|
|
401
|
+
const installedManifestPath = (0, path_1.join)(globalHooksDir, '.ekkos-manifest.json');
|
|
402
|
+
if (!(0, fs_1.existsSync)(installedManifestPath)) {
|
|
403
|
+
issues.push({
|
|
404
|
+
severity: 'error',
|
|
405
|
+
file: installedManifestPath,
|
|
406
|
+
message: 'Installed-state manifest not found. Run: ekkos hooks install --global'
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
try {
|
|
411
|
+
const installedManifest = JSON.parse((0, fs_1.readFileSync)(installedManifestPath, 'utf-8'));
|
|
412
|
+
console.log(chalk_1.default.gray(`Installed version: ${installedManifest.version}`));
|
|
413
|
+
console.log(chalk_1.default.gray(`Installed at: ${installedManifest.installedAt}`));
|
|
414
|
+
console.log(chalk_1.default.gray(`Installed by: ${installedManifest.installedBy}`));
|
|
415
|
+
console.log('');
|
|
416
|
+
// Verify each installed file
|
|
417
|
+
console.log(chalk_1.default.gray('Checking files:'));
|
|
418
|
+
for (const file of installedManifest.installedFiles) {
|
|
419
|
+
if (!(0, fs_1.existsSync)(file.path)) {
|
|
420
|
+
console.log(chalk_1.default.red(` ✗ ${(0, path_1.basename)(file.path)} - missing`));
|
|
421
|
+
issues.push({
|
|
422
|
+
severity: 'error',
|
|
423
|
+
file: file.path,
|
|
424
|
+
message: 'File missing'
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
const currentChecksum = sha256File(file.path);
|
|
429
|
+
if (currentChecksum !== file.checksum) {
|
|
430
|
+
console.log(chalk_1.default.yellow(` ○ ${(0, path_1.basename)(file.path)} - modified`));
|
|
431
|
+
issues.push({
|
|
432
|
+
severity: 'warning',
|
|
433
|
+
file: file.path,
|
|
434
|
+
message: 'File has been modified since installation'
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
console.log(chalk_1.default.green(` ✓ ${(0, path_1.basename)(file.path)}`));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
console.log('');
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
issues.push({
|
|
446
|
+
severity: 'error',
|
|
447
|
+
file: installedManifestPath,
|
|
448
|
+
message: `Invalid installed manifest: ${err.message}`
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Check required files
|
|
453
|
+
console.log(chalk_1.default.gray('Required files:'));
|
|
454
|
+
// Helper
|
|
455
|
+
const helperPath = (0, path_1.join)(configDir, '.helpers', 'json-parse.cjs');
|
|
456
|
+
if (!(0, fs_1.existsSync)(helperPath)) {
|
|
457
|
+
console.log(chalk_1.default.red(` ✗ json-parse.cjs - missing`));
|
|
458
|
+
issues.push({
|
|
459
|
+
severity: 'error',
|
|
460
|
+
file: helperPath,
|
|
461
|
+
message: 'JSON parse helper missing. Run: ekkos hooks install --global'
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
console.log(chalk_1.default.green(` ✓ json-parse.cjs`));
|
|
466
|
+
}
|
|
467
|
+
// Defaults
|
|
468
|
+
const defaultsPath = (0, path_1.join)(configDir, '.defaults', 'session-words.json');
|
|
469
|
+
if (!(0, fs_1.existsSync)(defaultsPath)) {
|
|
470
|
+
console.log(chalk_1.default.red(` ✗ session-words.json (defaults) - missing`));
|
|
471
|
+
issues.push({
|
|
472
|
+
severity: 'error',
|
|
473
|
+
file: defaultsPath,
|
|
474
|
+
message: 'Default session words missing. Run: ekkos hooks install --global'
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
console.log(chalk_1.default.green(` ✓ session-words.json (defaults)`));
|
|
479
|
+
}
|
|
480
|
+
// Check EKKOS_MANAGED header in hooks
|
|
481
|
+
console.log('');
|
|
482
|
+
console.log(chalk_1.default.gray('Hook fingerprints:'));
|
|
483
|
+
const hookFiles = isWindows ? manifest.files.hooks.powershell : manifest.files.hooks.bash;
|
|
484
|
+
for (const hookFile of hookFiles) {
|
|
485
|
+
const hookPath = (0, path_1.join)(globalHooksDir, hookFile.destination);
|
|
486
|
+
if ((0, fs_1.existsSync)(hookPath)) {
|
|
487
|
+
const content = (0, fs_1.readFileSync)(hookPath, 'utf-8');
|
|
488
|
+
if (content.includes('EKKOS_MANAGED=1')) {
|
|
489
|
+
console.log(chalk_1.default.green(` ✓ ${(0, path_1.basename)(hookPath)} - managed`));
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
console.log(chalk_1.default.yellow(` ○ ${(0, path_1.basename)(hookPath)} - custom (not managed)`));
|
|
493
|
+
issues.push({
|
|
494
|
+
severity: 'warning',
|
|
495
|
+
file: hookPath,
|
|
496
|
+
message: 'Hook does not have EKKOS_MANAGED=1 fingerprint'
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
console.log(chalk_1.default.red(` ✗ ${(0, path_1.basename)(hookFile.destination)} - missing`));
|
|
502
|
+
issues.push({
|
|
503
|
+
severity: 'error',
|
|
504
|
+
file: hookPath,
|
|
505
|
+
message: 'Hook file missing'
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
console.log('');
|
|
510
|
+
}
|
|
511
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
512
|
+
// PROJECT VERIFICATION
|
|
513
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
514
|
+
if (checkProject) {
|
|
515
|
+
const projectRoot = findProjectRoot();
|
|
516
|
+
const projectManifestPath = (0, path_1.join)(projectRoot, '.ekkos', '.ekkos-project-manifest.json');
|
|
517
|
+
console.log(chalk_1.default.cyan(`Verifying project installation: ${projectRoot}`));
|
|
518
|
+
console.log('');
|
|
519
|
+
if (!(0, fs_1.existsSync)(projectManifestPath)) {
|
|
520
|
+
issues.push({
|
|
521
|
+
severity: 'warning',
|
|
522
|
+
file: projectManifestPath,
|
|
523
|
+
message: 'No project-level hooks installed (using global)'
|
|
524
|
+
});
|
|
525
|
+
console.log(chalk_1.default.yellow(' No project-level hooks installed'));
|
|
526
|
+
console.log(chalk_1.default.gray(' Using global hooks from ~/.claude/hooks/'));
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
const projectManifest = JSON.parse((0, fs_1.readFileSync)(projectManifestPath, 'utf-8'));
|
|
530
|
+
console.log(chalk_1.default.gray(`Project hooks version: ${projectManifest.version}`));
|
|
531
|
+
for (const file of projectManifest.installedFiles) {
|
|
532
|
+
if (!(0, fs_1.existsSync)(file.path)) {
|
|
533
|
+
console.log(chalk_1.default.red(` ✗ ${(0, path_1.basename)(file.path)} - missing`));
|
|
534
|
+
issues.push({ severity: 'error', file: file.path, message: 'Missing' });
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
console.log(chalk_1.default.green(` ✓ ${(0, path_1.basename)(file.path)}`));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
console.log('');
|
|
542
|
+
}
|
|
543
|
+
// Summary
|
|
544
|
+
console.log(chalk_1.default.gray('-'.repeat(50)));
|
|
545
|
+
const errors = issues.filter(i => i.severity === 'error').length;
|
|
546
|
+
const warnings = issues.filter(i => i.severity === 'warning').length;
|
|
547
|
+
let status = 'PASS';
|
|
548
|
+
if (errors > 0) {
|
|
549
|
+
status = 'FAIL';
|
|
550
|
+
console.log(chalk_1.default.red.bold(`FAIL: ${errors} error(s), ${warnings} warning(s)`));
|
|
551
|
+
}
|
|
552
|
+
else if (warnings > 0) {
|
|
553
|
+
status = 'WARN';
|
|
554
|
+
console.log(chalk_1.default.yellow.bold(`WARN: ${warnings} warning(s)`));
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
console.log(chalk_1.default.green.bold('PASS: All checks passed'));
|
|
558
|
+
}
|
|
559
|
+
console.log('');
|
|
560
|
+
return { status, issues };
|
|
561
|
+
}
|
|
562
|
+
async function hooksStatus(options) {
|
|
563
|
+
const verbose = options.verbose || false;
|
|
564
|
+
console.log('');
|
|
565
|
+
console.log(chalk_1.default.cyan.bold('ekkOS Hooks Status'));
|
|
566
|
+
console.log(chalk_1.default.gray('-'.repeat(50)));
|
|
567
|
+
console.log('');
|
|
568
|
+
const manifestData = loadManifest();
|
|
569
|
+
const platformConfig = manifestData?.manifest.platforms[(0, os_1.platform)()];
|
|
570
|
+
const configDir = platformConfig ? expandPath(platformConfig.configDir) : (0, path_1.join)(HOME_DIR, '.ekkos');
|
|
571
|
+
const globalHooksDir = platformConfig ? expandPath(platformConfig.globalHooksDir) : (0, path_1.join)(HOME_DIR, '.claude', 'hooks');
|
|
572
|
+
const projectRoot = findProjectRoot();
|
|
573
|
+
const projectHooksDir = (0, path_1.join)(projectRoot, '.claude', 'hooks');
|
|
574
|
+
// Display directories
|
|
575
|
+
console.log(chalk_1.default.gray('Directories:'));
|
|
576
|
+
console.log(` Config: ${configDir}`);
|
|
577
|
+
console.log(` Global hooks: ${globalHooksDir}`);
|
|
578
|
+
console.log(` Project: ${projectRoot}`);
|
|
579
|
+
console.log('');
|
|
580
|
+
// Check global installation
|
|
581
|
+
console.log(chalk_1.default.cyan('Global Installation:'));
|
|
582
|
+
const globalManifestPath = (0, path_1.join)(globalHooksDir, '.ekkos-manifest.json');
|
|
583
|
+
if ((0, fs_1.existsSync)(globalManifestPath)) {
|
|
584
|
+
const globalManifest = JSON.parse((0, fs_1.readFileSync)(globalManifestPath, 'utf-8'));
|
|
585
|
+
console.log(chalk_1.default.green(` ✓ Installed (v${globalManifest.version})`));
|
|
586
|
+
console.log(chalk_1.default.gray(` Installed: ${new Date(globalManifest.installedAt).toLocaleString()}`));
|
|
587
|
+
console.log(chalk_1.default.gray(` Files: ${globalManifest.installedFiles.length}`));
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
console.log(chalk_1.default.yellow(' ○ Not installed'));
|
|
591
|
+
console.log(chalk_1.default.gray(' Run: ekkos hooks install --global'));
|
|
592
|
+
}
|
|
593
|
+
console.log('');
|
|
594
|
+
// Check hooks enablement
|
|
595
|
+
console.log(chalk_1.default.cyan('Hook Enablement:'));
|
|
596
|
+
const hooksEnabledPath = (0, path_1.join)(configDir, 'hooks-enabled.json');
|
|
597
|
+
if ((0, fs_1.existsSync)(hooksEnabledPath)) {
|
|
598
|
+
try {
|
|
599
|
+
const hooksEnabled = JSON.parse((0, fs_1.readFileSync)(hooksEnabledPath, 'utf-8'));
|
|
600
|
+
const targets = hooksEnabled.targets || {};
|
|
601
|
+
for (const [target, config] of Object.entries(targets)) {
|
|
602
|
+
const hooks = config || {};
|
|
603
|
+
const enabled = Object.entries(hooks)
|
|
604
|
+
.filter(([, v]) => v === true)
|
|
605
|
+
.map(([k]) => k);
|
|
606
|
+
const disabled = Object.entries(hooks)
|
|
607
|
+
.filter(([, v]) => v === false)
|
|
608
|
+
.map(([k]) => k);
|
|
609
|
+
console.log(` ${target}:`);
|
|
610
|
+
if (enabled.length > 0) {
|
|
611
|
+
console.log(chalk_1.default.green(` Enabled: ${enabled.join(', ')}`));
|
|
612
|
+
}
|
|
613
|
+
if (disabled.length > 0) {
|
|
614
|
+
console.log(chalk_1.default.yellow(` Disabled: ${disabled.join(', ')}`));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
console.log(chalk_1.default.yellow(' ○ Could not read hooks-enabled.json'));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
console.log(chalk_1.default.gray(' Using defaults (all enabled)'));
|
|
624
|
+
}
|
|
625
|
+
console.log('');
|
|
626
|
+
// List hooks with modification times
|
|
627
|
+
console.log(chalk_1.default.cyan('Installed Hooks:'));
|
|
628
|
+
const hookExt = isWindows ? '.ps1' : '.sh';
|
|
629
|
+
if ((0, fs_1.existsSync)(globalHooksDir)) {
|
|
630
|
+
const hookFiles = (0, fs_1.readdirSync)(globalHooksDir)
|
|
631
|
+
.filter(f => f.endsWith(hookExt))
|
|
632
|
+
.sort();
|
|
633
|
+
for (const hookFile of hookFiles) {
|
|
634
|
+
const hookPath = (0, path_1.join)(globalHooksDir, hookFile);
|
|
635
|
+
const stat = (0, fs_1.statSync)(hookPath);
|
|
636
|
+
const mtime = stat.mtime.toLocaleString();
|
|
637
|
+
const content = (0, fs_1.readFileSync)(hookPath, 'utf-8');
|
|
638
|
+
const isManaged = content.includes('EKKOS_MANAGED=1');
|
|
639
|
+
const icon = isManaged ? chalk_1.default.green('✓') : chalk_1.default.yellow('○');
|
|
640
|
+
const tag = isManaged ? chalk_1.default.gray('[managed]') : chalk_1.default.yellow('[custom]');
|
|
641
|
+
console.log(` ${icon} ${hookFile} ${tag}`);
|
|
642
|
+
if (verbose) {
|
|
643
|
+
console.log(chalk_1.default.gray(` Modified: ${mtime}`));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
console.log(chalk_1.default.gray(' No hooks directory found'));
|
|
649
|
+
}
|
|
650
|
+
console.log('');
|
|
651
|
+
// Check project-level hooks
|
|
652
|
+
console.log(chalk_1.default.cyan('Project Hooks:'));
|
|
653
|
+
if ((0, fs_1.existsSync)(projectHooksDir) && projectHooksDir !== globalHooksDir) {
|
|
654
|
+
const projectHookFiles = (0, fs_1.readdirSync)(projectHooksDir).filter(f => f.endsWith(hookExt));
|
|
655
|
+
if (projectHookFiles.length > 0) {
|
|
656
|
+
for (const hookFile of projectHookFiles) {
|
|
657
|
+
console.log(chalk_1.default.green(` ✓ ${hookFile}`));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
console.log(chalk_1.default.gray(' Using global hooks'));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
console.log(chalk_1.default.gray(' Using global hooks'));
|
|
666
|
+
}
|
|
667
|
+
console.log('');
|
|
668
|
+
}
|