@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scheduler-systems/gal-cli",
3
- "version": "0.1.3",
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": "tsc && chmod +x dist/index.js",
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();