@mfjjs/ruflo-setup 0.2.7 → 0.2.9

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/src/setup.js CHANGED
@@ -1,380 +1,414 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import os from 'node:os';
4
- import { spawnSync } from 'node:child_process';
5
- import { pathExists, copyFileSync, confirm, toPlatformMcpConfig, writeJson } from './utils.js';
6
- import { installGlobalCheckRufloHook } from './hooks.js';
7
-
8
- function logLine(message) {
9
- process.stdout.write(`${message}\n`);
10
- }
11
-
12
- function getPnpmInstallSuggestions(platform) {
13
- if (platform === 'win32') {
14
- return [
15
- 'winget install -e --id pnpm.pnpm',
16
- 'corepack enable && corepack prepare pnpm@latest --activate',
17
- 'npm install -g pnpm'
18
- ];
19
- }
20
-
21
- if (platform === 'darwin') {
22
- return [
23
- 'brew install pnpm',
24
- 'corepack enable && corepack prepare pnpm@latest --activate',
25
- 'npm install -g pnpm'
26
- ];
27
- }
28
-
29
- return [
30
- 'curl -fsSL https://get.pnpm.io/install.sh | sh -',
31
- 'corepack enable && corepack prepare pnpm@latest --activate',
32
- 'npm install -g pnpm'
33
- ];
34
- }
35
-
36
- const MIN_PNPM_VERSION = '10.32.1';
37
-
38
- function parseSemver(str) {
39
- const [major = 0, minor = 0, patch = 0] = str.trim().split('.').map(Number);
40
- return { major, minor, patch };
41
- }
42
-
43
- function semverGte(a, b) {
44
- if (a.major !== b.major) return a.major > b.major;
45
- if (a.minor !== b.minor) return a.minor > b.minor;
46
- return a.patch >= b.patch;
47
- }
48
-
49
- function ensurePnpmAvailable() {
50
- const check = spawnSync('pnpm', ['--version'], {
51
- stdio: ['ignore', 'pipe', 'ignore'],
52
- shell: process.platform === 'win32'
53
- });
54
-
55
- if (check.status !== 0 || check.error) {
56
- const platformLabel = process.platform === 'win32'
57
- ? 'Windows'
58
- : process.platform === 'darwin'
59
- ? 'macOS'
60
- : 'Linux';
61
- const suggestions = getPnpmInstallSuggestions(process.platform)
62
- .map((command) => ` - ${command}`)
63
- .join('\n');
64
-
65
- throw new Error(
66
- `pnpm is required but was not found in PATH.\n` +
67
- `Install pnpm, then re-run ruflo-setup.\n` +
68
- `Quick install options for ${platformLabel}:\n${suggestions}`
69
- );
70
- }
71
-
72
- const version = (check.stdout || '').toString().trim();
73
- if (!semverGte(parseSemver(version), parseSemver(MIN_PNPM_VERSION))) {
74
- throw new Error(
75
- `pnpm ${MIN_PNPM_VERSION} or higher is required, but found ${version}.\n` +
76
- `Upgrade with: pnpm self-update`
77
- );
78
- }
79
- }
80
-
81
- function runPnpmInit({ force, cwd, dryRun }) {
82
- const initArgs = ['init', '--full'];
83
- if (force) {
84
- initArgs.push('--force');
85
- }
86
-
87
- if (dryRun) {
88
- logLine(` [DRY RUN] Would run: pnpm add -g ruflo@latest`);
89
- logLine(` [DRY RUN] Would run: pnpm approve-builds -g --all (if changes detected)`);
90
- logLine(` [DRY RUN] Would run: ruflo ${initArgs.join(' ')}`);
91
- return;
92
- }
93
-
94
- ensurePnpmAvailable();
95
-
96
- // Capture stdout to detect whether pnpm installed/updated anything.
97
- // Progress spinners go to stderr (still shown to user); stdout has the summary.
98
- const install = spawnSync('pnpm', ['add', '-g', 'ruflo@latest'], {
99
- cwd,
100
- stdio: ['inherit', 'pipe', 'inherit'],
101
- shell: process.platform === 'win32'
102
- });
103
-
104
- const installOutput = (install.stdout || '').toString();
105
- if (installOutput) {
106
- process.stdout.write(installOutput);
107
- }
108
-
109
- if (install.status !== 0) {
110
- throw new Error(`pnpm add -g ruflo@latest failed with exit code ${install.status}`);
111
- }
112
-
113
- // pnpm prints a "Packages:" summary line and "+ pkg version" lines when
114
- // something is actually installed or updated. When already up to date the
115
- // stdout is empty or contains only "Already up to date".
116
- const somethingChanged = /Packages:/i.test(installOutput) || /^\+\s/m.test(installOutput);
117
- if (somethingChanged) {
118
- logLine(' Changes detected — running pnpm approve-builds -g --all ...');
119
- const approve = spawnSync('pnpm', ['approve-builds', '-g', '--all'], {
120
- cwd,
121
- stdio: 'inherit',
122
- shell: process.platform === 'win32'
123
- });
124
- if (approve.status !== 0) {
125
- throw new Error(`pnpm approve-builds -g --all failed with exit code ${approve.status}`);
126
- }
127
- }
128
-
129
- const run = spawnSync('ruflo', initArgs, {
130
- cwd,
131
- stdio: 'inherit',
132
- shell: process.platform === 'win32'
133
- });
134
-
135
- if (run.status !== 0) {
136
- throw new Error(`ruflo init failed with exit code ${run.status}`);
137
- }
138
- }
139
-
140
- function writeMcpJson({ cwd, dryRun }) {
141
- const mcpPath = path.join(cwd, '.mcp.json');
142
- const mcpConfig = toPlatformMcpConfig(process.platform);
143
-
144
- if (dryRun) {
145
- const action = pathExists(mcpPath) ? 'overwrite' : 'write';
146
- logLine(` [DRY RUN] Would ${action}: ${mcpPath}`);
147
- return;
148
- }
149
-
150
- writeJson(mcpPath, mcpConfig);
151
- logLine(' .mcp.json written for this platform.');
152
- }
153
-
154
- function syncGlobalCommandTemplate({ packageRoot, dryRun }) {
155
- const src = path.join(packageRoot, 'templates', 'ruflo-setup.md');
156
- const dest = path.join(os.homedir(), '.claude', 'commands', 'ruflo-setup.md');
157
- const exists = pathExists(dest);
158
- const operation = exists ? 'update' : 'install';
159
- const srcContent = fs.readFileSync(src, 'utf8');
160
- const changed = !exists || fs.readFileSync(dest, 'utf8') !== srcContent;
161
-
162
- if (dryRun || !changed) {
163
- return { dest, changed, operation };
164
- }
165
-
166
- copyFileSync(src, dest);
167
- return { dest, changed, operation };
168
- }
169
-
170
- function isAlreadyConfigured(cwd) {
171
- return pathExists(path.join(cwd, '.mcp.json')) || pathExists(path.join(cwd, '.claude', 'settings.json'));
172
- }
173
-
174
- function getCurrentVersion(packageRoot) {
175
- try {
176
- const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
177
- return pkg.version || '0.0.0';
178
- } catch {
179
- return '0.0.0';
180
- }
181
- }
182
-
183
- function getLatestVersion() {
184
- try {
185
- const result = spawnSync('pnpm', ['view', '@mfjjs/ruflo-setup', 'version'], {
186
- stdio: ['ignore', 'pipe', 'ignore'],
187
- shell: process.platform === 'win32',
188
- timeout: 8000
189
- });
190
- if (result.status !== 0 || result.error) return null;
191
- return (result.stdout || '').toString().trim() || null;
192
- } catch {
193
- return null;
194
- }
195
- }
196
-
197
- export async function runSetup({
198
- cwd,
199
- packageRoot,
200
- force = false,
201
- dryRun = false,
202
- skipInit = false,
203
- noHooks = false,
204
- yes = false,
205
- verbose = false
206
- }) {
207
- logLine('');
208
- logLine('Ruflo Setup (npm CLI)');
209
- logLine(`Target directory: ${cwd}`);
210
- if (dryRun) {
211
- logLine('[DRY RUN - no changes will be made]');
212
- }
213
- logLine('');
214
-
215
- // Check if a newer version of ruflo-setup itself is available.
216
- if (!dryRun && !yes) {
217
- const currentVersion = getCurrentVersion(packageRoot);
218
- const latestVersion = getLatestVersion();
219
- if (latestVersion && !semverGte(parseSemver(currentVersion), parseSemver(latestVersion))) {
220
- logLine(`A newer version of ruflo-setup is available: ${latestVersion} (you have ${currentVersion}).`);
221
- logLine('It is best to always have the latest version before running setup.');
222
- const doUpdate = await confirm('Update @mfjjs/ruflo-setup now? [y/N] ');
223
- if (doUpdate) {
224
- runUpdate({ dryRun: false });
225
- logLine('');
226
- logLine('Please re-run ruflo-setup to continue with the updated version.');
227
- return;
228
- }
229
- logLine('');
230
- }
231
- }
232
-
233
- logLine('Preflight: Syncing global /ruflo-setup command template ...');
234
- const preflightCommandResult = syncGlobalCommandTemplate({ packageRoot, dryRun });
235
- if (preflightCommandResult.changed) {
236
- if (dryRun) {
237
- logLine(` [DRY RUN] Would ${preflightCommandResult.operation}: ${preflightCommandResult.dest}`);
238
- } else if (preflightCommandResult.operation === 'install') {
239
- logLine(` Installed command template at: ${preflightCommandResult.dest}`);
240
- } else {
241
- logLine(` Updated command template at: ${preflightCommandResult.dest}`);
242
- }
243
- } else {
244
- logLine(` Command template already up to date: ${preflightCommandResult.dest}`);
245
- }
246
- logLine('');
247
-
248
- if (isAlreadyConfigured(cwd) && !force && !yes) {
249
- logLine('WARNING: This project already has Ruflo configuration.');
250
- const shouldOverwrite = await confirm('Overwrite existing configuration? [y/N] ');
251
- if (!shouldOverwrite) {
252
- logLine('Aborted. No changes made.');
253
- return;
254
- }
255
- }
256
-
257
- if (!skipInit) {
258
- logLine('Step 1: Running pnpm add -g ruflo@latest && ruflo init --full ...');
259
- runPnpmInit({ force, cwd, dryRun });
260
- if (!dryRun) {
261
- logLine(' ruflo init completed.');
262
- }
263
- logLine('');
264
- } else {
265
- logLine('Step 1: Skipped ruflo init (--skip-init).');
266
- logLine('');
267
- }
268
-
269
- logLine('Step 2: Writing platform-aware .mcp.json ...');
270
- writeMcpJson({ cwd, dryRun });
271
- logLine('');
272
-
273
- if (!noHooks) {
274
- logLine('Step 3: Installing global SessionStart check-ruflo hook ...');
275
- const hookResult = installGlobalCheckRufloHook({ packageRoot, dryRun });
276
- if (hookResult.inserted) {
277
- logLine(` Hook installed in: ${hookResult.settingsPath}`);
278
- } else {
279
- logLine(` Hook already present in: ${hookResult.settingsPath}`);
280
- }
281
- if (verbose) {
282
- logLine(` Hook command: ${hookResult.hookCommand}`);
283
- }
284
- logLine('');
285
- } else {
286
- logLine('Step 3: Skipped hook installation (--no-hooks).');
287
- logLine('');
288
- }
289
-
290
- logLine('Step 4: Installing global /ruflo-setup command ...');
291
- if (dryRun) {
292
- if (preflightCommandResult.changed) {
293
- logLine(` [DRY RUN] Would ${preflightCommandResult.operation}: ${preflightCommandResult.dest}`);
294
- } else {
295
- logLine(` [DRY RUN] Command already up to date: ${preflightCommandResult.dest}`);
296
- }
297
- } else if (preflightCommandResult.changed) {
298
- if (preflightCommandResult.operation === 'install') {
299
- logLine(` Command installed at: ${preflightCommandResult.dest}`);
300
- } else {
301
- logLine(` Command updated at: ${preflightCommandResult.dest}`);
302
- }
303
- } else {
304
- logLine(` Command already up to date: ${preflightCommandResult.dest}`);
305
- }
306
- logLine('');
307
-
308
- if (dryRun) {
309
- logLine('Dry run complete. No changes were made.');
310
- return;
311
- }
312
-
313
- logLine('Setup complete!');
314
- logLine('');
315
- logLine('Next steps:');
316
- logLine(' 1. Edit CLAUDE.md for project-specific Build & Test commands');
317
- logLine(' 2. Run: claude');
318
- logLine(' 3. Verify hooks: ruflo-setup hooks status');
319
- }
320
-
321
- const CLEANUP_NPM_PACKAGES = [
322
- 'ruflo',
323
- '@mfjjs/ruflo-setup',
324
- 'ruflo-setup',
325
- 'claude-flow',
326
- '@claude-flow/cli',
327
- 'ruv-swarm'
328
- ];
329
-
330
- export function runCleanup({ dryRun = false } = {}) {
331
- logLine('');
332
- logLine('Ruflo Cleanup — removing from npm global registry');
333
- logLine(`Packages: ${CLEANUP_NPM_PACKAGES.join(', ')}`);
334
- logLine('');
335
-
336
- if (dryRun) {
337
- logLine(` [DRY RUN] Would run: npm uninstall -g ${CLEANUP_NPM_PACKAGES.join(' ')}`);
338
- logLine('');
339
- return;
340
- }
341
-
342
- const result = spawnSync('npm', ['uninstall', '-g', ...CLEANUP_NPM_PACKAGES], {
343
- stdio: 'inherit',
344
- shell: process.platform === 'win32'
345
- });
346
-
347
- if (result.status !== 0) {
348
- throw new Error(`npm uninstall -g failed with exit code ${result.status}`);
349
- }
350
-
351
- logLine('');
352
- logLine('Cleanup complete.');
353
- }
354
-
355
- export function runUpdate({ dryRun = false } = {}) {
356
- logLine('');
357
- logLine('Ruflo Setup Update');
358
- logLine('');
359
-
360
- if (dryRun) {
361
- logLine('[DRY RUN] Would run: pnpm add -g @mfjjs/ruflo-setup@latest');
362
- logLine('');
363
- return;
364
- }
365
-
366
- ensurePnpmAvailable();
367
-
368
- logLine('Updating @mfjjs/ruflo-setup to latest...');
369
- const result = spawnSync('pnpm', ['add', '-g', '@mfjjs/ruflo-setup@latest'], {
370
- stdio: 'inherit',
371
- shell: process.platform === 'win32'
372
- });
373
-
374
- if (result.status !== 0) {
375
- throw new Error(`pnpm add -g @mfjjs/ruflo-setup@latest failed with exit code ${result.status}`);
376
- }
377
-
378
- logLine('');
379
- logLine('Update complete. Re-run ruflo-setup to continue with the updated version.');
380
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { pathExists, copyFileSync, confirm, toPlatformMcpConfig, writeJson } from './utils.js';
6
+ import { installGlobalCheckRufloHook } from './hooks.js';
7
+
8
+ function logLine(message) {
9
+ process.stdout.write(`${message}\n`);
10
+ }
11
+
12
+ function getPnpmInstallSuggestions(platform) {
13
+ if (platform === 'win32') {
14
+ return [
15
+ 'winget install -e --id pnpm.pnpm',
16
+ 'corepack enable && corepack prepare pnpm@latest --activate',
17
+ 'npm install -g pnpm'
18
+ ];
19
+ }
20
+
21
+ if (platform === 'darwin') {
22
+ return [
23
+ 'brew install pnpm',
24
+ 'corepack enable && corepack prepare pnpm@latest --activate',
25
+ 'npm install -g pnpm'
26
+ ];
27
+ }
28
+
29
+ return [
30
+ 'curl -fsSL https://get.pnpm.io/install.sh | sh -',
31
+ 'corepack enable && corepack prepare pnpm@latest --activate',
32
+ 'npm install -g pnpm'
33
+ ];
34
+ }
35
+
36
+ const MIN_PNPM_VERSION = '10.32.1';
37
+
38
+ function parseSemver(str) {
39
+ const [major = 0, minor = 0, patch = 0] = str.trim().split('.').map(Number);
40
+ return { major, minor, patch };
41
+ }
42
+
43
+ function semverGte(a, b) {
44
+ if (a.major !== b.major) return a.major > b.major;
45
+ if (a.minor !== b.minor) return a.minor > b.minor;
46
+ return a.patch >= b.patch;
47
+ }
48
+
49
+ function ensurePnpmAvailable() {
50
+ const check = spawnSync('pnpm', ['--version'], {
51
+ stdio: ['ignore', 'pipe', 'ignore'],
52
+ shell: process.platform === 'win32'
53
+ });
54
+
55
+ if (check.status !== 0 || check.error) {
56
+ const platformLabel = process.platform === 'win32'
57
+ ? 'Windows'
58
+ : process.platform === 'darwin'
59
+ ? 'macOS'
60
+ : 'Linux';
61
+ const suggestions = getPnpmInstallSuggestions(process.platform)
62
+ .map((command) => ` - ${command}`)
63
+ .join('\n');
64
+
65
+ throw new Error(
66
+ `pnpm is required but was not found in PATH.\n` +
67
+ `Install pnpm, then re-run ruflo-setup.\n` +
68
+ `Quick install options for ${platformLabel}:\n${suggestions}`
69
+ );
70
+ }
71
+
72
+ const version = (check.stdout || '').toString().trim();
73
+ if (!semverGte(parseSemver(version), parseSemver(MIN_PNPM_VERSION))) {
74
+ throw new Error(
75
+ `pnpm ${MIN_PNPM_VERSION} or higher is required, but found ${version}.\n` +
76
+ `Upgrade with: pnpm self-update`
77
+ );
78
+ }
79
+ }
80
+
81
+ function runPnpmInit({ force, cwd, dryRun }) {
82
+ const initArgs = ['init', '--full'];
83
+ if (force) {
84
+ initArgs.push('--force');
85
+ }
86
+
87
+ if (dryRun) {
88
+ logLine(` [DRY RUN] Would run: pnpm add -g ruflo@latest`);
89
+ logLine(` [DRY RUN] Would run: pnpm approve-builds -g --all (if changes detected)`);
90
+ logLine(` [DRY RUN] Would run: ruflo ${initArgs.join(' ')}`);
91
+ return;
92
+ }
93
+
94
+ ensurePnpmAvailable();
95
+
96
+ // Capture stdout to detect whether pnpm installed/updated anything.
97
+ // Progress spinners go to stderr (still shown to user); stdout has the summary.
98
+ const install = spawnSync('pnpm', ['add', '-g', 'ruflo@latest'], {
99
+ cwd,
100
+ stdio: ['inherit', 'pipe', 'inherit'],
101
+ shell: process.platform === 'win32'
102
+ });
103
+
104
+ const installOutput = (install.stdout || '').toString();
105
+ if (installOutput) {
106
+ process.stdout.write(installOutput);
107
+ }
108
+
109
+ if (install.status !== 0) {
110
+ throw new Error(`pnpm add -g ruflo@latest failed with exit code ${install.status}`);
111
+ }
112
+
113
+ // pnpm prints a "Packages:" summary line and "+ pkg version" lines when
114
+ // something is actually installed or updated. When already up to date the
115
+ // stdout is empty or contains only "Already up to date".
116
+ const somethingChanged = /Packages:/i.test(installOutput) || /^\+\s/m.test(installOutput);
117
+ if (somethingChanged) {
118
+ logLine(' Changes detected — running pnpm approve-builds -g --all ...');
119
+ const approve = spawnSync('pnpm', ['approve-builds', '-g', '--all'], {
120
+ cwd,
121
+ stdio: 'inherit',
122
+ shell: process.platform === 'win32'
123
+ });
124
+ if (approve.status !== 0) {
125
+ throw new Error(`pnpm approve-builds -g --all failed with exit code ${approve.status}`);
126
+ }
127
+ }
128
+
129
+ const run = spawnSync('ruflo', initArgs, {
130
+ cwd,
131
+ stdio: 'inherit',
132
+ shell: process.platform === 'win32'
133
+ });
134
+
135
+ if (run.status !== 0) {
136
+ throw new Error(`ruflo init failed with exit code ${run.status}`);
137
+ }
138
+ }
139
+
140
+ function writeMcpJson({ cwd, dryRun }) {
141
+ const mcpPath = path.join(cwd, '.mcp.json');
142
+ const mcpConfig = toPlatformMcpConfig(process.platform);
143
+
144
+ if (dryRun) {
145
+ const action = pathExists(mcpPath) ? 'overwrite' : 'write';
146
+ logLine(` [DRY RUN] Would ${action}: ${mcpPath}`);
147
+ return;
148
+ }
149
+
150
+ writeJson(mcpPath, mcpConfig);
151
+ logLine(' .mcp.json written for this platform.');
152
+ }
153
+
154
+ function syncGlobalCommandTemplate({ packageRoot, dryRun }) {
155
+ const src = path.join(packageRoot, 'templates', 'ruflo-setup.md');
156
+ const dest = path.join(os.homedir(), '.claude', 'commands', 'ruflo-setup.md');
157
+ const exists = pathExists(dest);
158
+ const operation = exists ? 'update' : 'install';
159
+ const srcContent = fs.readFileSync(src, 'utf8');
160
+ const changed = !exists || fs.readFileSync(dest, 'utf8') !== srcContent;
161
+
162
+ if (dryRun || !changed) {
163
+ return { dest, changed, operation };
164
+ }
165
+
166
+ copyFileSync(src, dest);
167
+ return { dest, changed, operation };
168
+ }
169
+
170
+ function updateGitignore({ cwd, dryRun }) {
171
+ const gitignorePath = path.join(cwd, '.gitignore');
172
+ const entries = ['.mcp.json', '.claude/settings.json'];
173
+
174
+ if (dryRun) {
175
+ logLine(` [DRY RUN] Would ensure ${gitignorePath} contains: ${entries.join(', ')}`);
176
+ return;
177
+ }
178
+
179
+ let content = pathExists(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
180
+ const lines = content.split('\n');
181
+ const added = [];
182
+
183
+ for (const entry of entries) {
184
+ if (!lines.some((line) => line.trim() === entry)) {
185
+ added.push(entry);
186
+ }
187
+ }
188
+
189
+ if (added.length === 0) {
190
+ logLine(` .gitignore already contains required entries.`);
191
+ return;
192
+ }
193
+
194
+ const suffix = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
195
+ content = content + suffix + added.join('\n') + '\n';
196
+ fs.writeFileSync(gitignorePath, content, 'utf8');
197
+ logLine(` Added to .gitignore: ${added.join(', ')}`);
198
+ }
199
+
200
+ function isAlreadyConfigured(cwd) {
201
+ return pathExists(path.join(cwd, '.mcp.json')) || pathExists(path.join(cwd, '.claude', 'settings.json'));
202
+ }
203
+
204
+ function getCurrentVersion(packageRoot) {
205
+ try {
206
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
207
+ return pkg.version || '0.0.0';
208
+ } catch {
209
+ return '0.0.0';
210
+ }
211
+ }
212
+
213
+ function getLatestVersion() {
214
+ try {
215
+ const result = spawnSync('pnpm', ['view', '@mfjjs/ruflo-setup', 'version'], {
216
+ stdio: ['ignore', 'pipe', 'ignore'],
217
+ shell: process.platform === 'win32',
218
+ timeout: 8000
219
+ });
220
+ if (result.status !== 0 || result.error) return null;
221
+ return (result.stdout || '').toString().trim() || null;
222
+ } catch {
223
+ return null;
224
+ }
225
+ }
226
+
227
+ export async function runSetup({
228
+ cwd,
229
+ packageRoot,
230
+ force = false,
231
+ dryRun = false,
232
+ skipInit = false,
233
+ noHooks = false,
234
+ yes = false,
235
+ verbose = false
236
+ }) {
237
+ logLine('');
238
+ logLine('Ruflo Setup (npm CLI)');
239
+ logLine(`Target directory: ${cwd}`);
240
+ if (dryRun) {
241
+ logLine('[DRY RUN - no changes will be made]');
242
+ }
243
+ logLine('');
244
+
245
+ // Check if a newer version of ruflo-setup itself is available.
246
+ if (!dryRun && !yes) {
247
+ const currentVersion = getCurrentVersion(packageRoot);
248
+ const latestVersion = getLatestVersion();
249
+ if (latestVersion && !semverGte(parseSemver(currentVersion), parseSemver(latestVersion))) {
250
+ logLine(`A newer version of ruflo-setup is available: ${latestVersion} (you have ${currentVersion}).`);
251
+ logLine('It is best to always have the latest version before running setup.');
252
+ const doUpdate = await confirm('Update @mfjjs/ruflo-setup now? [y/N] ');
253
+ if (doUpdate) {
254
+ runUpdate({ dryRun: false });
255
+ logLine('');
256
+ logLine('Please re-run ruflo-setup to continue with the updated version.');
257
+ return;
258
+ }
259
+ logLine('');
260
+ }
261
+ }
262
+
263
+ logLine('Preflight: Syncing global /ruflo-setup command template ...');
264
+ const preflightCommandResult = syncGlobalCommandTemplate({ packageRoot, dryRun });
265
+ if (preflightCommandResult.changed) {
266
+ if (dryRun) {
267
+ logLine(` [DRY RUN] Would ${preflightCommandResult.operation}: ${preflightCommandResult.dest}`);
268
+ } else if (preflightCommandResult.operation === 'install') {
269
+ logLine(` Installed command template at: ${preflightCommandResult.dest}`);
270
+ } else {
271
+ logLine(` Updated command template at: ${preflightCommandResult.dest}`);
272
+ }
273
+ } else {
274
+ logLine(` Command template already up to date: ${preflightCommandResult.dest}`);
275
+ }
276
+ logLine('');
277
+
278
+ if (isAlreadyConfigured(cwd) && !force && !yes) {
279
+ logLine('WARNING: This project already has Ruflo configuration.');
280
+ const shouldOverwrite = await confirm('Overwrite existing configuration? [y/N] ');
281
+ if (!shouldOverwrite) {
282
+ logLine('Aborted. No changes made.');
283
+ return;
284
+ }
285
+ }
286
+
287
+ if (!skipInit) {
288
+ logLine('Step 1: Running pnpm add -g ruflo@latest && ruflo init --full ...');
289
+ runPnpmInit({ force, cwd, dryRun });
290
+ if (!dryRun) {
291
+ logLine(' ruflo init completed.');
292
+ }
293
+ logLine('');
294
+ } else {
295
+ logLine('Step 1: Skipped ruflo init (--skip-init).');
296
+ logLine('');
297
+ }
298
+
299
+ logLine('Step 2: Writing platform-aware .mcp.json ...');
300
+ writeMcpJson({ cwd, dryRun });
301
+ logLine('');
302
+
303
+ logLine('Step 3: Updating .gitignore ...');
304
+ updateGitignore({ cwd, dryRun });
305
+ logLine('');
306
+
307
+ if (!noHooks) {
308
+ logLine('Step 4: Installing global SessionStart check-ruflo hook ...');
309
+ const hookResult = installGlobalCheckRufloHook({ packageRoot, dryRun });
310
+ if (hookResult.inserted) {
311
+ logLine(` Hook installed in: ${hookResult.settingsPath}`);
312
+ } else {
313
+ logLine(` Hook already present in: ${hookResult.settingsPath}`);
314
+ }
315
+ if (verbose) {
316
+ logLine(` Hook command: ${hookResult.hookCommand}`);
317
+ }
318
+ logLine('');
319
+ } else {
320
+ logLine('Step 4: Skipped hook installation (--no-hooks).');
321
+ logLine('');
322
+ }
323
+
324
+ logLine('Step 5: Installing global /ruflo-setup command ...');
325
+ if (dryRun) {
326
+ if (preflightCommandResult.changed) {
327
+ logLine(` [DRY RUN] Would ${preflightCommandResult.operation}: ${preflightCommandResult.dest}`);
328
+ } else {
329
+ logLine(` [DRY RUN] Command already up to date: ${preflightCommandResult.dest}`);
330
+ }
331
+ } else if (preflightCommandResult.changed) {
332
+ if (preflightCommandResult.operation === 'install') {
333
+ logLine(` Command installed at: ${preflightCommandResult.dest}`);
334
+ } else {
335
+ logLine(` Command updated at: ${preflightCommandResult.dest}`);
336
+ }
337
+ } else {
338
+ logLine(` Command already up to date: ${preflightCommandResult.dest}`);
339
+ }
340
+ logLine('');
341
+
342
+ if (dryRun) {
343
+ logLine('Dry run complete. No changes were made.');
344
+ return;
345
+ }
346
+
347
+ logLine('Setup complete!');
348
+ logLine('');
349
+ logLine('Next steps:');
350
+ logLine(' 1. Edit CLAUDE.md for project-specific Build & Test commands');
351
+ logLine(' 2. Run: claude');
352
+ logLine(' 3. Verify hooks: ruflo-setup hooks status');
353
+ }
354
+
355
+ const CLEANUP_NPM_PACKAGES = [
356
+ 'ruflo',
357
+ '@mfjjs/ruflo-setup',
358
+ 'ruflo-setup',
359
+ 'claude-flow',
360
+ '@claude-flow/cli',
361
+ 'ruv-swarm'
362
+ ];
363
+
364
+ export function runCleanup({ dryRun = false } = {}) {
365
+ logLine('');
366
+ logLine('Ruflo Cleanup — removing from npm global registry');
367
+ logLine(`Packages: ${CLEANUP_NPM_PACKAGES.join(', ')}`);
368
+ logLine('');
369
+
370
+ if (dryRun) {
371
+ logLine(` [DRY RUN] Would run: npm uninstall -g ${CLEANUP_NPM_PACKAGES.join(' ')}`);
372
+ logLine('');
373
+ return;
374
+ }
375
+
376
+ const result = spawnSync('npm', ['uninstall', '-g', ...CLEANUP_NPM_PACKAGES], {
377
+ stdio: 'inherit',
378
+ shell: process.platform === 'win32'
379
+ });
380
+
381
+ if (result.status !== 0) {
382
+ throw new Error(`npm uninstall -g failed with exit code ${result.status}`);
383
+ }
384
+
385
+ logLine('');
386
+ logLine('Cleanup complete.');
387
+ }
388
+
389
+ export function runUpdate({ dryRun = false } = {}) {
390
+ logLine('');
391
+ logLine('Ruflo Setup Update');
392
+ logLine('');
393
+
394
+ if (dryRun) {
395
+ logLine('[DRY RUN] Would run: pnpm add -g @mfjjs/ruflo-setup@latest');
396
+ logLine('');
397
+ return;
398
+ }
399
+
400
+ ensurePnpmAvailable();
401
+
402
+ logLine('Updating @mfjjs/ruflo-setup to latest...');
403
+ const result = spawnSync('pnpm', ['add', '-g', '@mfjjs/ruflo-setup@latest'], {
404
+ stdio: 'inherit',
405
+ shell: process.platform === 'win32'
406
+ });
407
+
408
+ if (result.status !== 0) {
409
+ throw new Error(`pnpm add -g @mfjjs/ruflo-setup@latest failed with exit code ${result.status}`);
410
+ }
411
+
412
+ logLine('');
413
+ logLine('Update complete. Re-run ruflo-setup to continue with the updated version.');
414
+ }