@scheduler-systems/gal-run 0.0.291 → 0.0.293
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/index.cjs +50 -21
- package/package.json +3 -1
- package/scripts/postinstall.cjs +932 -0
- package/scripts/preuninstall.cjs +237 -0
package/dist/index.cjs
CHANGED
|
@@ -3970,7 +3970,7 @@ var cliVersion, defaultApiUrl, BUILD_CONSTANTS, constants_default;
|
|
|
3970
3970
|
var init_constants = __esm({
|
|
3971
3971
|
"src/constants.ts"() {
|
|
3972
3972
|
"use strict";
|
|
3973
|
-
cliVersion = true ? "0.0.
|
|
3973
|
+
cliVersion = true ? "0.0.293" : "0.0.0-dev";
|
|
3974
3974
|
defaultApiUrl = true ? "https://api.gal.run" : "http://localhost:3000";
|
|
3975
3975
|
BUILD_CONSTANTS = Object.freeze([cliVersion, defaultApiUrl]);
|
|
3976
3976
|
constants_default = BUILD_CONSTANTS;
|
|
@@ -4437,6 +4437,37 @@ var init_path_conflict = __esm({
|
|
|
4437
4437
|
}
|
|
4438
4438
|
});
|
|
4439
4439
|
|
|
4440
|
+
// src/utils/update-notification.ts
|
|
4441
|
+
function compareVersions(v1, v2) {
|
|
4442
|
+
const parts1 = v1.replace(/^v/, "").split(".").map(Number);
|
|
4443
|
+
const parts2 = v2.replace(/^v/, "").split(".").map(Number);
|
|
4444
|
+
for (let i = 0; i < 3; i++) {
|
|
4445
|
+
if ((parts1[i] || 0) < (parts2[i] || 0)) return -1;
|
|
4446
|
+
if ((parts1[i] || 0) > (parts2[i] || 0)) return 1;
|
|
4447
|
+
}
|
|
4448
|
+
return 0;
|
|
4449
|
+
}
|
|
4450
|
+
function shouldShowUpdateNotification({
|
|
4451
|
+
cache,
|
|
4452
|
+
currentVersion,
|
|
4453
|
+
hasJsonFlag,
|
|
4454
|
+
now = Date.now(),
|
|
4455
|
+
throttleWindowMs = UPDATE_NOTIFICATION_WINDOW_MS
|
|
4456
|
+
}) {
|
|
4457
|
+
if (!cache?.latestVersion) return false;
|
|
4458
|
+
if (hasJsonFlag) return false;
|
|
4459
|
+
if (compareVersions(currentVersion, cache.latestVersion) >= 0) return false;
|
|
4460
|
+
if (!cache.lastNotificationShown) return true;
|
|
4461
|
+
return now - cache.lastNotificationShown > throttleWindowMs;
|
|
4462
|
+
}
|
|
4463
|
+
var UPDATE_NOTIFICATION_WINDOW_MS;
|
|
4464
|
+
var init_update_notification = __esm({
|
|
4465
|
+
"src/utils/update-notification.ts"() {
|
|
4466
|
+
"use strict";
|
|
4467
|
+
UPDATE_NOTIFICATION_WINDOW_MS = 60 * 60 * 1e3;
|
|
4468
|
+
}
|
|
4469
|
+
});
|
|
4470
|
+
|
|
4440
4471
|
// src/telemetry/event-queue.ts
|
|
4441
4472
|
var import_fs2, import_path2, import_os2, GAL_DIR, QUEUE_FILE, PENDING_EVENTS_FILE, MAX_BATCH_SIZE, OFFLINE_TTL_DAYS, OFFLINE_TTL_MS, EventQueue;
|
|
4442
4473
|
var init_event_queue = __esm({
|
|
@@ -4828,7 +4859,7 @@ function detectEnvironment() {
|
|
|
4828
4859
|
return "dev";
|
|
4829
4860
|
}
|
|
4830
4861
|
try {
|
|
4831
|
-
const version = true ? "0.0.
|
|
4862
|
+
const version = true ? "0.0.293" : void 0;
|
|
4832
4863
|
if (version && version.includes("-local")) {
|
|
4833
4864
|
return "dev";
|
|
4834
4865
|
}
|
|
@@ -5197,7 +5228,7 @@ function getId() {
|
|
|
5197
5228
|
}
|
|
5198
5229
|
function getCliVersion() {
|
|
5199
5230
|
try {
|
|
5200
|
-
return true ? "0.0.
|
|
5231
|
+
return true ? "0.0.293" : "0.0.0-dev";
|
|
5201
5232
|
} catch {
|
|
5202
5233
|
return "0.0.0-dev";
|
|
5203
5234
|
}
|
|
@@ -53010,7 +53041,7 @@ function fetchLatestVersion() {
|
|
|
53010
53041
|
});
|
|
53011
53042
|
});
|
|
53012
53043
|
}
|
|
53013
|
-
function
|
|
53044
|
+
function compareVersions2(v1, v2) {
|
|
53014
53045
|
const parts1 = v1.replace(/^v/, "").split(".").map(Number);
|
|
53015
53046
|
const parts2 = v2.replace(/^v/, "").split(".").map(Number);
|
|
53016
53047
|
for (let i = 0; i < 3; i++) {
|
|
@@ -53065,7 +53096,7 @@ function createUpdateCommand() {
|
|
|
53065
53096
|
spinner.info(source_default.dim(`Pre-release version (${cliVersion7}) \u2014 skipping update check`));
|
|
53066
53097
|
process.exit(0);
|
|
53067
53098
|
}
|
|
53068
|
-
const needsUpdate =
|
|
53099
|
+
const needsUpdate = compareVersions2(cliVersion7, latestVersion) < 0;
|
|
53069
53100
|
if (!needsUpdate) {
|
|
53070
53101
|
spinner.succeed(source_default.green(`Already up to date (v${cliVersion7})`));
|
|
53071
53102
|
process.exit(0);
|
|
@@ -54209,15 +54240,6 @@ function getRegistryAuthToken2() {
|
|
|
54209
54240
|
return void 0;
|
|
54210
54241
|
}
|
|
54211
54242
|
}
|
|
54212
|
-
function compareVersions2(v1, v2) {
|
|
54213
|
-
const parts1 = v1.replace(/^v/, "").split(".").map(Number);
|
|
54214
|
-
const parts2 = v2.replace(/^v/, "").split(".").map(Number);
|
|
54215
|
-
for (let i = 0; i < 3; i++) {
|
|
54216
|
-
if ((parts1[i] || 0) < (parts2[i] || 0)) return -1;
|
|
54217
|
-
if ((parts1[i] || 0) > (parts2[i] || 0)) return 1;
|
|
54218
|
-
}
|
|
54219
|
-
return 0;
|
|
54220
|
-
}
|
|
54221
54243
|
function readUpdateCache() {
|
|
54222
54244
|
try {
|
|
54223
54245
|
if ((0, import_fs40.existsSync)(UPDATE_CACHE_FILE)) {
|
|
@@ -54269,14 +54291,21 @@ function checkForUpdates() {
|
|
|
54269
54291
|
const hasJsonFlag = process.argv.includes("--json") || process.argv.includes("-j");
|
|
54270
54292
|
const cache = readUpdateCache();
|
|
54271
54293
|
const shouldRefresh = !cache || Date.now() - cache.lastCheck > ONE_DAY;
|
|
54272
|
-
const
|
|
54273
|
-
|
|
54294
|
+
const now = Date.now();
|
|
54295
|
+
const shouldShowNotification = shouldShowUpdateNotification({
|
|
54296
|
+
cache,
|
|
54297
|
+
currentVersion: cliVersion9,
|
|
54298
|
+
hasJsonFlag,
|
|
54299
|
+
now
|
|
54300
|
+
});
|
|
54301
|
+
if (shouldShowNotification && cache?.latestVersion) {
|
|
54274
54302
|
process.on("exit", () => {
|
|
54275
54303
|
showUpdateNotification(cliVersion9, cache.latestVersion);
|
|
54276
54304
|
});
|
|
54277
54305
|
writeUpdateCache({
|
|
54278
|
-
|
|
54279
|
-
|
|
54306
|
+
lastCheck: cache.lastCheck,
|
|
54307
|
+
latestVersion: cache.latestVersion,
|
|
54308
|
+
lastNotificationShown: now
|
|
54280
54309
|
});
|
|
54281
54310
|
const isUpdateCommand = process.argv.includes("update");
|
|
54282
54311
|
const autoUpdateDisabled = process.env.GAL_NO_AUTO_UPDATE === "1" || process.env.CI === "true";
|
|
@@ -54447,7 +54476,7 @@ function refreshOrgMemberships() {
|
|
|
54447
54476
|
} catch {
|
|
54448
54477
|
}
|
|
54449
54478
|
}
|
|
54450
|
-
var import_dotenv, import_https2, import_child_process13, import_fs40, import_path39, import_os26, originalEmit, GLOBAL_TIMEOUT_MS, globalTimeout, cliVersion9, UPDATE_CACHE_DIR, UPDATE_CACHE_FILE, ONE_DAY,
|
|
54479
|
+
var import_dotenv, import_https2, import_child_process13, import_fs40, import_path39, import_os26, originalEmit, GLOBAL_TIMEOUT_MS, globalTimeout, cliVersion9, UPDATE_CACHE_DIR, UPDATE_CACHE_FILE, ONE_DAY, REGISTRY_URL2, REGISTRY_HOST, sessionStartTime, isReadOnlyStatusCommand, isMachineMode, exitHooksRan, featureFlags, knownCommands, isKnownCommand, program2, allInternalFlags;
|
|
54451
54480
|
var init_index = __esm({
|
|
54452
54481
|
"src/index.ts"() {
|
|
54453
54482
|
"use strict";
|
|
@@ -54463,6 +54492,7 @@ var init_index = __esm({
|
|
|
54463
54492
|
init_sentry2();
|
|
54464
54493
|
init_path_conflict();
|
|
54465
54494
|
init_install();
|
|
54495
|
+
init_update_notification();
|
|
54466
54496
|
init_telemetry();
|
|
54467
54497
|
init_terms_acceptance();
|
|
54468
54498
|
init_research_preview();
|
|
@@ -54489,7 +54519,6 @@ var init_index = __esm({
|
|
|
54489
54519
|
UPDATE_CACHE_DIR = (0, import_path39.join)((0, import_os26.homedir)(), ".gal");
|
|
54490
54520
|
UPDATE_CACHE_FILE = (0, import_path39.join)(UPDATE_CACHE_DIR, "update-cache.json");
|
|
54491
54521
|
ONE_DAY = 24 * 60 * 60 * 1e3;
|
|
54492
|
-
ONE_HOUR = 60 * 60 * 1e3;
|
|
54493
54522
|
REGISTRY_URL2 = (() => {
|
|
54494
54523
|
const raw = process.env.GAL_REGISTRY_URL || "https://registry.npmjs.org";
|
|
54495
54524
|
try {
|
|
@@ -54635,7 +54664,7 @@ var init_index = __esm({
|
|
|
54635
54664
|
});
|
|
54636
54665
|
|
|
54637
54666
|
// src/bootstrap.ts
|
|
54638
|
-
var cliVersion10 = true ? "0.0.
|
|
54667
|
+
var cliVersion10 = true ? "0.0.293" : "0.0.0-dev";
|
|
54639
54668
|
var args = process.argv.slice(2);
|
|
54640
54669
|
var requestedGlobalHelp = args.length === 1 && (args[0] === "--help" || args[0] === "-h");
|
|
54641
54670
|
var requestedVersion = args.length === 1 && (args[0] === "--version" || args[0] === "-V");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scheduler-systems/gal-run",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.293",
|
|
4
4
|
"description": "GAL CLI - Command-line tool for managing AI agent configurations across your organization",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"private": false,
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"dist/index.cjs",
|
|
14
14
|
"dist/postinstall.cjs",
|
|
15
15
|
"dist/preuninstall.cjs",
|
|
16
|
+
"scripts/postinstall.cjs",
|
|
17
|
+
"scripts/preuninstall.cjs",
|
|
16
18
|
"README.md",
|
|
17
19
|
"LICENSE"
|
|
18
20
|
],
|
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GAL CLI Postinstall Script
|
|
4
|
+
*
|
|
5
|
+
* Automatically installs Claude Code integrations when GAL CLI is installed via pnpm.
|
|
6
|
+
* This script runs as a pnpm lifecycle hook after package installation completes.
|
|
7
|
+
*
|
|
8
|
+
* What it installs:
|
|
9
|
+
* 1. SessionStart hook → ~/.claude/hooks/gal-sync-reminder.js
|
|
10
|
+
* - Shows sync status at the start of each Claude session
|
|
11
|
+
* - Prompts user to login/sync if needed
|
|
12
|
+
* 2. Status line script → ~/.claude/status_lines/gal-sync-status.py
|
|
13
|
+
* - Displays sync warnings in Claude's status bar (when not synced)
|
|
14
|
+
* 3. GAL CLI rules → ~/.claude/rules/gal-cli.md
|
|
15
|
+
* - Provides persistent GAL CLI awareness to Claude Code
|
|
16
|
+
*
|
|
17
|
+
* Scope:
|
|
18
|
+
* - These are CLI-level integrations (user-wide, not project-specific)
|
|
19
|
+
* - Org-specific configs are handled by `gal sync --pull`
|
|
20
|
+
*
|
|
21
|
+
* Key behaviors:
|
|
22
|
+
* - Idempotent: Safe to run multiple times, only updates when versions change
|
|
23
|
+
* - Version-aware: Checks version markers before overwriting files
|
|
24
|
+
* - Self-cleaning: Installed scripts remove themselves if GAL CLI is uninstalled
|
|
25
|
+
* - Non-destructive: Won't overwrite user's custom configs (e.g., custom statusLine)
|
|
26
|
+
* - Telemetry: Queues installation event for next CLI run (GAL-114)
|
|
27
|
+
*
|
|
28
|
+
* When it runs:
|
|
29
|
+
* - Automatically during `pnpm add -g @scheduler-systems/gal-run`
|
|
30
|
+
* - Can be manually triggered via `pnpm run postinstall` in the CLI package directory
|
|
31
|
+
*
|
|
32
|
+
* Prerequisites:
|
|
33
|
+
* - Node.js 18+ (CommonJS module)
|
|
34
|
+
* - Writable ~/.claude directory
|
|
35
|
+
*
|
|
36
|
+
* Related files:
|
|
37
|
+
* - apps/cli/scripts/preuninstall.cjs - Cleanup script (runs before uninstall)
|
|
38
|
+
* - apps/cli/package.json - pnpm lifecycle hooks configuration
|
|
39
|
+
*
|
|
40
|
+
* @see specs/gal-cli/installation.md
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const fs = require('fs');
|
|
44
|
+
const path = require('path');
|
|
45
|
+
const os = require('os');
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Version Configuration
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// Version markers control when files get updated.
|
|
51
|
+
// Bump these when file contents change to trigger reinstallation.
|
|
52
|
+
|
|
53
|
+
// Get current CLI version from package.json
|
|
54
|
+
const cliPackageJson = require('../package.json');
|
|
55
|
+
const cliVersion = cliPackageJson.version;
|
|
56
|
+
|
|
57
|
+
// Version markers for idempotency checks
|
|
58
|
+
// Bump these to force updates to installed files
|
|
59
|
+
const HOOK_VERSION = '4.0.0'; // SessionStart hook (4.0.0: Background cache refresh + sync)
|
|
60
|
+
const STATUS_LINE_VERSION = '1.0.0'; // Status line script
|
|
61
|
+
const RULES_VERSION = '1.0.0'; // GAL CLI rules
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// GAL CLI Rules Content
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Injected into ~/.claude/rules/gal-cli.md
|
|
67
|
+
// Provides persistent GAL CLI awareness to Claude without hook overhead.
|
|
68
|
+
// Claude automatically loads rules from ~/.claude/rules/ at session start.
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
const GAL_CLI_RULES_CONTENT = `# GAL CLI
|
|
72
|
+
|
|
73
|
+
<!-- GAL_RULES_VERSION = "${RULES_VERSION}" -->
|
|
74
|
+
|
|
75
|
+
The \`gal\` CLI is available for managing org-approved AI agent configurations.
|
|
76
|
+
|
|
77
|
+
## Available Commands
|
|
78
|
+
- \`gal sync --pull\` - Download latest approved config from your organization
|
|
79
|
+
- \`gal auth login\` - Authenticate with GitHub
|
|
80
|
+
- \`gal --help\` - See all available commands
|
|
81
|
+
|
|
82
|
+
## Behavior Rules
|
|
83
|
+
- **Confirmation Required**: Always ask the user before running any \`gal\` command
|
|
84
|
+
- **Self-Discovery**: If unsure about syntax, run \`gal --help\` or \`gal <command> --help\` first
|
|
85
|
+
- **Sync Notifications**: When you see a GAL sync notification, ask: "Do you want me to sync gal now?"
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
// =============================================================================
|
|
89
|
+
// SessionStart Hook Content
|
|
90
|
+
// =============================================================================
|
|
91
|
+
// Shows sync status notification at Claude session start.
|
|
92
|
+
// Appears once at the top of the chat window and stays visible.
|
|
93
|
+
// For continuous status updates, see the status line script below.
|
|
94
|
+
// =============================================================================
|
|
95
|
+
|
|
96
|
+
const HOOK_CONTENT = `#!/usr/bin/env node
|
|
97
|
+
/**
|
|
98
|
+
* GAL Config Sync Hook for Claude Code (SessionStart)
|
|
99
|
+
* Version: ${HOOK_VERSION}
|
|
100
|
+
*
|
|
101
|
+
* Shows sync status at session start:
|
|
102
|
+
* - Not authenticated → prompt to login
|
|
103
|
+
* - Token expired → prompt to re-login
|
|
104
|
+
* - Not synced → prompt to sync
|
|
105
|
+
* - Config outdated → prompt to sync
|
|
106
|
+
* - All good → show synced status
|
|
107
|
+
*
|
|
108
|
+
* Self-cleaning: removes itself if GAL CLI is uninstalled.
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
// GAL_HOOK_VERSION = "${HOOK_VERSION}"
|
|
112
|
+
|
|
113
|
+
const fs = require('fs');
|
|
114
|
+
const path = require('path');
|
|
115
|
+
const { execSync, spawn } = require('child_process');
|
|
116
|
+
const os = require('os');
|
|
117
|
+
|
|
118
|
+
const GAL_DIR = '.gal';
|
|
119
|
+
const SYNC_STATE_FILE = 'sync-state.json';
|
|
120
|
+
const GAL_CONFIG_FILE = path.join(os.homedir(), '.gal', 'config.json');
|
|
121
|
+
|
|
122
|
+
function showMessage(message, status) {
|
|
123
|
+
// Queue telemetry event before showing message
|
|
124
|
+
queueTelemetryEvent(status);
|
|
125
|
+
console.log(JSON.stringify({ systemMessage: message }));
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// Telemetry: Queue events for CLI to send on next run (GAL-114)
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// Hook runs in Claude context (no network access), so we queue telemetry events
|
|
133
|
+
// in a JSON file that the CLI reads and flushes on next run. This allows us to
|
|
134
|
+
// track hook executions without blocking the user or requiring network calls.
|
|
135
|
+
// =============================================================================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Queue a telemetry event for the next CLI run.
|
|
139
|
+
*
|
|
140
|
+
* Since hooks run synchronously in Claude's context, we can't send telemetry
|
|
141
|
+
* directly. Instead, we write events to a pending queue file that the CLI
|
|
142
|
+
* reads and flushes on its next execution.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} status - Sync status from the hook (auth_required, synced, etc.)
|
|
145
|
+
*/
|
|
146
|
+
function queueTelemetryEvent(status) {
|
|
147
|
+
const pendingEventsPath = path.join(os.homedir(), '.gal', 'telemetry-pending-events.json');
|
|
148
|
+
const galDir = path.join(os.homedir(), '.gal');
|
|
149
|
+
|
|
150
|
+
let pending = [];
|
|
151
|
+
try {
|
|
152
|
+
if (fs.existsSync(pendingEventsPath)) {
|
|
153
|
+
pending = JSON.parse(fs.readFileSync(pendingEventsPath, 'utf-8'));
|
|
154
|
+
}
|
|
155
|
+
} catch {}
|
|
156
|
+
|
|
157
|
+
// Add session start hook event
|
|
158
|
+
pending.push({
|
|
159
|
+
id: require('crypto').randomUUID(),
|
|
160
|
+
eventType: 'hook_triggered',
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
payload: {
|
|
163
|
+
notificationType: 'session_start',
|
|
164
|
+
hookVersion: '${HOOK_VERSION}',
|
|
165
|
+
status: status,
|
|
166
|
+
cwd: process.cwd(),
|
|
167
|
+
platform: process.platform,
|
|
168
|
+
nodeVersion: process.version,
|
|
169
|
+
},
|
|
170
|
+
queuedAt: Date.now(),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
if (!fs.existsSync(galDir)) {
|
|
175
|
+
fs.mkdirSync(galDir, { recursive: true });
|
|
176
|
+
}
|
|
177
|
+
fs.writeFileSync(pendingEventsPath, JSON.stringify(pending), 'utf-8');
|
|
178
|
+
} catch {
|
|
179
|
+
// Ignore errors - telemetry is optional
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// =============================================================================
|
|
184
|
+
// Self-cleaning: Remove hook if GAL CLI is uninstalled
|
|
185
|
+
// =============================================================================
|
|
186
|
+
|
|
187
|
+
function isGalInstalled() {
|
|
188
|
+
try {
|
|
189
|
+
execSync('which gal', { stdio: 'ignore' });
|
|
190
|
+
return true;
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function selfClean() {
|
|
197
|
+
const hookPath = __filename;
|
|
198
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
199
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
200
|
+
const rulesPath = path.join(claudeDir, 'rules', 'gal-cli.md');
|
|
201
|
+
|
|
202
|
+
// Remove hook file
|
|
203
|
+
try { fs.unlinkSync(hookPath); } catch {}
|
|
204
|
+
|
|
205
|
+
// Remove rules file
|
|
206
|
+
try { fs.unlinkSync(rulesPath); } catch {}
|
|
207
|
+
|
|
208
|
+
// Remove hook entries from settings.json
|
|
209
|
+
try {
|
|
210
|
+
if (fs.existsSync(settingsPath)) {
|
|
211
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
212
|
+
const hookEvents = ['SessionStart', 'UserPromptSubmit'];
|
|
213
|
+
|
|
214
|
+
for (const event of hookEvents) {
|
|
215
|
+
if (settings.hooks?.[event]) {
|
|
216
|
+
settings.hooks[event] = settings.hooks[event].filter(entry => {
|
|
217
|
+
if (!entry.hooks) return true;
|
|
218
|
+
entry.hooks = entry.hooks.filter(h => !h.command?.includes('gal-'));
|
|
219
|
+
return entry.hooks.length > 0;
|
|
220
|
+
});
|
|
221
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (Object.keys(settings.hooks || {}).length === 0) delete settings.hooks;
|
|
226
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
227
|
+
}
|
|
228
|
+
} catch {}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check if GAL is installed, self-clean if not
|
|
232
|
+
if (!isGalInstalled()) {
|
|
233
|
+
selfClean();
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Read GAL CLI config (auth token, default org)
|
|
238
|
+
function readGalConfig() {
|
|
239
|
+
if (!fs.existsSync(GAL_CONFIG_FILE)) return null;
|
|
240
|
+
try {
|
|
241
|
+
return JSON.parse(fs.readFileSync(GAL_CONFIG_FILE, 'utf-8'));
|
|
242
|
+
} catch { return null; }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Decode JWT without verification (just to check expiration)
|
|
246
|
+
function decodeJwt(token) {
|
|
247
|
+
try {
|
|
248
|
+
const parts = token.split('.');
|
|
249
|
+
if (parts.length !== 3) return null;
|
|
250
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
|
|
251
|
+
return payload;
|
|
252
|
+
} catch { return null; }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// =============================================================================
|
|
256
|
+
// Auto-update check: run gal update if a newer version is cached
|
|
257
|
+
// =============================================================================
|
|
258
|
+
function checkAndAutoUpdate() {
|
|
259
|
+
const updateCachePath = path.join(os.homedir(), '.gal', 'update-cache.json');
|
|
260
|
+
if (!fs.existsSync(updateCachePath)) return null;
|
|
261
|
+
try {
|
|
262
|
+
const cache = JSON.parse(fs.readFileSync(updateCachePath, 'utf-8'));
|
|
263
|
+
if (!cache.latestVersion) return null;
|
|
264
|
+
let currentVersion;
|
|
265
|
+
try {
|
|
266
|
+
currentVersion = execSync('gal --version', { stdio: 'pipe', timeout: 5000 }).toString().trim();
|
|
267
|
+
} catch { return null; }
|
|
268
|
+
const cv = currentVersion.replace(/^v/, '').split('.').map(Number);
|
|
269
|
+
const lv = cache.latestVersion.replace(/^v/, '').split('.').map(Number);
|
|
270
|
+
let needsUpdate = false;
|
|
271
|
+
for (let i = 0; i < 3; i++) {
|
|
272
|
+
if ((cv[i] || 0) < (lv[i] || 0)) { needsUpdate = true; break; }
|
|
273
|
+
if ((cv[i] || 0) > (lv[i] || 0)) break;
|
|
274
|
+
}
|
|
275
|
+
if (!needsUpdate) return null;
|
|
276
|
+
if (process.env.GAL_NO_AUTO_UPDATE === '1' || process.env.CI) return null;
|
|
277
|
+
try {
|
|
278
|
+
execSync('gal update', { stdio: 'pipe', timeout: 30000 });
|
|
279
|
+
return cache.latestVersion;
|
|
280
|
+
} catch { return null; }
|
|
281
|
+
} catch { return null; }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Refresh update cache in background if stale (>24h)
|
|
285
|
+
function refreshUpdateCacheIfStale() {
|
|
286
|
+
try {
|
|
287
|
+
const updateCachePath = path.join(os.homedir(), '.gal', 'update-cache.json');
|
|
288
|
+
let needsRefresh = true;
|
|
289
|
+
if (fs.existsSync(updateCachePath)) {
|
|
290
|
+
try {
|
|
291
|
+
const cache = JSON.parse(fs.readFileSync(updateCachePath, 'utf-8'));
|
|
292
|
+
if (cache.lastCheck && (Date.now() - cache.lastCheck) < 24 * 60 * 60 * 1000) {
|
|
293
|
+
needsRefresh = false;
|
|
294
|
+
}
|
|
295
|
+
} catch {}
|
|
296
|
+
}
|
|
297
|
+
if (needsRefresh) {
|
|
298
|
+
const child = spawn('gal', ['update', '--check'], {
|
|
299
|
+
stdio: 'ignore',
|
|
300
|
+
detached: true,
|
|
301
|
+
});
|
|
302
|
+
child.unref();
|
|
303
|
+
}
|
|
304
|
+
} catch {}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const updatedVersion = checkAndAutoUpdate();
|
|
308
|
+
refreshUpdateCacheIfStale();
|
|
309
|
+
|
|
310
|
+
// Check authentication status
|
|
311
|
+
const galConfig = readGalConfig();
|
|
312
|
+
|
|
313
|
+
// Check 1: Not authenticated
|
|
314
|
+
if (!galConfig || !galConfig.authToken) {
|
|
315
|
+
showMessage("🔐 GAL: Authentication required.\\nRun: gal auth login", 'auth_required');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check 2: Token expired
|
|
319
|
+
const tokenPayload = decodeJwt(galConfig.authToken);
|
|
320
|
+
if (tokenPayload && tokenPayload.exp) {
|
|
321
|
+
const expiresAt = tokenPayload.exp * 1000;
|
|
322
|
+
if (Date.now() > expiresAt) {
|
|
323
|
+
showMessage("🔐 GAL: Session expired.\\nRun: gal auth login", 'token_expired');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Check 3: Project not synced
|
|
328
|
+
function readSyncState() {
|
|
329
|
+
const statePath = path.join(process.cwd(), GAL_DIR, SYNC_STATE_FILE);
|
|
330
|
+
if (!fs.existsSync(statePath)) return null;
|
|
331
|
+
try {
|
|
332
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
333
|
+
} catch { return null; }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let state = readSyncState();
|
|
337
|
+
|
|
338
|
+
if (!state) {
|
|
339
|
+
// Attempt auto-sync
|
|
340
|
+
try {
|
|
341
|
+
execSync('gal sync --pull --auto', { stdio: 'pipe', timeout: 30000 });
|
|
342
|
+
state = readSyncState();
|
|
343
|
+
} catch {}
|
|
344
|
+
|
|
345
|
+
if (!state) {
|
|
346
|
+
const orgName = galConfig.defaultOrg || 'your organization';
|
|
347
|
+
showMessage(\`📥 GAL: Not synced with \${orgName}'s approved config.\\nRun: gal sync --pull\`, 'not_synced');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Check 4: Config outdated
|
|
352
|
+
if (state && state.lastSyncHash !== state.approvedConfigHash) {
|
|
353
|
+
// Attempt auto-sync for outdated configs
|
|
354
|
+
try {
|
|
355
|
+
execSync('gal sync --pull --auto', { stdio: 'pipe', timeout: 30000 });
|
|
356
|
+
state = readSyncState();
|
|
357
|
+
} catch {}
|
|
358
|
+
|
|
359
|
+
if (state && state.lastSyncHash !== state.approvedConfigHash) {
|
|
360
|
+
const days = Math.floor((Date.now() - new Date(state.lastSyncTimestamp).getTime()) / (24 * 60 * 60 * 1000));
|
|
361
|
+
showMessage(\`⚠️ GAL: Config is \${days} day(s) behind \${state.organization}'s approved version.\\nRun: gal sync --pull\`, 'config_outdated');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check 5: Missing synced files
|
|
366
|
+
if (state && state.syncedFiles && state.syncedFiles.length > 0) {
|
|
367
|
+
const missingFiles = state.syncedFiles.filter(f => {
|
|
368
|
+
const fullPath = path.join(process.cwd(), f);
|
|
369
|
+
return !fs.existsSync(fullPath);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
if (missingFiles.length > 0) {
|
|
373
|
+
showMessage(\`⚠️ GAL: Missing synced file(s): \${missingFiles.join(', ')}.\\nRun: gal sync --pull\`, 'missing_files');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// All good - build synced status message with optional dispatch rules
|
|
378
|
+
if (!state) {
|
|
379
|
+
showMessage("✅ GAL: Ready", 'synced');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let syncMessage = \`✅ GAL: Synced with \${state.organization}'s approved config (v\${state.version || 'latest'})\`;
|
|
383
|
+
|
|
384
|
+
if (updatedVersion) {
|
|
385
|
+
syncMessage = \`🔄 GAL: Updated to v\${updatedVersion}. \` + syncMessage;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Inject dispatch rules summary if available
|
|
389
|
+
try {
|
|
390
|
+
const dispatchPath = path.join(process.cwd(), '.gal', 'dispatch-rules.json');
|
|
391
|
+
if (fs.existsSync(dispatchPath)) {
|
|
392
|
+
const rules = JSON.parse(fs.readFileSync(dispatchPath, 'utf-8'));
|
|
393
|
+
if (rules.enabled && rules.categories) {
|
|
394
|
+
const eligible = rules.categories.filter(c => c.enabled).map(c => c.name);
|
|
395
|
+
const local = rules.categories.filter(c => !c.enabled).map(c => c.name);
|
|
396
|
+
if (eligible.length > 0) {
|
|
397
|
+
syncMessage += \`\\n📋 Background dispatch: \${eligible.join(', ')} → use \\\`gal dispatch\\\`. \${local.length > 0 ? local.join(', ') + ' → keep local.' : ''}\`;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
// Dispatch rules are optional - ignore errors
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
showMessage(syncMessage, 'synced');
|
|
406
|
+
`;
|
|
407
|
+
|
|
408
|
+
// =============================================================================
|
|
409
|
+
// Status Line Script Content
|
|
410
|
+
// =============================================================================
|
|
411
|
+
// Python script that runs continuously in Claude's status bar.
|
|
412
|
+
// Shows warnings when not synced, silent when synced (avoids status bar spam).
|
|
413
|
+
// Uses uv's inline script runner for dependency management.
|
|
414
|
+
// =============================================================================
|
|
415
|
+
|
|
416
|
+
const STATUS_LINE_CONTENT = `#!/usr/bin/env -S uv run --script
|
|
417
|
+
# /// script
|
|
418
|
+
# requires-python = ">=3.11"
|
|
419
|
+
# dependencies = [
|
|
420
|
+
# "python-dotenv",
|
|
421
|
+
# ]
|
|
422
|
+
# ///
|
|
423
|
+
"""
|
|
424
|
+
GAL Sync Status Line for Claude Code
|
|
425
|
+
Generated by GAL CLI
|
|
426
|
+
|
|
427
|
+
Version: ${STATUS_LINE_VERSION}
|
|
428
|
+
|
|
429
|
+
Behavior:
|
|
430
|
+
- NOT synced: Always show warning
|
|
431
|
+
- Synced: Silent (no output)
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
# GAL_STATUS_LINE_VERSION = "${STATUS_LINE_VERSION}"
|
|
435
|
+
|
|
436
|
+
import json
|
|
437
|
+
import os
|
|
438
|
+
import sys
|
|
439
|
+
import subprocess
|
|
440
|
+
from pathlib import Path
|
|
441
|
+
|
|
442
|
+
# =============================================================================
|
|
443
|
+
# CONFIGURATION
|
|
444
|
+
# =============================================================================
|
|
445
|
+
GAL_DIR = '.gal'
|
|
446
|
+
SYNC_STATE_FILE = 'sync-state.json'
|
|
447
|
+
GAL_CONFIG_FILE = Path.home() / '.gal' / 'config.json'
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def is_gal_installed() -> bool:
|
|
451
|
+
"""Check if GAL CLI is installed."""
|
|
452
|
+
try:
|
|
453
|
+
subprocess.run(['which', 'gal'], capture_output=True, check=True)
|
|
454
|
+
return True
|
|
455
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
456
|
+
return False
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def self_clean():
|
|
460
|
+
"""Remove this status line if GAL CLI is uninstalled."""
|
|
461
|
+
script_path = Path(__file__).resolve()
|
|
462
|
+
settings_path = Path.home() / '.claude' / 'settings.json'
|
|
463
|
+
|
|
464
|
+
# Remove script file
|
|
465
|
+
try:
|
|
466
|
+
script_path.unlink()
|
|
467
|
+
except (OSError, IOError):
|
|
468
|
+
pass
|
|
469
|
+
|
|
470
|
+
# Remove from settings.json
|
|
471
|
+
try:
|
|
472
|
+
if settings_path.exists():
|
|
473
|
+
settings = json.loads(settings_path.read_text())
|
|
474
|
+
status_line_cmd = settings.get('statusLine', {}).get('command', '')
|
|
475
|
+
if 'gal-sync-status' in status_line_cmd:
|
|
476
|
+
del settings['statusLine']
|
|
477
|
+
settings_path.write_text(json.dumps(settings, indent=2))
|
|
478
|
+
except (json.JSONDecodeError, IOError):
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def read_gal_config():
|
|
483
|
+
"""Read GAL CLI config (auth token, default org)."""
|
|
484
|
+
if not GAL_CONFIG_FILE.exists():
|
|
485
|
+
return None
|
|
486
|
+
try:
|
|
487
|
+
return json.loads(GAL_CONFIG_FILE.read_text())
|
|
488
|
+
except (json.JSONDecodeError, IOError):
|
|
489
|
+
return None
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def read_sync_state():
|
|
493
|
+
"""Read sync state from .gal/sync-state.json in current directory."""
|
|
494
|
+
state_path = Path.cwd() / GAL_DIR / SYNC_STATE_FILE
|
|
495
|
+
if not state_path.exists():
|
|
496
|
+
return None
|
|
497
|
+
try:
|
|
498
|
+
return json.loads(state_path.read_text())
|
|
499
|
+
except (json.JSONDecodeError, IOError):
|
|
500
|
+
return None
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def generate_status_line(input_data):
|
|
504
|
+
"""Generate the GAL sync status line.
|
|
505
|
+
|
|
506
|
+
Behavior:
|
|
507
|
+
- NOT synced: Always show warning (no throttle)
|
|
508
|
+
- Synced: Silent (no message)
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
# Self-clean if GAL is uninstalled
|
|
512
|
+
if not is_gal_installed():
|
|
513
|
+
self_clean()
|
|
514
|
+
return ""
|
|
515
|
+
|
|
516
|
+
# Read GAL config
|
|
517
|
+
gal_config = read_gal_config()
|
|
518
|
+
|
|
519
|
+
# Check 1: Not authenticated - always show
|
|
520
|
+
if not gal_config or not gal_config.get('authToken'):
|
|
521
|
+
return "\\033[33m🔐 GAL: login\\033[0m"
|
|
522
|
+
|
|
523
|
+
# Check 2: Project not synced - always show
|
|
524
|
+
state = read_sync_state()
|
|
525
|
+
|
|
526
|
+
if not state:
|
|
527
|
+
return "\\033[33m📥 GAL: sync\\033[0m"
|
|
528
|
+
|
|
529
|
+
# Check 3: Config outdated (hash mismatch) - always show
|
|
530
|
+
if state.get('lastSyncHash') != state.get('approvedConfigHash'):
|
|
531
|
+
return "\\033[33m⚠️ GAL: outdated\\033[0m"
|
|
532
|
+
|
|
533
|
+
# Check 4: Missing synced files - always show
|
|
534
|
+
synced_files = state.get('syncedFiles', [])
|
|
535
|
+
if synced_files:
|
|
536
|
+
missing = [f for f in synced_files if not (Path.cwd() / f).exists()]
|
|
537
|
+
if missing:
|
|
538
|
+
return "\\033[33m⚠️ GAL: missing files\\033[0m"
|
|
539
|
+
|
|
540
|
+
# Synced - stay silent
|
|
541
|
+
return ""
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def main():
|
|
545
|
+
try:
|
|
546
|
+
# Read JSON input from stdin (Claude Code passes context)
|
|
547
|
+
input_data = json.loads(sys.stdin.read())
|
|
548
|
+
|
|
549
|
+
# Generate status line
|
|
550
|
+
status_line = generate_status_line(input_data)
|
|
551
|
+
|
|
552
|
+
# Only output if there's something to show
|
|
553
|
+
if status_line:
|
|
554
|
+
print(status_line)
|
|
555
|
+
|
|
556
|
+
sys.exit(0)
|
|
557
|
+
|
|
558
|
+
except json.JSONDecodeError:
|
|
559
|
+
# Handle JSON decode errors gracefully - stay silent
|
|
560
|
+
sys.exit(0)
|
|
561
|
+
except Exception:
|
|
562
|
+
# Handle any other errors gracefully - stay silent
|
|
563
|
+
sys.exit(0)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
if __name__ == '__main__':
|
|
567
|
+
main()
|
|
568
|
+
`;
|
|
569
|
+
|
|
570
|
+
// =============================================================================
|
|
571
|
+
// Installation Functions
|
|
572
|
+
// =============================================================================
|
|
573
|
+
|
|
574
|
+
// =============================================================================
|
|
575
|
+
// Installation Functions - SessionStart Hook
|
|
576
|
+
// =============================================================================
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Install the SessionStart hook to ~/.claude/hooks/gal-sync-reminder.js
|
|
580
|
+
*
|
|
581
|
+
* The hook shows sync status at the start of each Claude session, prompting
|
|
582
|
+
* users to login or sync if needed. It checks:
|
|
583
|
+
* 1. Authentication status (GAL CLI login)
|
|
584
|
+
* 2. Project sync state (gal sync --pull)
|
|
585
|
+
* 3. Config staleness (hash mismatch)
|
|
586
|
+
* 4. Missing synced files
|
|
587
|
+
*
|
|
588
|
+
* Key behaviors:
|
|
589
|
+
* - Idempotent: Checks HOOK_VERSION marker before writing
|
|
590
|
+
* - Migration: Cleans up old UserPromptSubmit hooks (v1.x used those)
|
|
591
|
+
* - Registration: Adds hook to ~/.claude/settings.json
|
|
592
|
+
* - Self-cleaning: Hook removes itself if GAL CLI is uninstalled
|
|
593
|
+
*
|
|
594
|
+
* @returns {boolean} True if hook was installed or updated, false on error
|
|
595
|
+
*/
|
|
596
|
+
function installHook() {
|
|
597
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
598
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
599
|
+
const hookPath = path.join(hooksDir, 'gal-sync-reminder.js');
|
|
600
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
// Create directories if needed
|
|
604
|
+
if (!fs.existsSync(hooksDir)) {
|
|
605
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Check if hook already exists with current version
|
|
609
|
+
let needsUpdate = true;
|
|
610
|
+
if (fs.existsSync(hookPath)) {
|
|
611
|
+
const existingContent = fs.readFileSync(hookPath, 'utf-8');
|
|
612
|
+
const versionMatch = existingContent.match(/GAL_HOOK_VERSION = "([^"]+)"/);
|
|
613
|
+
if (versionMatch && versionMatch[1] === HOOK_VERSION) {
|
|
614
|
+
needsUpdate = false;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Write the hook file if needed
|
|
619
|
+
if (needsUpdate) {
|
|
620
|
+
fs.writeFileSync(hookPath, HOOK_CONTENT, 'utf-8');
|
|
621
|
+
fs.chmodSync(hookPath, '755');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Update settings.json
|
|
625
|
+
let settings = {};
|
|
626
|
+
if (fs.existsSync(settingsPath)) {
|
|
627
|
+
try {
|
|
628
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
629
|
+
} catch {
|
|
630
|
+
settings = {};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// CLEANUP: Remove old UserPromptSubmit hooks (v1.x migration)
|
|
635
|
+
// GAL CLI v1.x used UserPromptSubmit hooks, but they caused performance issues
|
|
636
|
+
// by running on every user message. v2.x uses SessionStart hooks instead.
|
|
637
|
+
if (settings.hooks?.UserPromptSubmit) {
|
|
638
|
+
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(entry => {
|
|
639
|
+
if (!entry.hooks) return true;
|
|
640
|
+
entry.hooks = entry.hooks.filter(h =>
|
|
641
|
+
!h.command?.includes('gal-') && !h.command?.includes('/gal/')
|
|
642
|
+
);
|
|
643
|
+
return entry.hooks.length > 0;
|
|
644
|
+
});
|
|
645
|
+
if (settings.hooks.UserPromptSubmit.length === 0) {
|
|
646
|
+
delete settings.hooks.UserPromptSubmit;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Register SessionStart hook if not already registered
|
|
651
|
+
const hookCommand = `node ${hookPath}`;
|
|
652
|
+
if (!settings.hooks) settings.hooks = {};
|
|
653
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
654
|
+
|
|
655
|
+
const alreadyRegistered = settings.hooks.SessionStart.some(entry =>
|
|
656
|
+
entry.hooks?.some(h => h.command?.includes('gal-sync-reminder'))
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
if (!alreadyRegistered) {
|
|
660
|
+
settings.hooks.SessionStart.push({
|
|
661
|
+
hooks: [{ type: 'command', command: hookCommand }]
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Write settings
|
|
666
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
667
|
+
|
|
668
|
+
if (needsUpdate) {
|
|
669
|
+
console.log('✓ GAL SessionStart hook installed');
|
|
670
|
+
}
|
|
671
|
+
return true;
|
|
672
|
+
} catch (error) {
|
|
673
|
+
// Silent fail - hook is optional enhancement
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// =============================================================================
|
|
679
|
+
// Installation Functions - GAL CLI Rules
|
|
680
|
+
// =============================================================================
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Install GAL CLI rules to ~/.claude/rules/gal-cli.md
|
|
684
|
+
*
|
|
685
|
+
* Rules provide persistent awareness of GAL CLI commands without hook overhead.
|
|
686
|
+
* Claude automatically loads rules from ~/.claude/rules/ at session start,
|
|
687
|
+
* so the AI knows about `gal` commands and can suggest their usage.
|
|
688
|
+
*
|
|
689
|
+
* Key behaviors:
|
|
690
|
+
* - Idempotent: Checks RULES_VERSION marker before writing
|
|
691
|
+
* - Lightweight: No runtime overhead (unlike hooks that execute on events)
|
|
692
|
+
* - Persistent: Remains loaded for entire Claude session
|
|
693
|
+
*
|
|
694
|
+
* @returns {boolean} True if rules were installed or updated, false on error
|
|
695
|
+
*/
|
|
696
|
+
function installRules() {
|
|
697
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
698
|
+
const rulesDir = path.join(claudeDir, 'rules');
|
|
699
|
+
const rulesPath = path.join(rulesDir, 'gal-cli.md');
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
// Create rules directory if needed
|
|
703
|
+
if (!fs.existsSync(rulesDir)) {
|
|
704
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Check if rules file already exists with current version
|
|
708
|
+
let needsUpdate = true;
|
|
709
|
+
if (fs.existsSync(rulesPath)) {
|
|
710
|
+
const existingContent = fs.readFileSync(rulesPath, 'utf-8');
|
|
711
|
+
const versionMatch = existingContent.match(/GAL_RULES_VERSION = "([^"]+)"/);
|
|
712
|
+
if (versionMatch && versionMatch[1] === RULES_VERSION) {
|
|
713
|
+
needsUpdate = false;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Write the rules file if needed
|
|
718
|
+
if (needsUpdate) {
|
|
719
|
+
fs.writeFileSync(rulesPath, GAL_CLI_RULES_CONTENT, 'utf-8');
|
|
720
|
+
console.log('✓ GAL CLI rules installed');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return true;
|
|
724
|
+
} catch (error) {
|
|
725
|
+
// Silent fail - rules are optional enhancement
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// =============================================================================
|
|
731
|
+
// Installation Functions - Telemetry Queue
|
|
732
|
+
// =============================================================================
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Queue a telemetry event for the next CLI run (GAL-114)
|
|
736
|
+
*
|
|
737
|
+
* This postinstall script is CommonJS, but the telemetry module is ESM,
|
|
738
|
+
* so we can't import it directly. Instead, we write events to a pending
|
|
739
|
+
* file (~/.gal/telemetry-pending-events.json) that the CLI picks up and
|
|
740
|
+
* flushes on its next execution.
|
|
741
|
+
*
|
|
742
|
+
* Events are structured as:
|
|
743
|
+
* - id: Unique event identifier (UUID)
|
|
744
|
+
* - eventType: 'hook_triggered' for installation
|
|
745
|
+
* - timestamp: ISO 8601 timestamp
|
|
746
|
+
* - payload: Event-specific data (cliVersion, platform, nodeVersion)
|
|
747
|
+
* - queuedAt: Unix timestamp when event was queued
|
|
748
|
+
*
|
|
749
|
+
* @returns {void}
|
|
750
|
+
*/
|
|
751
|
+
function queueTelemetryEvent() {
|
|
752
|
+
const pendingEventsPath = path.join(os.homedir(), '.gal', 'telemetry-pending-events.json');
|
|
753
|
+
const galDir = path.join(os.homedir(), '.gal');
|
|
754
|
+
|
|
755
|
+
let pending = [];
|
|
756
|
+
try {
|
|
757
|
+
if (fs.existsSync(pendingEventsPath)) {
|
|
758
|
+
pending = JSON.parse(fs.readFileSync(pendingEventsPath, 'utf-8'));
|
|
759
|
+
}
|
|
760
|
+
} catch {}
|
|
761
|
+
|
|
762
|
+
// Add postinstall hook event
|
|
763
|
+
pending.push({
|
|
764
|
+
id: require('crypto').randomUUID(),
|
|
765
|
+
eventType: 'hook_triggered',
|
|
766
|
+
timestamp: new Date().toISOString(),
|
|
767
|
+
payload: {
|
|
768
|
+
notificationType: 'postinstall',
|
|
769
|
+
cliVersion,
|
|
770
|
+
platform: process.platform,
|
|
771
|
+
nodeVersion: process.version,
|
|
772
|
+
},
|
|
773
|
+
queuedAt: Date.now(),
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
if (!fs.existsSync(galDir)) {
|
|
778
|
+
fs.mkdirSync(galDir, { recursive: true });
|
|
779
|
+
}
|
|
780
|
+
fs.writeFileSync(pendingEventsPath, JSON.stringify(pending), 'utf-8');
|
|
781
|
+
} catch {
|
|
782
|
+
// Ignore errors - telemetry is optional
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// =============================================================================
|
|
787
|
+
// Installation Functions - Status Line Script
|
|
788
|
+
// =============================================================================
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Install the status line script to ~/.claude/status_lines/gal-sync-status.py
|
|
792
|
+
*
|
|
793
|
+
* The status line script runs continuously in Claude's status bar, showing
|
|
794
|
+
* sync warnings when the project is not synced with the org's approved config.
|
|
795
|
+
* Silent when synced (avoids status bar spam).
|
|
796
|
+
*
|
|
797
|
+
* Key behaviors:
|
|
798
|
+
* - Idempotent: Checks STATUS_LINE_VERSION marker before writing
|
|
799
|
+
* - Respectful: Won't overwrite user's existing custom statusLine
|
|
800
|
+
* - Registration: Adds to ~/.claude/settings.json statusLine field
|
|
801
|
+
* - Self-cleaning: Script removes itself if GAL CLI is uninstalled
|
|
802
|
+
*
|
|
803
|
+
* @returns {boolean} True if status line was installed or updated, false on error
|
|
804
|
+
*/
|
|
805
|
+
function installStatusLine() {
|
|
806
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
807
|
+
const statusLinesDir = path.join(claudeDir, 'status_lines');
|
|
808
|
+
const scriptPath = path.join(statusLinesDir, 'gal-sync-status.py');
|
|
809
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
810
|
+
|
|
811
|
+
try {
|
|
812
|
+
// Check existing settings for custom statusLine
|
|
813
|
+
let settings = {};
|
|
814
|
+
if (fs.existsSync(settingsPath)) {
|
|
815
|
+
try {
|
|
816
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
817
|
+
} catch {
|
|
818
|
+
settings = {};
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Don't overwrite user's custom statusLine (respect user's existing config)
|
|
823
|
+
if (settings.statusLine?.command && !settings.statusLine.command.includes('gal-sync-status')) {
|
|
824
|
+
console.log('ℹ Custom statusLine detected, skipping GAL status line');
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Create directories if needed
|
|
829
|
+
if (!fs.existsSync(statusLinesDir)) {
|
|
830
|
+
fs.mkdirSync(statusLinesDir, { recursive: true });
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Check if script already exists with current version
|
|
834
|
+
let needsUpdate = true;
|
|
835
|
+
if (fs.existsSync(scriptPath)) {
|
|
836
|
+
const existingContent = fs.readFileSync(scriptPath, 'utf-8');
|
|
837
|
+
const versionMatch = existingContent.match(/GAL_STATUS_LINE_VERSION = "([^"]+)"/);
|
|
838
|
+
if (versionMatch && versionMatch[1] === STATUS_LINE_VERSION) {
|
|
839
|
+
// Also check if it's registered in settings
|
|
840
|
+
if (settings.statusLine?.command?.includes('gal-sync-status')) {
|
|
841
|
+
needsUpdate = false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Write the script file if needed
|
|
847
|
+
if (needsUpdate) {
|
|
848
|
+
fs.writeFileSync(scriptPath, STATUS_LINE_CONTENT, 'utf-8');
|
|
849
|
+
fs.chmodSync(scriptPath, '755');
|
|
850
|
+
|
|
851
|
+
// Register in settings.json
|
|
852
|
+
settings.statusLine = {
|
|
853
|
+
type: 'command',
|
|
854
|
+
command: scriptPath
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
858
|
+
console.log('✓ GAL status line installed');
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return true;
|
|
862
|
+
} catch (error) {
|
|
863
|
+
// Silent fail - status line is optional enhancement
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function detectPackageManager() {
|
|
869
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
870
|
+
|
|
871
|
+
if (userAgent.startsWith('pnpm/')) {
|
|
872
|
+
return 'pnpm';
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (userAgent.startsWith('npm/')) {
|
|
876
|
+
return 'npm';
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return 'unknown';
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function recordInstallMetadata() {
|
|
883
|
+
try {
|
|
884
|
+
const galDir = path.join(os.homedir(), '.gal');
|
|
885
|
+
if (!fs.existsSync(galDir)) {
|
|
886
|
+
fs.mkdirSync(galDir, { recursive: true });
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const packageManager = detectPackageManager();
|
|
890
|
+
const method = packageManager === 'pnpm' ? 'pnpm' : packageManager === 'npm' ? 'npm' : 'unknown';
|
|
891
|
+
const binaryPath = path.join(__dirname, '..', 'dist', 'index.cjs');
|
|
892
|
+
const metadataPath = path.join(galDir, 'install-metadata.json');
|
|
893
|
+
const metadata = {
|
|
894
|
+
binaryPath,
|
|
895
|
+
installedAt: new Date().toISOString(),
|
|
896
|
+
method,
|
|
897
|
+
packageManager,
|
|
898
|
+
platform: process.platform,
|
|
899
|
+
version: cliVersion,
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
|
|
903
|
+
} catch {
|
|
904
|
+
// Silent fail - metadata is best-effort only
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// =============================================================================
|
|
909
|
+
// Main
|
|
910
|
+
// =============================================================================
|
|
911
|
+
|
|
912
|
+
function main() {
|
|
913
|
+
recordInstallMetadata();
|
|
914
|
+
const hookInstalled = installHook();
|
|
915
|
+
const rulesInstalled = installRules();
|
|
916
|
+
const statusLineInstalled = installStatusLine();
|
|
917
|
+
|
|
918
|
+
// Queue telemetry event (GAL-114)
|
|
919
|
+
queueTelemetryEvent();
|
|
920
|
+
|
|
921
|
+
if (hookInstalled || rulesInstalled || statusLineInstalled) {
|
|
922
|
+
console.log('');
|
|
923
|
+
console.log('Restart Claude Code/Cursor for changes to take effect.');
|
|
924
|
+
console.log('');
|
|
925
|
+
console.log('Next steps:');
|
|
926
|
+
console.log(' 1. gal auth login - Authenticate with GitHub');
|
|
927
|
+
console.log(' 2. gal sync --pull - Download org-approved config');
|
|
928
|
+
console.log('');
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
main();
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GAL CLI Preuninstall Script
|
|
4
|
+
*
|
|
5
|
+
* Automatically cleans up GAL-installed files when the CLI is uninstalled via pnpm.
|
|
6
|
+
* This script runs as a package lifecycle hook before package removal.
|
|
7
|
+
*
|
|
8
|
+
* What it cleans:
|
|
9
|
+
* 1. Hook files → ~/.claude/hooks/gal-*.js
|
|
10
|
+
* 2. Status line script → ~/.claude/status_lines/gal-sync-status.py
|
|
11
|
+
* 3. Rules file → ~/.claude/rules/gal-cli.md
|
|
12
|
+
* 4. Hook entries in ~/.claude/settings.json (preserves file and other hooks)
|
|
13
|
+
* 5. GAL config directory → ~/.gal/ (auth tokens, sync state, telemetry queue)
|
|
14
|
+
*
|
|
15
|
+
* Cleanup scope:
|
|
16
|
+
* - User-level files only (not project-level .gal directories)
|
|
17
|
+
* - GAL-specific entries only (preserves user's other Claude configs)
|
|
18
|
+
* - Silent fail strategy (won't prevent uninstall on errors)
|
|
19
|
+
*
|
|
20
|
+
* When it runs:
|
|
21
|
+
* - Automatically during `pnpm rm -g @scheduler-systems/gal-run`
|
|
22
|
+
* - Before package files are removed from node_modules
|
|
23
|
+
* - Can be manually triggered via `pnpm run preuninstall` in the CLI package directory
|
|
24
|
+
*
|
|
25
|
+
* Prerequisites:
|
|
26
|
+
* - Node.js 18+ (CommonJS module)
|
|
27
|
+
* - Runs with user's file system permissions
|
|
28
|
+
*
|
|
29
|
+
* Related files:
|
|
30
|
+
* - apps/cli/scripts/postinstall.cjs - Installation script
|
|
31
|
+
* - apps/cli/package.json - package lifecycle hooks configuration
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const os = require('os');
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// Cleanup Functions - settings.json
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Remove GAL hook entries from settings.json without deleting the file.
|
|
44
|
+
*
|
|
45
|
+
* This function surgically removes only GAL-specific hook entries while
|
|
46
|
+
* preserving the user's other hooks and settings. It handles:
|
|
47
|
+
* - Filtering GAL hooks from UserPromptSubmit array
|
|
48
|
+
* - Filtering GAL hooks from SessionStart array (v2.x)
|
|
49
|
+
* - Removing empty hook arrays after filtering
|
|
50
|
+
* - Preserving the settings.json file structure
|
|
51
|
+
*
|
|
52
|
+
* @param {string} settingsPath - Full path to ~/.claude/settings.json
|
|
53
|
+
* @returns {boolean} True if any GAL entries were removed, false otherwise
|
|
54
|
+
*/
|
|
55
|
+
function removeGalHookEntries(settingsPath) {
|
|
56
|
+
try {
|
|
57
|
+
if (!fs.existsSync(settingsPath)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
62
|
+
|
|
63
|
+
if (!settings.hooks?.UserPromptSubmit) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Filter out GAL hooks while preserving user's other hooks
|
|
68
|
+
const originalLength = settings.hooks.UserPromptSubmit.length;
|
|
69
|
+
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter((entry) => {
|
|
70
|
+
if (!entry.hooks) return true;
|
|
71
|
+
// Keep entry only if it has non-GAL hooks
|
|
72
|
+
entry.hooks = entry.hooks.filter((hook) =>
|
|
73
|
+
!hook.command?.includes('gal-') && !hook.command?.includes('/gal/')
|
|
74
|
+
);
|
|
75
|
+
return entry.hooks.length > 0;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Remove empty hooks array
|
|
79
|
+
if (settings.hooks.UserPromptSubmit.length === 0) {
|
|
80
|
+
delete settings.hooks.UserPromptSubmit;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Remove empty hooks object
|
|
84
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
85
|
+
delete settings.hooks;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (settings.hooks?.UserPromptSubmit?.length !== originalLength) {
|
|
89
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false;
|
|
94
|
+
} catch {
|
|
95
|
+
// Silent fail - don't prevent uninstall if settings.json is corrupted
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// Cleanup Functions - User-Level Files
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Clean up GAL-installed files from user's home directory.
|
|
106
|
+
*
|
|
107
|
+
* Removes:
|
|
108
|
+
* - Hook files: ~/.claude/hooks/gal-*.js
|
|
109
|
+
* - Status line script: ~/.claude/status_lines/gal-sync-status.py
|
|
110
|
+
* - Rules file: ~/.claude/rules/gal-cli.md
|
|
111
|
+
* - Hook entries from settings.json
|
|
112
|
+
*
|
|
113
|
+
* Uses silent fail strategy - logs removed files but doesn't throw errors.
|
|
114
|
+
* This ensures uninstall proceeds even if individual files are missing or
|
|
115
|
+
* have permission issues.
|
|
116
|
+
*
|
|
117
|
+
* @returns {string[]} Array of paths to files that were successfully removed
|
|
118
|
+
*/
|
|
119
|
+
function cleanupUserLevel() {
|
|
120
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
121
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
122
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
123
|
+
|
|
124
|
+
let removed = [];
|
|
125
|
+
|
|
126
|
+
// Remove GAL hook files
|
|
127
|
+
if (fs.existsSync(hooksDir)) {
|
|
128
|
+
try {
|
|
129
|
+
const files = fs.readdirSync(hooksDir);
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
if (file.startsWith('gal-')) {
|
|
132
|
+
const hookPath = path.join(hooksDir, file);
|
|
133
|
+
try {
|
|
134
|
+
fs.unlinkSync(hookPath);
|
|
135
|
+
removed.push(hookPath);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
// Silent fail
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
// Silent fail
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Remove GAL hook entries from settings.json
|
|
147
|
+
if (removeGalHookEntries(settingsPath)) {
|
|
148
|
+
removed.push(`${settingsPath} (GAL hooks removed)`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return removed;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// =============================================================================
|
|
155
|
+
// Cleanup Functions - GAL Config Directory
|
|
156
|
+
// =============================================================================
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Clean up GAL config directory from user's home directory.
|
|
160
|
+
*
|
|
161
|
+
* Removes entire ~/.gal directory, including:
|
|
162
|
+
* - config.json (auth token, default org)
|
|
163
|
+
* - telemetry-pending-events.json (queued telemetry events)
|
|
164
|
+
* - Any other GAL-specific cache or state files
|
|
165
|
+
*
|
|
166
|
+
* Note: Does NOT remove project-level .gal directories (those belong to repos)
|
|
167
|
+
*
|
|
168
|
+
* Uses silent fail strategy to ensure uninstall proceeds even if directory
|
|
169
|
+
* is missing or has permission issues.
|
|
170
|
+
*
|
|
171
|
+
* @returns {string[]} Array containing the removed directory path, or empty array
|
|
172
|
+
*/
|
|
173
|
+
function cleanupGalConfig() {
|
|
174
|
+
const galConfigDir = path.join(os.homedir(), '.gal');
|
|
175
|
+
|
|
176
|
+
if (fs.existsSync(galConfigDir)) {
|
|
177
|
+
try {
|
|
178
|
+
fs.rmSync(galConfigDir, { recursive: true, force: true });
|
|
179
|
+
return [galConfigDir];
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// Silent fail
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// =============================================================================
|
|
190
|
+
// Main Cleanup Orchestration
|
|
191
|
+
// =============================================================================
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Main cleanup orchestration function.
|
|
195
|
+
*
|
|
196
|
+
* Coordinates all cleanup operations and provides user feedback.
|
|
197
|
+
* Runs both user-level and config directory cleanups, then reports results.
|
|
198
|
+
*
|
|
199
|
+
* @returns {void}
|
|
200
|
+
*/
|
|
201
|
+
function cleanup() {
|
|
202
|
+
console.log('\n═══════════════════════════════════════════════════');
|
|
203
|
+
console.log(' GAL CLI Uninstall Cleanup');
|
|
204
|
+
console.log('═══════════════════════════════════════════════════\n');
|
|
205
|
+
|
|
206
|
+
const userLevelFiles = cleanupUserLevel();
|
|
207
|
+
const galConfigFiles = cleanupGalConfig();
|
|
208
|
+
const allRemoved = [...userLevelFiles, ...galConfigFiles];
|
|
209
|
+
|
|
210
|
+
if (allRemoved.length > 0) {
|
|
211
|
+
console.log('✓ Cleaned up GAL files:');
|
|
212
|
+
for (const file of allRemoved) {
|
|
213
|
+
console.log(` - ${file}`);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
console.log('No GAL files found to clean up.');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log('\n═══════════════════════════════════════════════════');
|
|
220
|
+
console.log(' GAL CLI has been uninstalled');
|
|
221
|
+
console.log('═══════════════════════════════════════════════════\n');
|
|
222
|
+
console.log('To reinstall: pnpm add -g @scheduler-systems/gal-run\n');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// =============================================================================
|
|
226
|
+
// Entry Point
|
|
227
|
+
// =============================================================================
|
|
228
|
+
|
|
229
|
+
// Run cleanup with silent fail strategy
|
|
230
|
+
// Even if cleanup fails, we don't prevent npm uninstall from proceeding
|
|
231
|
+
try {
|
|
232
|
+
cleanup();
|
|
233
|
+
} catch (error) {
|
|
234
|
+
// Log error but allow uninstall to continue
|
|
235
|
+
console.error('GAL cleanup encountered an error, but uninstall will proceed.');
|
|
236
|
+
// Note: We don't process.exit(1) here - uninstall should always succeed
|
|
237
|
+
}
|