@scheduler-systems/gal-cli 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -5
- package/dist/index.cjs +189 -885
- package/package.json +5 -3
- package/scripts/postinstall.cjs +269 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scheduler-systems/gal-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "GAL CLI - Command-line tool for managing AI agent configurations across your organization",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist/index.cjs",
|
|
14
|
+
"scripts/postinstall.cjs",
|
|
14
15
|
"README.md",
|
|
15
16
|
"LICENSE"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
19
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
18
20
|
"dev": "tsx watch src/index.ts",
|
|
19
|
-
"build": "
|
|
20
|
-
"build:publish": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --target=node18 --minify --outfile=dist/index.cjs --format=cjs && sed -i '' '1s/^#!.*$//' dist/index.cjs && printf '%s\\n' '#!/usr/bin/env node' | cat - dist/index.cjs > dist/temp.cjs && mv dist/temp.cjs dist/index.cjs && chmod +x dist/index.cjs",
|
|
21
|
+
"build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.cjs --format=cjs --define:__CLI_VERSION__=\\\"$(node -p \"require('./package.json').version\")\\\" --define:__DEFAULT_API_URL__=\\\"http://localhost:3000\\\" && chmod +x dist/index.cjs",
|
|
22
|
+
"build:publish": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --target=node18 --minify --outfile=dist/index.cjs --format=cjs --define:__CLI_VERSION__=\\\"$(node -p \"require('./package.json').version\")\\\" --define:__DEFAULT_API_URL__=\\\"https://api.gal.run\\\" && sed -i '' '1s/^#!.*$//' dist/index.cjs && printf '%s\\n' '#!/usr/bin/env node' | cat - dist/index.cjs > dist/temp.cjs && mv dist/temp.cjs dist/index.cjs && chmod +x dist/index.cjs",
|
|
21
23
|
"start": "node dist/index.js",
|
|
22
24
|
"test": "vitest run",
|
|
23
25
|
"test:watch": "vitest",
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GAL CLI Postinstall Script
|
|
4
|
+
* Installs the sync reminder hook to ~/.claude/hooks/
|
|
5
|
+
* This runs automatically when the CLI is installed via npm
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
const HOOK_CONTENT = `#!/usr/bin/env node
|
|
13
|
+
/**
|
|
14
|
+
* GAL Config Sync Hook for Claude Code (UserPromptSubmit)
|
|
15
|
+
* Reminds developers to authenticate and sync with org-approved configs.
|
|
16
|
+
* Self-cleaning: removes itself if GAL CLI is uninstalled.
|
|
17
|
+
*
|
|
18
|
+
* Uses Claude Code hook protocol:
|
|
19
|
+
* - systemMessage: shown to user as notification
|
|
20
|
+
* - continue: true = non-blocking
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { execSync } = require('child_process');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
|
|
28
|
+
const GAL_DIR = '.gal';
|
|
29
|
+
const SYNC_STATE_FILE = 'sync-state.json';
|
|
30
|
+
const GAL_CONFIG_FILE = path.join(os.homedir(), '.gal', 'config.json');
|
|
31
|
+
const SESSION_FILE = path.join(os.tmpdir(), 'gal-hook-session.json');
|
|
32
|
+
|
|
33
|
+
// Output a notification to the user (non-blocking)
|
|
34
|
+
function notify(message) {
|
|
35
|
+
// Output JSON to stdout - Claude Code will parse this
|
|
36
|
+
process.stdout.write(JSON.stringify({
|
|
37
|
+
systemMessage: message
|
|
38
|
+
}));
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Track reminders per session to avoid spamming
|
|
43
|
+
function shouldRemind(key) {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
let session = {};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(SESSION_FILE)) {
|
|
49
|
+
session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
|
|
53
|
+
// Remind every 10 minutes per issue type
|
|
54
|
+
const lastRemind = session[key] || 0;
|
|
55
|
+
const tenMinutes = 10 * 60 * 1000;
|
|
56
|
+
|
|
57
|
+
if (now - lastRemind > tenMinutes) {
|
|
58
|
+
session[key] = now;
|
|
59
|
+
try {
|
|
60
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(session));
|
|
61
|
+
} catch {}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Self-cleaning: Check if GAL CLI is still installed
|
|
68
|
+
function isGalInstalled() {
|
|
69
|
+
try {
|
|
70
|
+
execSync('which gal', { stdio: 'ignore' });
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Remove this hook and clean up settings.json
|
|
78
|
+
function selfClean() {
|
|
79
|
+
const hookPath = __filename;
|
|
80
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
81
|
+
|
|
82
|
+
// Remove hook file
|
|
83
|
+
try { fs.unlinkSync(hookPath); } catch {}
|
|
84
|
+
|
|
85
|
+
// Remove hook entry from settings.json
|
|
86
|
+
try {
|
|
87
|
+
if (fs.existsSync(settingsPath)) {
|
|
88
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
89
|
+
if (settings.hooks?.UserPromptSubmit) {
|
|
90
|
+
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(entry => {
|
|
91
|
+
if (!entry.hooks) return true;
|
|
92
|
+
entry.hooks = entry.hooks.filter(h => !h.command?.includes('gal-'));
|
|
93
|
+
return entry.hooks.length > 0;
|
|
94
|
+
});
|
|
95
|
+
if (settings.hooks.UserPromptSubmit.length === 0) delete settings.hooks.UserPromptSubmit;
|
|
96
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
97
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
|
|
102
|
+
// Clean up session file
|
|
103
|
+
try { fs.unlinkSync(SESSION_FILE); } catch {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check if GAL is installed, self-clean if not
|
|
107
|
+
if (!isGalInstalled()) {
|
|
108
|
+
selfClean();
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Read GAL CLI config (auth token, default org)
|
|
113
|
+
function readGalConfig() {
|
|
114
|
+
if (!fs.existsSync(GAL_CONFIG_FILE)) return null;
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(fs.readFileSync(GAL_CONFIG_FILE, 'utf-8'));
|
|
117
|
+
} catch { return null; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Decode JWT without verification (just to check expiration)
|
|
121
|
+
function decodeJwt(token) {
|
|
122
|
+
try {
|
|
123
|
+
const parts = token.split('.');
|
|
124
|
+
if (parts.length !== 3) return null;
|
|
125
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
|
|
126
|
+
return payload;
|
|
127
|
+
} catch { return null; }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check authentication status
|
|
131
|
+
const galConfig = readGalConfig();
|
|
132
|
+
|
|
133
|
+
// Step 1: Check if authenticated
|
|
134
|
+
if (!galConfig || !galConfig.authToken) {
|
|
135
|
+
if (shouldRemind('auth-required')) {
|
|
136
|
+
notify('🔐 GAL: Authentication required. Run: gal auth login');
|
|
137
|
+
}
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Step 2: Check if token is expired
|
|
142
|
+
const tokenPayload = decodeJwt(galConfig.authToken);
|
|
143
|
+
if (tokenPayload && tokenPayload.exp) {
|
|
144
|
+
const expiresAt = tokenPayload.exp * 1000;
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
|
|
147
|
+
if (now > expiresAt) {
|
|
148
|
+
if (shouldRemind('auth-expired')) {
|
|
149
|
+
notify('🔐 GAL: Session expired. Run: gal auth login');
|
|
150
|
+
}
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Warn if expiring soon (within 24 hours)
|
|
155
|
+
const hoursUntilExpiry = (expiresAt - now) / (1000 * 60 * 60);
|
|
156
|
+
if (hoursUntilExpiry < 24) {
|
|
157
|
+
if (shouldRemind('auth-expiring')) {
|
|
158
|
+
notify(\`⏰ GAL: Session expires in \${Math.round(hoursUntilExpiry)} hours. Run: gal auth login\`);
|
|
159
|
+
}
|
|
160
|
+
// Don't exit - continue to check sync state
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Step 3: Check sync state
|
|
165
|
+
function readSyncState() {
|
|
166
|
+
const statePath = path.join(process.cwd(), GAL_DIR, SYNC_STATE_FILE);
|
|
167
|
+
if (!fs.existsSync(statePath)) return null;
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
170
|
+
} catch { return null; }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const state = readSyncState();
|
|
174
|
+
|
|
175
|
+
// No sync state = project not synced yet
|
|
176
|
+
if (!state) {
|
|
177
|
+
const orgName = galConfig.defaultOrg || 'your organization';
|
|
178
|
+
if (shouldRemind('sync-required')) {
|
|
179
|
+
notify(\`📥 GAL: Project not synced with \${orgName}. Run: gal sync --pull\`);
|
|
180
|
+
}
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Step 4: Compare hashes - check if config is outdated
|
|
185
|
+
if (state.lastSyncHash !== state.approvedConfigHash) {
|
|
186
|
+
const days = Math.floor((Date.now() - new Date(state.lastSyncTimestamp).getTime()) / (24 * 60 * 60 * 1000));
|
|
187
|
+
if (shouldRemind('sync-outdated')) {
|
|
188
|
+
notify(\`⚠️ GAL: Config \${days} day(s) behind \${state.organization}. Run: gal sync --pull\`);
|
|
189
|
+
}
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Step 5: Check if synced files still exist
|
|
194
|
+
if (state.syncedFiles && state.syncedFiles.length > 0) {
|
|
195
|
+
const missingFiles = state.syncedFiles.filter(f => {
|
|
196
|
+
const fullPath = path.join(process.cwd(), f);
|
|
197
|
+
return !fs.existsSync(fullPath);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (missingFiles.length > 0) {
|
|
201
|
+
if (shouldRemind('sync-missing')) {
|
|
202
|
+
notify('⚠️ GAL: Missing config files. Run: gal sync --pull');
|
|
203
|
+
}
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// All good - config is up-to-date and complete
|
|
209
|
+
process.exit(0);
|
|
210
|
+
`;
|
|
211
|
+
|
|
212
|
+
function installHook() {
|
|
213
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
214
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
215
|
+
const hookPath = path.join(hooksDir, 'gal-sync-reminder.js');
|
|
216
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
// Create directories if needed
|
|
220
|
+
if (!fs.existsSync(hooksDir)) {
|
|
221
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Write the hook file
|
|
225
|
+
fs.writeFileSync(hookPath, HOOK_CONTENT, 'utf-8');
|
|
226
|
+
fs.chmodSync(hookPath, '755');
|
|
227
|
+
|
|
228
|
+
// Register hook in settings.json
|
|
229
|
+
let settings = {};
|
|
230
|
+
if (fs.existsSync(settingsPath)) {
|
|
231
|
+
try {
|
|
232
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
233
|
+
} catch {
|
|
234
|
+
settings = {};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check if hook is already registered
|
|
239
|
+
const hookCommand = `node ${hookPath}`;
|
|
240
|
+
const existingHooks = settings.hooks?.UserPromptSubmit || [];
|
|
241
|
+
const alreadyRegistered = existingHooks.some(entry =>
|
|
242
|
+
entry.hooks?.some(h => h.command?.includes('gal-sync-reminder'))
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (!alreadyRegistered) {
|
|
246
|
+
// Add hook registration
|
|
247
|
+
if (!settings.hooks) settings.hooks = {};
|
|
248
|
+
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
249
|
+
|
|
250
|
+
settings.hooks.UserPromptSubmit.push({
|
|
251
|
+
matcher: "",
|
|
252
|
+
hooks: [{
|
|
253
|
+
type: "command",
|
|
254
|
+
command: hookCommand
|
|
255
|
+
}]
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log('✓ GAL sync reminder hook installed');
|
|
262
|
+
console.log(' Restart Claude Code for the hook to take effect.');
|
|
263
|
+
} catch (error) {
|
|
264
|
+
// Silent fail - hook is optional enhancement
|
|
265
|
+
// console.error('Could not install hook:', error.message);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
installHook();
|