This commit is contained in:
19
packages/hive-mind-hooks-codex-desktop/LICENSE
Normal file
19
packages/hive-mind-hooks-codex-desktop/LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright 2026 Egzakta Group d.o.o. · waggle-os.ai
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Full license text: https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
73
packages/hive-mind-hooks-codex-desktop/README.md
Normal file
73
packages/hive-mind-hooks-codex-desktop/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# @waggle/hive-mind-hooks-codex-desktop
|
||||
|
||||
Silent-capture shim that wires **OpenAI Codex Desktop** lifecycle hooks into
|
||||
[hive-mind](https://github.com/marolinik/hive-mind) frames. Codex Desktop shares
|
||||
the **same `~/.codex/` config root** as the Codex CLI — there is no separate
|
||||
codex-desktop config directory — so this package is a **thin re-export of
|
||||
[`@waggle/hive-mind-hooks-codex`](../hive-mind-hooks-codex)**. Install / uninstall
|
||||
/ verify behave **identically** and write into the same `~/.codex/hooks.json` +
|
||||
pointer (`~/.codex/hive-mind-install.json`).
|
||||
|
||||
It exists as a distinct package so the dependency graph + OSS subtree-split see
|
||||
the package boundary, and so the launcher's `hookPackageFor('codex-desktop')`
|
||||
resolves to this package.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npx @waggle/hive-mind-hooks-codex-desktop install
|
||||
# Windows / production: pin the CLI path
|
||||
npx @waggle/hive-mind-hooks-codex-desktop install --cli-path "C:\\path\\to\\hive-mind-cli\\dist\\index.js"
|
||||
```
|
||||
|
||||
The installer additively merges four hook groups into `~/.codex/hooks.json`
|
||||
(creating the file if absent), preserving any existing hooks you have, and
|
||||
writes a pointer + byte-identical backup so uninstall is exact. This is the
|
||||
**same file** the Codex CLI shim writes — installing one is installing the other.
|
||||
|
||||
> **One-time trust step.** Non-managed Codex hooks require a one-time `/hooks`
|
||||
> trust before they execute. After installing, run `/hooks` in Codex once to
|
||||
> trust the hive-mind hooks.
|
||||
|
||||
```bash
|
||||
npx @waggle/hive-mind-hooks-codex-desktop verify # smoke-check + surface admin lockdown
|
||||
npx @waggle/hive-mind-hooks-codex-desktop uninstall # byte-identical restore (or remove if we created it)
|
||||
```
|
||||
|
||||
`verify` also surfaces the admin lockdown `allow_managed_hooks_only = true`
|
||||
(in `~/.codex/requirements.toml`), which suppresses user hooks so install would
|
||||
silently no-op.
|
||||
|
||||
## Capture fidelity
|
||||
|
||||
| Lifecycle | Codex event | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| SessionStart (recall + inject) | `SessionStart` | full | matcher `startup\|resume\|clear\|compact`; injects recalled frames as additional context |
|
||||
| UserPromptSubmit (save temporary) | `UserPromptSubmit` | full | persists the prompt as a temporary frame |
|
||||
| Stop (summarize + save) | `Stop` | full | reads `last_assistant_message` in the response fallback; saves an important/critical frame |
|
||||
| PreCompact (compact memory) | `PreCompact` | full | carries `trigger` (`manual\|auto`); runs `cleanup_frames` before truncation |
|
||||
|
||||
**Full parity** with claude-code — no degraded or absent events. The only
|
||||
non-silent step is the one-time `/hooks` trust step above (an install-UX step,
|
||||
not a degraded event).
|
||||
|
||||
> **Runtime-verification note.** The **Codex CLI definitely fires** these hooks
|
||||
> (the shared `~/.codex/hooks.json` is the CLI's own hook surface). Whether the
|
||||
> Codex **Desktop App** honors the same `~/.codex/hooks.json` is **doc-asserted
|
||||
> and still needs runtime verification on a real App install**. If you run the
|
||||
> desktop App, confirm capture lands in your hive-mind frames after a session.
|
||||
|
||||
## How it works
|
||||
|
||||
This package re-exports the codex installer surface
|
||||
(`export * from '@waggle/hive-mind-hooks-codex'`); the `codex-desktop-hooks` bin
|
||||
delegates to the codex `install` / `uninstall` / `verify`. The compiled hook
|
||||
scripts live in the codex package's `dist/hooks/` (resolved by the codex
|
||||
installer's own module URL), so codex-desktop ships no duplicate hook bodies.
|
||||
|
||||
Each hook is a short-lived Node subprocess. Codex pipes the event JSON to the
|
||||
hook's stdin; the hook shells to `hive-mind-cli` to recall or save frames, then
|
||||
exits 0. **Fail-open:** if `hive-mind-cli` is unreachable or the payload is
|
||||
malformed, the hook logs to stderr and exits 0 — Codex is never blocked.
|
||||
|
||||
License: Apache-2.0.
|
||||
62
packages/hive-mind-hooks-codex-desktop/package.json
Normal file
62
packages/hive-mind-hooks-codex-desktop/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "@waggle/hive-mind-hooks-codex-desktop",
|
||||
"version": "0.1.0",
|
||||
"description": "OpenAI Codex Desktop silent capture shim for hive-mind. A thin re-export of @waggle/hive-mind-hooks-codex — Codex Desktop shares the same ~/.codex/ config root as the Codex CLI, so install / uninstall / verify behave identically and write into the same ~/.codex/hooks.json + pointer. Exists as a distinct package so the dependency graph + subtree-split see the boundary and the launcher resolves hookPackageFor('codex-desktop').",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"codex-desktop-hooks": "dist/bin/codex-desktop-hooks.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"build:clean": "tsc --build --clean",
|
||||
"typecheck": "tsc --build && tsc --noEmit -p tsconfig.test.json",
|
||||
"test": "node ../../node_modules/vitest/vitest.mjs run --root ../.. --config vitest.config.ts packages/hive-mind-hooks-codex-desktop/tests",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@waggle/hive-mind-hooks-codex": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@waggle/hive-mind-cli": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@waggle/hive-mind-cli": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/marolinik/waggle-os.git",
|
||||
"directory": "packages/hive-mind-hooks-codex-desktop"
|
||||
},
|
||||
"homepage": "https://github.com/marolinik/waggle-os/tree/main/packages/hive-mind-hooks-codex-desktop#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/marolinik/waggle-os/issues"
|
||||
},
|
||||
"author": "Egzakta Group d.o.o. <hello@egzakta.com> (https://egzakta.com)",
|
||||
"keywords": [
|
||||
"codex-desktop",
|
||||
"codex",
|
||||
"openai",
|
||||
"hive-mind",
|
||||
"memory",
|
||||
"ai",
|
||||
"hook",
|
||||
"silent-capture"
|
||||
],
|
||||
"types": "dist/index.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* `codex-desktop-hooks` — CLI entry for the @waggle/hive-mind-hooks-codex-desktop shim.
|
||||
*
|
||||
* codex-desktop-hooks install Patch ~/.codex/hooks.json (additive, create-if-missing).
|
||||
* codex-desktop-hooks uninstall Restore the byte-identical pre-install state (or remove
|
||||
* the hooks.json we created).
|
||||
* codex-desktop-hooks verify Smoke-check the install + hive-mind-cli reachability.
|
||||
*
|
||||
* Codex Desktop shares the same `~/.codex/` config root as the Codex CLI, so
|
||||
* this delegates to the codex installer (`@waggle/hive-mind-hooks-codex`) and
|
||||
* behaves IDENTICALLY to `codex-hooks` — same `~/.codex/hooks.json`, same
|
||||
* pointer (`~/.codex/hive-mind-install.json`), same compiled hook scripts (the
|
||||
* codex package's `dist/hooks/`, resolved by the codex installer's own module
|
||||
* URL).
|
||||
*/
|
||||
|
||||
import {
|
||||
install,
|
||||
uninstall,
|
||||
verify,
|
||||
type InstallResult,
|
||||
type UninstallResult,
|
||||
type VerifyResult,
|
||||
} from '@waggle/hive-mind-hooks-codex';
|
||||
|
||||
type ParsedArgs = {
|
||||
command: 'install' | 'uninstall' | 'verify' | 'help';
|
||||
flags: Record<string, string | boolean>;
|
||||
};
|
||||
|
||||
function parseArgs(argv: readonly string[]): ParsedArgs {
|
||||
const [first, ...rest] = argv;
|
||||
const valid = ['install', 'uninstall', 'verify'] as const;
|
||||
const command = first === '-h' || first === '--help' || first === undefined
|
||||
? 'help'
|
||||
: valid.includes(first as typeof valid[number]) ? first as typeof valid[number] : 'help';
|
||||
|
||||
const flags: Record<string, string | boolean> = {};
|
||||
for (let i = 0; i < rest.length; i += 1) {
|
||||
const arg = rest[i];
|
||||
if (!arg.startsWith('--')) continue;
|
||||
const eq = arg.indexOf('=');
|
||||
if (eq !== -1) {
|
||||
flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
||||
} else {
|
||||
const next = rest[i + 1];
|
||||
if (next && !next.startsWith('--')) {
|
||||
flags[arg.slice(2)] = next;
|
||||
i += 1;
|
||||
} else {
|
||||
flags[arg.slice(2)] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { command: command as ParsedArgs['command'], flags };
|
||||
}
|
||||
|
||||
function printHelp(): void {
|
||||
process.stdout.write([
|
||||
'Usage: codex-desktop-hooks <command> [options]',
|
||||
'',
|
||||
'Codex Desktop shares ~/.codex/ with the Codex CLI; this delegates to',
|
||||
'the @waggle/hive-mind-hooks-codex installer and behaves identically.',
|
||||
'',
|
||||
'Commands:',
|
||||
' install Patch ~/.codex/hooks.json (additive, create-if-missing, with backup).',
|
||||
' uninstall Restore the byte-identical pre-install hooks.json (or remove it if created).',
|
||||
' verify Smoke-check the install + hive-mind-cli reachability.',
|
||||
'',
|
||||
'Options:',
|
||||
' --help, -h Show this help.',
|
||||
' --hooks-dir <PATH> Override compiled hooks directory (testing).',
|
||||
' --hook-timeout <S> Override per-hook timeout in seconds (default 5).',
|
||||
' --cli-path <PATH> Absolute path to the hive-mind-cli binary or its',
|
||||
' compiled JS entry. Required on Windows (npm bin',
|
||||
' is a .cmd shim) and recommended for production',
|
||||
' installs. Threaded into every hook command.',
|
||||
'',
|
||||
'Repo: https://github.com/marolinik/waggle-os',
|
||||
'',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
function printInstallSummary(result: InstallResult): void {
|
||||
const lines: string[] = [
|
||||
'hive-mind/codex-desktop-hooks: install',
|
||||
` - hooks.json: ${result.paths.configPath}`,
|
||||
` - backup: ${result.backupPath ?? '(none — hooks.json created by us)'}`,
|
||||
` - pointer: ${result.pointerPath}`,
|
||||
` - added hooks: ${result.installedHooks.join(', ')}`,
|
||||
` - cli path: ${result.cliPath ?? '(default — hive-mind-cli on PATH)'}`,
|
||||
'',
|
||||
'Shared config root:',
|
||||
' Codex Desktop honors the same ~/.codex/hooks.json as the Codex CLI.',
|
||||
'',
|
||||
'One-time trust step required:',
|
||||
' Run `/hooks` in Codex once to trust the hive-mind hooks before they execute.',
|
||||
'',
|
||||
'Note: the Codex CLI definitely fires these hooks. The desktop App honoring',
|
||||
'hooks is doc-asserted and still needs runtime verification on a real App',
|
||||
'install.',
|
||||
'',
|
||||
'Done. New Codex sessions will silently capture to hive-mind.',
|
||||
'Run "codex-desktop-hooks verify" to inspect, "codex-desktop-hooks uninstall" to revert.',
|
||||
'',
|
||||
];
|
||||
process.stdout.write(lines.join('\n'));
|
||||
}
|
||||
|
||||
function printUninstallSummary(result: UninstallResult): void {
|
||||
const lines: string[] = [
|
||||
'hive-mind/codex-desktop-hooks: uninstall',
|
||||
` - hooks.json: ${result.paths.configPath}`,
|
||||
` - restored from: ${result.restoredFrom ?? '(none — removed file we created)'}`,
|
||||
` - created removed: ${result.createdRemoved ? 'yes' : 'no'}`,
|
||||
` - backup removed: ${result.backupRemoved ? 'yes' : 'no (kept on disk)'}`,
|
||||
` - pointer removed: ${result.pointerRemoved ? 'yes' : 'no'}`,
|
||||
'',
|
||||
'Done. hooks.json is byte-identical to pre-install state (or removed if we created it).',
|
||||
'',
|
||||
];
|
||||
process.stdout.write(lines.join('\n'));
|
||||
}
|
||||
|
||||
function printVerifySummary(result: VerifyResult): void {
|
||||
const lines: string[] = ['hive-mind/codex-desktop-hooks: verify'];
|
||||
for (const c of result.checks) {
|
||||
const tag = c.ok ? 'PASS' : 'FAIL';
|
||||
const detail = c.detail ? ` — ${c.detail}` : '';
|
||||
lines.push(` [${tag}] ${c.name}${detail}`);
|
||||
}
|
||||
lines.push('');
|
||||
lines.push(result.ok ? 'All checks passed.' : 'One or more checks failed.');
|
||||
lines.push('');
|
||||
process.stdout.write(lines.join('\n'));
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const { command, flags } = parseArgs(process.argv.slice(2));
|
||||
if (command === 'help') {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const hooksDir = typeof flags['hooks-dir'] === 'string' ? flags['hooks-dir'] : undefined;
|
||||
const hookTimeoutRaw = flags['hook-timeout'];
|
||||
const hookTimeoutSeconds = typeof hookTimeoutRaw === 'string'
|
||||
? Number.parseInt(hookTimeoutRaw, 10) || undefined
|
||||
: undefined;
|
||||
const cliPath = typeof flags['cli-path'] === 'string' ? flags['cli-path'] : undefined;
|
||||
|
||||
const baseOpts = {
|
||||
...(hooksDir ? { hooksDir } : {}),
|
||||
};
|
||||
|
||||
try {
|
||||
if (command === 'install') {
|
||||
const installOpts = {
|
||||
...baseOpts,
|
||||
...(hookTimeoutSeconds !== undefined ? { hookTimeoutSeconds } : {}),
|
||||
...(cliPath !== undefined ? { cliPath } : {}),
|
||||
};
|
||||
const result = await install(installOpts);
|
||||
printInstallSummary(result);
|
||||
return;
|
||||
}
|
||||
if (command === 'uninstall') {
|
||||
const result = await uninstall(baseOpts);
|
||||
printUninstallSummary(result);
|
||||
return;
|
||||
}
|
||||
if (command === 'verify') {
|
||||
const result = await verify(baseOpts);
|
||||
printVerifySummary(result);
|
||||
if (!result.ok) process.exit(1);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void main();
|
||||
17
packages/hive-mind-hooks-codex-desktop/src/index.ts
Normal file
17
packages/hive-mind-hooks-codex-desktop/src/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @waggle/hive-mind-hooks-codex-desktop — barrel export.
|
||||
*
|
||||
* Codex Desktop silent-capture shim for hive-mind. Codex Desktop shares the
|
||||
* SAME `~/.codex/` config root as the Codex CLI — there is no separate
|
||||
* codex-desktop config directory — so this package is a THIN RE-EXPORT of
|
||||
* `@waggle/hive-mind-hooks-codex`. Install / uninstall / verify behave
|
||||
* identically and write into the same `~/.codex/hooks.json` + pointer
|
||||
* (`~/.codex/hive-mind-install.json`).
|
||||
*
|
||||
* It exists as a distinct package so the dependency graph + OSS subtree-split
|
||||
* see the package boundary, and so the launcher's
|
||||
* `hookPackageFor('codex-desktop')` resolves to this package. Most users
|
||||
* invoke the `codex-desktop-hooks` bin (which delegates to the codex installer).
|
||||
*/
|
||||
|
||||
export * from '@waggle/hive-mind-hooks-codex';
|
||||
167
packages/hive-mind-hooks-codex-desktop/tests/parity.test.ts
Normal file
167
packages/hive-mind-hooks-codex-desktop/tests/parity.test.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { describe, expect, it, afterEach } from 'vitest';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdtemp, mkdir, readFile, writeFile, rm } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
// codex-desktop is a THIN RE-EXPORT — install/uninstall/verify come from the
|
||||
// codex package via `export * from '@waggle/hive-mind-hooks-codex'`. Importing
|
||||
// them through the desktop barrel proves the re-export is wired and that the
|
||||
// SAME installer writes the SAME ~/.codex/hooks.json + pointer as codex.
|
||||
import { install, uninstall } from '../src/index.js';
|
||||
import { install as codexInstall } from '@waggle/hive-mind-hooks-codex';
|
||||
import { HIVE_MIND_MARKER } from '@waggle/hive-mind-hooks-codex';
|
||||
|
||||
interface TestEnv {
|
||||
home: string;
|
||||
hooksDir: string;
|
||||
configPath: string;
|
||||
pointerPath: string;
|
||||
}
|
||||
|
||||
/** Codex Desktop shares ~/.codex/ — same config + pointer paths as the CLI. */
|
||||
async function bootstrap(initial: Record<string, unknown> | undefined): Promise<TestEnv> {
|
||||
const home = await mkdtemp(join(tmpdir(), 'hmcdxd-parity-'));
|
||||
const codexDir = join(home, '.codex');
|
||||
await mkdir(codexDir, { recursive: true });
|
||||
const configPath = join(codexDir, 'hooks.json');
|
||||
if (initial !== undefined) {
|
||||
await writeFile(configPath, JSON.stringify(initial, null, 2) + '\n', 'utf-8');
|
||||
}
|
||||
const hooksDir = resolve(home, 'fake-dist', 'hooks');
|
||||
await mkdir(hooksDir, { recursive: true });
|
||||
return { home, hooksDir, configPath, pointerPath: join(codexDir, 'hive-mind-install.json') };
|
||||
}
|
||||
|
||||
function sha256(s: string): string {
|
||||
return createHash('sha256').update(s, 'utf-8').digest('hex');
|
||||
}
|
||||
|
||||
describe('parity (codex-desktop ↔ codex)', () => {
|
||||
let env: TestEnv;
|
||||
|
||||
afterEach(async () => {
|
||||
if (env) await rm(env.home, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ── shared ~/.codex/ surface ──────────────────────────────────────────
|
||||
|
||||
it('installs into the SAME ~/.codex/hooks.json the Codex CLI uses', async () => {
|
||||
env = await bootstrap(undefined);
|
||||
expect(existsSync(env.configPath)).toBe(false);
|
||||
|
||||
const result = await install({ home: env.home, hooksDir: env.hooksDir });
|
||||
|
||||
// The config + pointer land at the shared ~/.codex/ paths.
|
||||
expect(result.paths.configPath).toBe(env.configPath);
|
||||
expect(result.pointerPath).toBe(env.pointerPath);
|
||||
expect(existsSync(env.configPath)).toBe(true);
|
||||
expect(existsSync(env.pointerPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('writes the hive group (marker) + all four lifecycle events', async () => {
|
||||
env = await bootstrap(undefined);
|
||||
await install({ home: env.home, hooksDir: env.hooksDir });
|
||||
|
||||
const after = JSON.parse(await readFile(env.configPath, 'utf-8')) as {
|
||||
hooks: Record<string, Array<{ _hiveMindShim?: string }>>;
|
||||
};
|
||||
expect(Object.keys(after.hooks).sort()).toEqual(
|
||||
['PreCompact', 'SessionStart', 'Stop', 'UserPromptSubmit'],
|
||||
);
|
||||
// Every group we added carries the codex hive marker.
|
||||
for (const event of Object.keys(after.hooks)) {
|
||||
expect(after.hooks[event][0]._hiveMindShim).toBe(HIVE_MIND_MARKER);
|
||||
}
|
||||
});
|
||||
|
||||
it('writes a pointer carrying the codex installed_hooks list', async () => {
|
||||
env = await bootstrap(undefined);
|
||||
const result = await install({ home: env.home, hooksDir: env.hooksDir });
|
||||
const pointer = JSON.parse(await readFile(result.pointerPath, 'utf-8')) as Record<string, unknown>;
|
||||
expect(pointer['installed_hooks']).toEqual(
|
||||
['session-start', 'user-prompt-submit', 'stop', 'pre-compact'],
|
||||
);
|
||||
expect(pointer['config_path']).toBe(env.configPath);
|
||||
});
|
||||
|
||||
// ── byte-for-byte identical to the codex installer ────────────────────
|
||||
|
||||
it('produces a byte-identical hooks.json to the codex installer (same inputs)', async () => {
|
||||
const initial = {
|
||||
hooks: {
|
||||
SessionStart: [
|
||||
{ matcher: 'startup', hooks: [{ type: 'command', command: 'node /existing/ctx.js' }] },
|
||||
],
|
||||
},
|
||||
};
|
||||
const fixedTs = '2026-04-28T10:30:45.123Z';
|
||||
|
||||
// Desktop install into env A.
|
||||
env = await bootstrap(initial);
|
||||
await install({
|
||||
home: env.home,
|
||||
hooksDir: env.hooksDir,
|
||||
now: () => new Date(fixedTs),
|
||||
});
|
||||
const desktopConfig = await readFile(env.configPath, 'utf-8');
|
||||
|
||||
// Codex install into env B with IDENTICAL inputs — same hooksDir so the
|
||||
// embedded hook command strings (which carry the absolute hooksDir path)
|
||||
// match. Only the HOME differs (each install writes to its own ~/.codex/),
|
||||
// and HOME does not appear in the emitted config.
|
||||
const envB = await bootstrap(initial);
|
||||
try {
|
||||
await codexInstall({
|
||||
home: envB.home,
|
||||
hooksDir: env.hooksDir,
|
||||
now: () => new Date(fixedTs),
|
||||
});
|
||||
const codexConfig = await readFile(envB.configPath, 'utf-8');
|
||||
expect(sha256(desktopConfig)).toBe(sha256(codexConfig));
|
||||
} finally {
|
||||
await rm(envB.home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ── reversibility: uninstall restores the shared file ─────────────────
|
||||
|
||||
it('uninstall removes the hooks.json we created and leaves NO orphan', async () => {
|
||||
env = await bootstrap(undefined);
|
||||
const result = await install({ home: env.home, hooksDir: env.hooksDir });
|
||||
expect(result.createdByUs).toBe(true);
|
||||
expect(existsSync(env.configPath)).toBe(true);
|
||||
|
||||
const u = await uninstall({ home: env.home, hooksDir: env.hooksDir });
|
||||
expect(u.createdRemoved).toBe(true);
|
||||
expect(u.restoredFrom).toBeNull();
|
||||
expect(existsSync(env.configPath)).toBe(false);
|
||||
expect(existsSync(env.pointerPath)).toBe(false);
|
||||
});
|
||||
|
||||
it('uninstall restores a pre-existing hooks.json byte-identically', async () => {
|
||||
const initial = {
|
||||
schemaVersion: 1,
|
||||
hooks: {
|
||||
SessionStart: [
|
||||
{ matcher: 'startup', hooks: [{ type: 'command', command: 'node /existing/ctx.js' }] },
|
||||
],
|
||||
},
|
||||
};
|
||||
env = await bootstrap(initial);
|
||||
const preInstall = await readFile(env.configPath, 'utf-8');
|
||||
const preHash = sha256(preInstall);
|
||||
|
||||
await install({ home: env.home, hooksDir: env.hooksDir });
|
||||
const afterInstall = await readFile(env.configPath, 'utf-8');
|
||||
expect(sha256(afterInstall)).not.toBe(preHash);
|
||||
|
||||
const u = await uninstall({ home: env.home, hooksDir: env.hooksDir });
|
||||
expect(u.createdRemoved).toBe(false);
|
||||
const afterUninstall = await readFile(env.configPath, 'utf-8');
|
||||
expect(sha256(afterUninstall)).toBe(preHash);
|
||||
expect(afterUninstall).toBe(preInstall);
|
||||
// backup + pointer cleaned up.
|
||||
expect(existsSync(env.pointerPath)).toBe(false);
|
||||
});
|
||||
});
|
||||
14
packages/hive-mind-hooks-codex-desktop/tsconfig.json
Normal file
14
packages/hive-mind-hooks-codex-desktop/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"tsBuildInfoFile": "./dist/.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.test.ts", "tests/**", "dist/**", "node_modules/**"],
|
||||
"references": [
|
||||
{ "path": "../hive-mind-hooks-codex" }
|
||||
]
|
||||
}
|
||||
12
packages/hive-mind-hooks-codex-desktop/tsconfig.test.json
Normal file
12
packages/hive-mind-hooks-codex-desktop/tsconfig.test.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"composite": false,
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"sourceMap": false
|
||||
},
|
||||
"include": ["src/**/*.ts", "tests/**/*.ts"],
|
||||
"exclude": ["dist/**", "node_modules/**"]
|
||||
}
|
||||
Reference in New Issue
Block a user