@jaguilar87/gaia-ops 2.5.3 → 2.5.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.
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @jaguilar87/gaia-ops - Cleanup script
5
+ *
6
+ * Runs automatically on npm uninstall (preuninstall hook)
7
+ *
8
+ * Purpose:
9
+ * - Remove CLAUDE.md
10
+ * - Remove settings.json
11
+ * - Remove all symlinks (agents, tools, hooks, commands, config, templates, speckit, CHANGELOG.md)
12
+ * - Preserve project-specific data (logs, tests, project-context, session, metrics)
13
+ *
14
+ * Usage: Automatic (npm preuninstall hook)
15
+ */
16
+
17
+ import { fileURLToPath } from 'url';
18
+ import { dirname, join } from 'path';
19
+ import fs from 'fs/promises';
20
+ import { existsSync } from 'fs';
21
+ import chalk from 'chalk';
22
+ import ora from 'ora';
23
+
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = dirname(__filename);
26
+ const CWD = process.env.INIT_CWD || process.cwd();
27
+
28
+ /**
29
+ * Remove CLAUDE.md if it exists
30
+ */
31
+ async function removeClaudeMd() {
32
+ const spinner = ora('Removing CLAUDE.md...').start();
33
+
34
+ try {
35
+ const claudeMdPath = join(CWD, 'CLAUDE.md');
36
+
37
+ if (!existsSync(claudeMdPath)) {
38
+ spinner.info('CLAUDE.md not found, skipping');
39
+ return false;
40
+ }
41
+
42
+ await fs.unlink(claudeMdPath);
43
+ spinner.succeed('CLAUDE.md removed');
44
+ return true;
45
+ } catch (error) {
46
+ spinner.fail(`Failed to remove CLAUDE.md: ${error.message}`);
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Remove settings.json if it exists
53
+ */
54
+ async function removeSettingsJson() {
55
+ const spinner = ora('Removing settings.json...').start();
56
+
57
+ try {
58
+ const settingsPath = join(CWD, '.claude', 'settings.json');
59
+
60
+ if (!existsSync(settingsPath)) {
61
+ spinner.info('settings.json not found, skipping');
62
+ return false;
63
+ }
64
+
65
+ await fs.unlink(settingsPath);
66
+ spinner.succeed('settings.json removed');
67
+ return true;
68
+ } catch (error) {
69
+ spinner.fail(`Failed to remove settings.json: ${error.message}`);
70
+ return false;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Remove all symlinks in .claude/ directory
76
+ */
77
+ async function removeSymlinks() {
78
+ const spinner = ora('Removing symlinks...').start();
79
+
80
+ try {
81
+ const claudeDir = join(CWD, '.claude');
82
+
83
+ if (!existsSync(claudeDir)) {
84
+ spinner.info('.claude/ directory not found, skipping');
85
+ return false;
86
+ }
87
+
88
+ // Define all symlinks that should be removed
89
+ const symlinks = [
90
+ join(claudeDir, 'agents'),
91
+ join(claudeDir, 'tools'),
92
+ join(claudeDir, 'hooks'),
93
+ join(claudeDir, 'commands'),
94
+ join(claudeDir, 'templates'),
95
+ join(claudeDir, 'config'),
96
+ join(claudeDir, 'speckit'),
97
+ join(claudeDir, 'CHANGELOG.md'),
98
+ join(claudeDir, 'README.en.md'),
99
+ join(claudeDir, 'README.md')
100
+ ];
101
+
102
+ let removed = 0;
103
+ for (const symlinkPath of symlinks) {
104
+ if (existsSync(symlinkPath)) {
105
+ try {
106
+ await fs.unlink(symlinkPath);
107
+ removed++;
108
+ } catch (error) {
109
+ // Ignore errors
110
+ }
111
+ }
112
+ }
113
+
114
+ if (removed > 0) {
115
+ spinner.succeed(`Removed ${removed} symlink(s)`);
116
+ return true;
117
+ } else {
118
+ spinner.info('No symlinks found to remove');
119
+ return false;
120
+ }
121
+ } catch (error) {
122
+ spinner.fail(`Failed to remove symlinks: ${error.message}`);
123
+ return false;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Main function
129
+ */
130
+ async function main() {
131
+ console.log(chalk.cyan('\n🧹 @jaguilar87/gaia-ops cleanup\n'));
132
+
133
+ try {
134
+ const claudeRemoved = await removeClaudeMd();
135
+ const settingsRemoved = await removeSettingsJson();
136
+ const symlinksRemoved = await removeSymlinks();
137
+
138
+ if (claudeRemoved || settingsRemoved || symlinksRemoved) {
139
+ console.log(chalk.green('\n✅ Cleanup completed\n'));
140
+ console.log(chalk.gray('Preserved data:'));
141
+ console.log(chalk.gray(' • .claude/logs/'));
142
+ console.log(chalk.gray(' • .claude/tests/'));
143
+ console.log(chalk.gray(' • .claude/project-context/'));
144
+ console.log(chalk.gray(' • .claude/session/'));
145
+ console.log(chalk.gray(' • .claude/metrics/\n'));
146
+ } else {
147
+ console.log(chalk.gray('\n✓ Nothing to clean up\n'));
148
+ }
149
+ } catch (error) {
150
+ console.error(chalk.red(`\n❌ Cleanup failed: ${error.message}\n`));
151
+ // Don't fail npm uninstall, just warn
152
+ process.exit(0);
153
+ }
154
+ }
155
+
156
+ main();
@@ -29,11 +29,11 @@ const __dirname = dirname(__filename);
29
29
  const CWD = process.env.INIT_CWD || process.cwd();
30
30
 
31
31
  /**
32
- * Check if CLAUDE.md exists (to determine if this is first-time install)
32
+ * Check if .claude/ directory exists (to determine if this is first-time install)
33
33
  */
34
- async function claudeMdExists() {
35
- const claudeMdPath = join(CWD, 'CLAUDE.md');
36
- return existsSync(claudeMdPath);
34
+ async function isExistingInstallation() {
35
+ const claudeDir = join(CWD, '.claude');
36
+ return existsSync(claudeDir);
37
37
  }
38
38
 
39
39
  /**
@@ -52,22 +52,25 @@ async function updateClaudeMd() {
52
52
  }
53
53
 
54
54
  const claudeMdPath = join(CWD, 'CLAUDE.md');
55
+ const claudeDir = join(CWD, '.claude');
55
56
 
56
- // Check if this is first-time install
57
- if (!existsSync(claudeMdPath)) {
58
- // First time install - don't auto-generate, gaia-init handles it
57
+ // Check if .claude/ exists (indicates this is NOT first-time install)
58
+ if (!existsSync(claudeDir)) {
59
+ // True first-time install - skip, let gaia-init handle it
59
60
  spinner.info('First-time installation detected - skipping auto-update');
60
61
  return false;
61
62
  }
62
63
 
63
- // Read template and copy it directly (no placeholder replacement)
64
- // The template will be copied as-is with placeholders intact
64
+ // .claude/ exists, so this is an existing installation
65
+ // Regenerate CLAUDE.md (whether it exists or not)
66
+ const fileExistedBefore = existsSync(claudeMdPath);
65
67
  const template = await fs.readFile(templatePath, 'utf-8');
66
68
 
67
- // Write updated CLAUDE.md (OVERWRITES existing file)
69
+ // Write/overwrite CLAUDE.md
68
70
  await fs.writeFile(claudeMdPath, template, 'utf-8');
69
71
 
70
- spinner.succeed('CLAUDE.md updated successfully (existing file overwritten)');
72
+ const action = fileExistedBefore ? 'updated successfully (existing file overwritten)' : 'created successfully';
73
+ spinner.succeed(`CLAUDE.md ${action}`);
71
74
  return true;
72
75
  } catch (error) {
73
76
  spinner.fail(`Failed to update CLAUDE.md: ${error.message}`);
@@ -110,6 +113,74 @@ async function updateSettingsJson() {
110
113
  }
111
114
  }
112
115
 
116
+ /**
117
+ * Recreate missing symlinks in .claude/ directory
118
+ */
119
+ async function recreateSymlinks() {
120
+ const spinner = ora('Checking symlinks...').start();
121
+
122
+ try {
123
+ const claudeDir = join(CWD, '.claude');
124
+
125
+ // Skip if .claude/ doesn't exist (first-time install)
126
+ if (!existsSync(claudeDir)) {
127
+ spinner.info('First-time installation detected - skipping symlink check');
128
+ return false;
129
+ }
130
+
131
+ // Calculate relative path from .claude/ to node_modules/@jaguilar87/gaia-ops
132
+ const packagePath = join(CWD, 'node_modules', '@jaguilar87', 'gaia-ops');
133
+
134
+ // Verify package exists
135
+ if (!existsSync(packagePath)) {
136
+ spinner.fail('Package not found in node_modules');
137
+ return false;
138
+ }
139
+
140
+ const { relative } = await import('path');
141
+ const relativePath = relative(claudeDir, packagePath);
142
+
143
+ // Define all symlinks that should exist
144
+ const symlinks = [
145
+ { target: join(relativePath, 'agents'), link: join(claudeDir, 'agents'), name: 'agents' },
146
+ { target: join(relativePath, 'tools'), link: join(claudeDir, 'tools'), name: 'tools' },
147
+ { target: join(relativePath, 'hooks'), link: join(claudeDir, 'hooks'), name: 'hooks' },
148
+ { target: join(relativePath, 'commands'), link: join(claudeDir, 'commands'), name: 'commands' },
149
+ { target: join(relativePath, 'templates'), link: join(claudeDir, 'templates'), name: 'templates' },
150
+ { target: join(relativePath, 'config'), link: join(claudeDir, 'config'), name: 'config' },
151
+ { target: join(relativePath, 'speckit'), link: join(claudeDir, 'speckit'), name: 'speckit' },
152
+ { target: join(relativePath, 'CHANGELOG.md'), link: join(claudeDir, 'CHANGELOG.md'), name: 'CHANGELOG.md' }
153
+ ];
154
+
155
+ let recreated = 0;
156
+ for (const { target, link, name } of symlinks) {
157
+ // Check if symlink exists and is valid
158
+ if (existsSync(link)) {
159
+ continue; // Symlink exists, skip
160
+ }
161
+
162
+ // Symlink missing, recreate it
163
+ try {
164
+ await fs.symlink(target, link);
165
+ recreated++;
166
+ } catch (error) {
167
+ // Ignore errors, might be permissions or already exists
168
+ }
169
+ }
170
+
171
+ if (recreated > 0) {
172
+ spinner.succeed(`Recreated ${recreated} missing symlink(s)`);
173
+ return true;
174
+ } else {
175
+ spinner.succeed('All symlinks are valid');
176
+ return false;
177
+ }
178
+ } catch (error) {
179
+ spinner.fail(`Failed to check symlinks: ${error.message}`);
180
+ return false;
181
+ }
182
+ }
183
+
113
184
  /**
114
185
  * Main function
115
186
  */
@@ -118,7 +189,7 @@ async function main() {
118
189
 
119
190
  try {
120
191
  // Check if this is an update (not first-time install)
121
- const isUpdate = await claudeMdExists();
192
+ const isUpdate = await isExistingInstallation();
122
193
 
123
194
  if (isUpdate) {
124
195
  // Show warning before overwriting files
@@ -130,8 +201,9 @@ async function main() {
130
201
 
131
202
  const claudeUpdated = await updateClaudeMd();
132
203
  const settingsUpdated = await updateSettingsJson();
204
+ const symlinksRecreated = await recreateSymlinks();
133
205
 
134
- if (claudeUpdated || settingsUpdated) {
206
+ if (claudeUpdated || settingsUpdated || symlinksRecreated) {
135
207
  console.log(chalk.green('\n✅ Auto-update completed\n'));
136
208
  console.log(chalk.yellow('⚠️ IMPORTANT: Files have been overwritten from templates'));
137
209
  console.log(chalk.gray('\nNext steps:'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaguilar87/gaia-ops",
3
- "version": "2.5.3",
3
+ "version": "2.5.5",
4
4
  "description": "Multi-agent orchestration system for Claude Code - DevOps automation toolkit",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -56,7 +56,8 @@
56
56
  "pre-publish:dry": "node bin/pre-publish-validate.js --dry-run",
57
57
  "pre-publish:validate": "node bin/pre-publish-validate.js --validate-only",
58
58
  "prepublishOnly": "npm run clean",
59
- "postinstall": "node bin/gaia-update.js"
59
+ "postinstall": "node bin/gaia-update.js",
60
+ "preuninstall": "node bin/gaia-cleanup.js"
60
61
  },
61
62
  "_postinstall_note": "⚠️ postinstall hook overwrites CLAUDE.md and settings.json from templates on every npm install/update",
62
63
  "dependencies": {