@ghl-ai/aw 0.1.30-beta.3 → 0.1.30-beta.6
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/hooks.mjs +144 -0
- package/package.json +3 -2
- package/update.mjs +74 -21
package/hooks.mjs
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// hooks.mjs — Global git hooks via core.hooksPath (git-lfs pattern).
|
|
2
|
+
//
|
|
3
|
+
// installGlobalHooks() → writes dispatcher scripts to ~/.aw/hooks/,
|
|
4
|
+
// sets core.hooksPath with chaining to .git/hooks/
|
|
5
|
+
// removeGlobalHooks() → restores previous core.hooksPath, removes ~/.aw/hooks/
|
|
6
|
+
|
|
7
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync, rmSync, readdirSync } from 'node:fs';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import * as fmt from './fmt.mjs';
|
|
12
|
+
|
|
13
|
+
const HOME = homedir();
|
|
14
|
+
const HOOKS_DIR = join(HOME, '.aw', 'hooks');
|
|
15
|
+
const PREV_PATH_FILE = join(HOOKS_DIR, '.previous-hooks-path');
|
|
16
|
+
|
|
17
|
+
// ── Dispatcher scripts ─────────────────────────────────────────────────
|
|
18
|
+
// Each hook: (1) runs aw logic in background, (2) chains to previous
|
|
19
|
+
// hooksPath if another tool owned it, (3) chains to repo-local .git/hooks/.
|
|
20
|
+
// `exec` replaces the shell process so the local hook's exit code propagates.
|
|
21
|
+
|
|
22
|
+
function makeDispatcher(hookName, awBlock) {
|
|
23
|
+
return `#!/bin/sh
|
|
24
|
+
# aw: global ${hookName} dispatcher (installed by aw init)
|
|
25
|
+
# Chains to local hooks — see https://git-scm.com/docs/githooks
|
|
26
|
+
|
|
27
|
+
# Skip if inside aw's own temp sparse checkout (prevents recursion)
|
|
28
|
+
case "$(pwd)" in /tmp/aw-*|/var/folders/*/aw-*) exit 0 ;; esac
|
|
29
|
+
|
|
30
|
+
${awBlock}
|
|
31
|
+
|
|
32
|
+
# Chain to previous hooksPath (if another tool set it before aw)
|
|
33
|
+
PREV_PATH_FILE="$HOME/.aw/hooks/.previous-hooks-path"
|
|
34
|
+
if [ -f "$PREV_PATH_FILE" ]; then
|
|
35
|
+
PREV_HOOK="$(cat "$PREV_PATH_FILE")/${hookName}"
|
|
36
|
+
if [ -x "$PREV_HOOK" ]; then
|
|
37
|
+
exec "$PREV_HOOK" "$@"
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Chain to repo-local hook
|
|
42
|
+
GIT_DIR="$(git rev-parse --git-dir 2>/dev/null)"
|
|
43
|
+
if [ -n "$GIT_DIR" ]; then
|
|
44
|
+
LOCAL_HOOK="$GIT_DIR/hooks/${hookName}"
|
|
45
|
+
[ -x "$LOCAL_HOOK" ] && exec "$LOCAL_HOOK" "$@"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
exit 0
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const POST_MERGE = makeDispatcher('post-merge', `\
|
|
53
|
+
if command -v aw >/dev/null 2>&1; then
|
|
54
|
+
aw init --silent >/dev/null 2>&1 &
|
|
55
|
+
fi`);
|
|
56
|
+
|
|
57
|
+
const POST_CHECKOUT = makeDispatcher('post-checkout', `\
|
|
58
|
+
AW_REGISTRY="$HOME/.aw_registry"
|
|
59
|
+
if [ -d "$AW_REGISTRY" ] && [ ! -e ".aw_registry" ] && [ -d ".git" ]; then
|
|
60
|
+
ln -s "$AW_REGISTRY" ".aw_registry" 2>/dev/null
|
|
61
|
+
fi
|
|
62
|
+
if command -v aw >/dev/null 2>&1; then
|
|
63
|
+
aw init --silent >/dev/null 2>&1 &
|
|
64
|
+
fi`);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Install global git hooks at ~/.aw/hooks/ and set core.hooksPath.
|
|
68
|
+
* If core.hooksPath is already set by another tool, saves the previous
|
|
69
|
+
* value for chaining and restoration on uninstall.
|
|
70
|
+
* @returns {boolean} true if hooks were installed
|
|
71
|
+
*/
|
|
72
|
+
export function installGlobalHooks() {
|
|
73
|
+
try {
|
|
74
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
75
|
+
|
|
76
|
+
writeFileSync(join(HOOKS_DIR, 'post-merge'), POST_MERGE);
|
|
77
|
+
chmodSync(join(HOOKS_DIR, 'post-merge'), '755');
|
|
78
|
+
|
|
79
|
+
writeFileSync(join(HOOKS_DIR, 'post-checkout'), POST_CHECKOUT);
|
|
80
|
+
chmodSync(join(HOOKS_DIR, 'post-checkout'), '755');
|
|
81
|
+
|
|
82
|
+
// Detect and preserve existing core.hooksPath
|
|
83
|
+
let previousPath = null;
|
|
84
|
+
try {
|
|
85
|
+
previousPath = execSync('git config --global core.hooksPath', {
|
|
86
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
87
|
+
}).trim();
|
|
88
|
+
} catch { /* not set */ }
|
|
89
|
+
|
|
90
|
+
if (previousPath && previousPath !== HOOKS_DIR) {
|
|
91
|
+
writeFileSync(PREV_PATH_FILE, previousPath);
|
|
92
|
+
fmt.logStep(`Saved previous core.hooksPath (${previousPath}) for chaining`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
execSync(`git config --global core.hooksPath "${HOOKS_DIR}"`, { stdio: 'pipe' });
|
|
96
|
+
fmt.logStep('Global git hooks installed (auto-sync on pull/clone)');
|
|
97
|
+
return true;
|
|
98
|
+
} catch (e) {
|
|
99
|
+
fmt.logWarn(`Could not install global hooks: ${e.message}`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Remove global hooks and restore previous core.hooksPath if one existed.
|
|
106
|
+
*/
|
|
107
|
+
export function removeGlobalHooks() {
|
|
108
|
+
// Restore previous core.hooksPath or unset
|
|
109
|
+
let restored = false;
|
|
110
|
+
if (existsSync(PREV_PATH_FILE)) {
|
|
111
|
+
try {
|
|
112
|
+
const prev = readFileSync(PREV_PATH_FILE, 'utf8').trim();
|
|
113
|
+
if (prev) {
|
|
114
|
+
execSync(`git config --global core.hooksPath "${prev}"`, { stdio: 'pipe' });
|
|
115
|
+
restored = true;
|
|
116
|
+
}
|
|
117
|
+
} catch { /* best effort */ }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!restored) {
|
|
121
|
+
try {
|
|
122
|
+
const current = execSync('git config --global core.hooksPath', {
|
|
123
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
124
|
+
}).trim();
|
|
125
|
+
if (current === HOOKS_DIR) {
|
|
126
|
+
execSync('git config --global --unset core.hooksPath', { stdio: 'pipe' });
|
|
127
|
+
}
|
|
128
|
+
} catch { /* not set or already unset */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (existsSync(HOOKS_DIR)) {
|
|
132
|
+
rmSync(HOOKS_DIR, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Clean up parent ~/.aw/ if empty
|
|
136
|
+
const awDir = join(HOME, '.aw');
|
|
137
|
+
try {
|
|
138
|
+
if (existsSync(awDir) && readdirSync(awDir).length === 0) {
|
|
139
|
+
rmSync(awDir, { force: true });
|
|
140
|
+
}
|
|
141
|
+
} catch { /* best effort */ }
|
|
142
|
+
|
|
143
|
+
fmt.logStep('Global git hooks removed');
|
|
144
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghl-ai/aw",
|
|
3
|
-
"version": "0.1.30-beta.
|
|
3
|
+
"version": "0.1.30-beta.6",
|
|
4
4
|
"description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"plan.mjs",
|
|
24
24
|
"registry.mjs",
|
|
25
25
|
"apply.mjs",
|
|
26
|
-
"update.mjs"
|
|
26
|
+
"update.mjs",
|
|
27
|
+
"hooks.mjs"
|
|
27
28
|
],
|
|
28
29
|
"engines": {
|
|
29
30
|
"node": ">=18.0.0"
|
package/update.mjs
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
// notifyUpdate(r) → prints banner if r says update available
|
|
5
5
|
// autoUpdate(r) → synchronous install with atomic lock, verification, logging
|
|
6
6
|
// promptUpdate(r) → asks user interactively (normal init)
|
|
7
|
+
//
|
|
8
|
+
// Package manager detection uses import.meta.url (zero subprocesses):
|
|
9
|
+
// .volta/ → volta install pnpm/ → pnpm add -g
|
|
10
|
+
// else → npm i -g
|
|
7
11
|
|
|
8
|
-
import { readFileSync, mkdirSync, writeFileSync, statSync, rmSync, appendFileSync
|
|
12
|
+
import { readFileSync, mkdirSync, writeFileSync, statSync, rmSync, appendFileSync } from 'node:fs';
|
|
9
13
|
import { exec as execCb, execSync } from 'node:child_process';
|
|
10
14
|
import { promisify } from 'node:util';
|
|
11
|
-
import { join } from 'node:path';
|
|
15
|
+
import { join, dirname } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
12
17
|
import { homedir } from 'node:os';
|
|
13
18
|
import * as fmt from './fmt.mjs';
|
|
14
19
|
import { chalk } from './fmt.mjs';
|
|
@@ -21,9 +26,40 @@ const LOG_PATH = join(homedir(), '.aw_registry', '.aw-upgrade.log');
|
|
|
21
26
|
const LOCK_MAX_AGE_MS = 5 * 60 * 1000;
|
|
22
27
|
const LOG_MAX_ENTRIES = 50;
|
|
23
28
|
|
|
29
|
+
// ── Package manager detection ──────────────────────────────────────────
|
|
30
|
+
// We ARE the installed binary. Our own file path tells us who installed us
|
|
31
|
+
// — no subprocess needed.
|
|
32
|
+
//
|
|
33
|
+
// import.meta.url examples:
|
|
34
|
+
// Volta: file:///Users/x/.volta/tools/image/packages/@ghl-ai/aw/...
|
|
35
|
+
// pnpm: file:///Users/x/.local/share/pnpm/global/.../node_modules/@ghl-ai/aw/...
|
|
36
|
+
// npm: file:///opt/homebrew/lib/node_modules/@ghl-ai/aw/...
|
|
37
|
+
// npm: file:///usr/local/lib/node_modules/@ghl-ai/aw/...
|
|
38
|
+
// link: file:///Users/x/code/ghl-agentic-workspace/libs/aw/... (npm link / dev)
|
|
39
|
+
|
|
40
|
+
const SELF_PATH = fileURLToPath(import.meta.url);
|
|
41
|
+
|
|
42
|
+
function detectPM() {
|
|
43
|
+
if (SELF_PATH.includes('/.volta/')) return 'volta';
|
|
44
|
+
if (SELF_PATH.includes('/pnpm/')) return 'pnpm';
|
|
45
|
+
return 'npm';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const PM = detectPM();
|
|
49
|
+
|
|
50
|
+
function getInstallCmd(version = 'latest') {
|
|
51
|
+
const spec = `${PKG_NAME}@${version}`;
|
|
52
|
+
if (PM === 'volta') return `volta install ${spec}`;
|
|
53
|
+
if (PM === 'pnpm') return `pnpm add -g ${spec}`;
|
|
54
|
+
return `npm i -g ${spec}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Version helpers ────────────────────────────────────────────────────
|
|
58
|
+
|
|
24
59
|
function getLocalVersion() {
|
|
25
60
|
try {
|
|
26
|
-
|
|
61
|
+
const pkgPath = join(dirname(SELF_PATH), 'package.json');
|
|
62
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8')).version;
|
|
27
63
|
} catch {
|
|
28
64
|
return '0.0.0';
|
|
29
65
|
}
|
|
@@ -51,6 +87,32 @@ function compareVersions(a, b) {
|
|
|
51
87
|
return 0;
|
|
52
88
|
}
|
|
53
89
|
|
|
90
|
+
// ── Installed version verification ─────────────────────────────────────
|
|
91
|
+
// After install, re-resolve where the package actually landed and read
|
|
92
|
+
// its package.json. Falls back to getLocalVersion() (our own bundle).
|
|
93
|
+
|
|
94
|
+
function getInstalledVersion() {
|
|
95
|
+
try {
|
|
96
|
+
if (PM === 'volta') {
|
|
97
|
+
// Volta resolves via its own image; ask it directly
|
|
98
|
+
const resolved = execSync('volta which aw', {
|
|
99
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10_000,
|
|
100
|
+
}).trim();
|
|
101
|
+
const pkgRoot = join(resolved, '..', '..');
|
|
102
|
+
return JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')).version;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// npm / pnpm: read from the global prefix
|
|
106
|
+
const prefix = execSync('npm prefix -g', {
|
|
107
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10_000,
|
|
108
|
+
}).trim();
|
|
109
|
+
const pkgPath = join(prefix, 'lib', 'node_modules', PKG_NAME, 'package.json');
|
|
110
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8')).version;
|
|
111
|
+
} catch {
|
|
112
|
+
return getLocalVersion();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
54
116
|
// ── Atomic lock via mkdir ──────────────────────────────────────────────
|
|
55
117
|
// mkdir is atomic on all POSIX filesystems. Stale detection uses
|
|
56
118
|
// directory mtime (OS-maintained) rather than parsing PIDs.
|
|
@@ -92,18 +154,6 @@ function appendLog(entry) {
|
|
|
92
154
|
} catch { /* logging is best-effort */ }
|
|
93
155
|
}
|
|
94
156
|
|
|
95
|
-
// ── Installed version verification ─────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
function getInstalledVersion() {
|
|
98
|
-
try {
|
|
99
|
-
const prefix = execSync('npm prefix -g', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10_000 }).trim();
|
|
100
|
-
const pkgPath = join(prefix, 'lib', 'node_modules', PKG_NAME, 'package.json');
|
|
101
|
-
return JSON.parse(readFileSync(pkgPath, 'utf8')).version;
|
|
102
|
-
} catch {
|
|
103
|
-
return getLocalVersion();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
157
|
/**
|
|
108
158
|
* Fetch the latest version from npm (non-blocking, 15s timeout).
|
|
109
159
|
* Call at the start of any command, await at the end.
|
|
@@ -125,10 +175,11 @@ export async function checkForUpdate() {
|
|
|
125
175
|
*/
|
|
126
176
|
export function notifyUpdate(result) {
|
|
127
177
|
if (!result?.hasUpdate) return;
|
|
178
|
+
const cmd = getInstallCmd(result.latest);
|
|
128
179
|
const msg = [
|
|
129
180
|
'',
|
|
130
181
|
` ${chalk.bgHex('#FF6B35').bold(' UPDATE ')} New version available: ${chalk.dim(result.current)} → ${chalk.green(result.latest)}`,
|
|
131
|
-
` ${chalk.dim('Run')} ${chalk.bold(
|
|
182
|
+
` ${chalk.dim('Run')} ${chalk.bold(cmd)} ${chalk.dim('to update')}`,
|
|
132
183
|
'',
|
|
133
184
|
].join('\n');
|
|
134
185
|
console.log(msg);
|
|
@@ -144,15 +195,16 @@ export function autoUpdate(result) {
|
|
|
144
195
|
if (isPrerelease(result.current)) return { status: 'skipped', reason: 'prerelease' };
|
|
145
196
|
if (!acquireLock()) return { status: 'skipped', reason: 'locked' };
|
|
146
197
|
|
|
198
|
+
const cmd = getInstallCmd('latest');
|
|
147
199
|
try {
|
|
148
|
-
execSync(
|
|
200
|
+
execSync(cmd, { stdio: 'pipe', timeout: 60_000 });
|
|
149
201
|
const installed = getInstalledVersion();
|
|
150
202
|
const ok = compareVersions(installed, result.current) > 0;
|
|
151
|
-
const entry = { ts: new Date().toISOString(), from: result.current, to: installed, ok };
|
|
203
|
+
const entry = { ts: new Date().toISOString(), pm: PM, cmd, from: result.current, to: installed, ok };
|
|
152
204
|
appendLog(entry);
|
|
153
205
|
return { status: ok ? 'upgraded' : 'failed', ...entry };
|
|
154
206
|
} catch (e) {
|
|
155
|
-
const entry = { ts: new Date().toISOString(), from: result.current, error: e.message, ok: false };
|
|
207
|
+
const entry = { ts: new Date().toISOString(), pm: PM, cmd, from: result.current, error: e.message, ok: false };
|
|
156
208
|
appendLog(entry);
|
|
157
209
|
return { status: 'failed', error: e.message };
|
|
158
210
|
} finally {
|
|
@@ -168,6 +220,7 @@ export async function promptUpdate(result) {
|
|
|
168
220
|
if (!result?.hasUpdate) return false;
|
|
169
221
|
if (isPrerelease(result.current)) return false;
|
|
170
222
|
|
|
223
|
+
const cmd = getInstallCmd(result.latest);
|
|
171
224
|
fmt.logWarn(`Update available: ${chalk.dim(result.current)} → ${chalk.green(result.latest)}`);
|
|
172
225
|
|
|
173
226
|
const { default: clack } = await import('@clack/prompts');
|
|
@@ -184,14 +237,14 @@ export async function promptUpdate(result) {
|
|
|
184
237
|
const s = fmt.spinner();
|
|
185
238
|
s.start(`Updating to ${result.latest}...`);
|
|
186
239
|
try {
|
|
187
|
-
execSync(
|
|
240
|
+
execSync(cmd, { stdio: 'pipe', timeout: 60_000 });
|
|
188
241
|
const installed = getInstalledVersion();
|
|
189
242
|
const ok = compareVersions(installed, result.current) > 0;
|
|
190
243
|
s.stop(ok ? `Updated to ${chalk.green(installed)}` : chalk.red('Update may have failed — verify with: aw --version'));
|
|
191
244
|
return ok;
|
|
192
245
|
} catch {
|
|
193
246
|
s.stop(chalk.red('Update failed'));
|
|
194
|
-
fmt.logWarn(`Manual update: ${chalk.bold(
|
|
247
|
+
fmt.logWarn(`Manual update: ${chalk.bold(cmd)}`);
|
|
195
248
|
return false;
|
|
196
249
|
}
|
|
197
250
|
}
|