wiggum-cli 0.6.0 → 0.7.2

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.
Files changed (60) hide show
  1. package/README.md +49 -28
  2. package/dist/commands/init.d.ts +18 -0
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +68 -92
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/new.js +1 -1
  7. package/dist/commands/new.js.map +1 -1
  8. package/dist/commands/run.js +1 -1
  9. package/dist/commands/run.js.map +1 -1
  10. package/dist/generator/config.d.ts +3 -3
  11. package/dist/generator/config.js +3 -3
  12. package/dist/generator/index.js +1 -1
  13. package/dist/generator/index.js.map +1 -1
  14. package/dist/generator/writer.js +1 -1
  15. package/dist/generator/writer.js.map +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +37 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/repl/command-parser.d.ts +5 -0
  21. package/dist/repl/command-parser.d.ts.map +1 -1
  22. package/dist/repl/command-parser.js +5 -0
  23. package/dist/repl/command-parser.js.map +1 -1
  24. package/dist/repl/repl-loop.d.ts.map +1 -1
  25. package/dist/repl/repl-loop.js +64 -3
  26. package/dist/repl/repl-loop.js.map +1 -1
  27. package/dist/repl/session-state.d.ts +4 -2
  28. package/dist/repl/session-state.d.ts.map +1 -1
  29. package/dist/repl/session-state.js +2 -1
  30. package/dist/repl/session-state.js.map +1 -1
  31. package/dist/templates/root/README.md.tmpl +1 -1
  32. package/dist/templates/scripts/feature-loop.sh.tmpl +17 -17
  33. package/dist/templates/scripts/loop.sh.tmpl +7 -7
  34. package/dist/templates/scripts/ralph-monitor.sh.tmpl +5 -5
  35. package/dist/utils/config.d.ts +7 -7
  36. package/dist/utils/config.js +4 -4
  37. package/dist/utils/config.js.map +1 -1
  38. package/dist/utils/update-check.d.ts +21 -0
  39. package/dist/utils/update-check.d.ts.map +1 -0
  40. package/dist/utils/update-check.js +149 -0
  41. package/dist/utils/update-check.js.map +1 -0
  42. package/package.json +1 -1
  43. package/src/commands/init.ts +92 -108
  44. package/src/commands/new.ts +1 -1
  45. package/src/commands/run.ts +1 -1
  46. package/src/generator/config.ts +3 -3
  47. package/src/generator/index.ts +1 -1
  48. package/src/generator/writer.ts +1 -1
  49. package/src/index.ts +50 -0
  50. package/src/repl/command-parser.ts +5 -0
  51. package/src/repl/repl-loop.ts +73 -3
  52. package/src/repl/session-state.ts +7 -3
  53. package/src/templates/config/ralph.config.cjs.tmpl +38 -0
  54. package/src/templates/root/README.md.tmpl +1 -1
  55. package/src/templates/scripts/feature-loop.sh.tmpl +17 -17
  56. package/src/templates/scripts/loop.sh.tmpl +7 -7
  57. package/src/templates/scripts/ralph-monitor.sh.tmpl +5 -5
  58. package/src/utils/config.ts +9 -9
  59. package/src/utils/update-check.ts +182 -0
  60. /package/{src/templates/config/ralph.config.js.tmpl → dist/templates/config/ralph.config.cjs.tmpl} +0 -0
@@ -9,6 +9,8 @@ import { logger } from '../utils/logger.js';
9
9
  import { simpson } from '../utils/colors.js';
10
10
  import { runCommand } from '../commands/run.js';
11
11
  import { monitorCommand } from '../commands/monitor.js';
12
+ import { runInitWorkflow } from '../commands/init.js';
13
+ import { hasConfig } from '../utils/config.js';
12
14
  import type { SessionState } from './session-state.js';
13
15
  import { updateSessionState } from './session-state.js';
14
16
  import {
@@ -21,13 +23,64 @@ import {
21
23
  const PROMPT = `${simpson.yellow('wiggum')}${simpson.brown('>')} `;
22
24
 
23
25
  /**
24
- * Handler for the /new command - will be enhanced in Phase 3
26
+ * Handler for the /init command
27
+ */
28
+ async function handleInitCommand(
29
+ _args: string[],
30
+ state: SessionState,
31
+ rl: readline.Interface
32
+ ): Promise<SessionState> {
33
+ // Check if already initialized
34
+ if (state.initialized && hasConfig(state.projectRoot)) {
35
+ logger.warn('Project is already initialized. Re-running init will update configuration.');
36
+ console.log('');
37
+ }
38
+
39
+ // Pause REPL readline to avoid conflicts with subcommand's stdin usage
40
+ rl.pause();
41
+
42
+ try {
43
+ const result = await runInitWorkflow(state.projectRoot, {
44
+ yes: false, // Always interactive in REPL
45
+ });
46
+
47
+ if (result) {
48
+ // Update state with init result
49
+ return updateSessionState(state, {
50
+ provider: result.provider,
51
+ model: result.model,
52
+ scanResult: result.scanResult,
53
+ config: result.config,
54
+ initialized: true,
55
+ });
56
+ }
57
+
58
+ // User cancelled
59
+ return state;
60
+ } catch (error) {
61
+ logger.error(`Init failed: ${error instanceof Error ? error.message : String(error)}`);
62
+ return state;
63
+ } finally {
64
+ // Resume REPL readline after subcommand completes
65
+ rl.resume();
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Handler for the /new command
71
+ * Always uses AI interview mode in REPL (falls back to template if no API key)
25
72
  */
26
73
  async function handleNewCommand(
27
74
  args: string[],
28
75
  state: SessionState,
29
76
  rl: readline.Interface
30
77
  ): Promise<SessionState> {
78
+ // Check if initialized
79
+ if (!state.initialized && !hasConfig(state.projectRoot)) {
80
+ logger.warn('Project not initialized. Run /init first.');
81
+ return state;
82
+ }
83
+
31
84
  if (args.length === 0) {
32
85
  logger.error('Feature name required. Usage: /new <feature-name>');
33
86
  return state;
@@ -40,12 +93,14 @@ async function handleNewCommand(
40
93
 
41
94
  try {
42
95
  // Delegate to the existing new command behavior
96
+ // Always use AI mode in REPL (the command handles fallback to template if no API key)
43
97
  const { newCommand } = await import('../commands/new.js');
44
98
  await newCommand(featureName, {
45
99
  yes: false,
46
100
  scanResult: state.scanResult,
47
- provider: state.provider,
101
+ provider: state.provider ?? undefined,
48
102
  model: state.model,
103
+ ai: true, // Always use AI interview in REPL
49
104
  });
50
105
  } finally {
51
106
  // Resume REPL readline after subcommand completes
@@ -63,6 +118,12 @@ async function handleRunCommand(
63
118
  state: SessionState,
64
119
  rl: readline.Interface
65
120
  ): Promise<SessionState> {
121
+ // Check if initialized
122
+ if (!state.initialized && !hasConfig(state.projectRoot)) {
123
+ logger.warn('Project not initialized. Run /init first.');
124
+ return state;
125
+ }
126
+
66
127
  if (args.length === 0) {
67
128
  logger.error('Feature name required. Usage: /run <feature-name>');
68
129
  return state;
@@ -128,6 +189,9 @@ async function executeCommand(
128
189
  rl: readline.Interface
129
190
  ): Promise<{ state: SessionState; shouldExit: boolean }> {
130
191
  switch (commandName) {
192
+ case 'init':
193
+ return { state: await handleInitCommand(args, state, rl), shouldExit: false };
194
+
131
195
  case 'new':
132
196
  return { state: await handleNewCommand(args, state, rl), shouldExit: false };
133
197
 
@@ -218,7 +282,13 @@ export async function startRepl(initialState: SessionState): Promise<void> {
218
282
 
219
283
  console.log('');
220
284
  console.log(simpson.yellow('Wiggum Interactive Mode'));
221
- console.log(pc.dim('Type /help for commands, /exit to quit'));
285
+
286
+ // Show context-aware welcome message
287
+ if (!state.initialized && !hasConfig(state.projectRoot)) {
288
+ console.log(pc.dim('Not initialized. Run /init to set up this project.'));
289
+ } else {
290
+ console.log(pc.dim('Type /help for commands, /exit to quit'));
291
+ }
222
292
  console.log('');
223
293
 
224
294
  const rl = readline.createInterface({
@@ -16,7 +16,7 @@ export interface SessionState {
16
16
  /** Loaded Ralph configuration */
17
17
  config: RalphConfig | null;
18
18
  /** AI provider being used */
19
- provider: AIProvider;
19
+ provider: AIProvider | null;
20
20
  /** Model to use for AI operations */
21
21
  model: string;
22
22
  /** Cached scan result from init */
@@ -25,6 +25,8 @@ export interface SessionState {
25
25
  conversationMode: boolean;
26
26
  /** Current conversation context (e.g., 'spec-generation') */
27
27
  conversationContext?: string;
28
+ /** Whether /init has been run in this session */
29
+ initialized: boolean;
28
30
  }
29
31
 
30
32
  /**
@@ -32,10 +34,11 @@ export interface SessionState {
32
34
  */
33
35
  export function createSessionState(
34
36
  projectRoot: string,
35
- provider: AIProvider,
37
+ provider: AIProvider | null,
36
38
  model: string,
37
39
  scanResult?: ScanResult,
38
- config?: RalphConfig | null
40
+ config?: RalphConfig | null,
41
+ initialized?: boolean
39
42
  ): SessionState {
40
43
  return {
41
44
  projectRoot,
@@ -45,6 +48,7 @@ export function createSessionState(
45
48
  scanResult,
46
49
  conversationMode: false,
47
50
  conversationContext: undefined,
51
+ initialized: initialized ?? false,
48
52
  };
49
53
  }
50
54
 
@@ -0,0 +1,38 @@
1
+ module.exports = {
2
+ name: '{{projectName}}',
3
+ stack: {
4
+ framework: {
5
+ name: '{{framework}}',
6
+ version: '{{frameworkVersion}}',
7
+ variant: '{{frameworkVariant}}',
8
+ },
9
+ packageManager: '{{packageManager}}',
10
+ testing: {
11
+ unit: '{{unitTest}}',
12
+ e2e: '{{e2eTest}}',
13
+ },
14
+ styling: '{{styling}}',
15
+ },
16
+ commands: {
17
+ dev: '{{devCommand}}',
18
+ build: '{{buildCommand}}',
19
+ test: '{{testCommand}}',
20
+ lint: '{{lintCommand}}',
21
+ typecheck: '{{typecheckCommand}}',
22
+ },
23
+ paths: {
24
+ root: '.ralph',
25
+ prompts: '.ralph/prompts',
26
+ guides: '.ralph/guides',
27
+ specs: '.ralph/specs',
28
+ scripts: '.ralph/scripts',
29
+ learnings: '.ralph/LEARNINGS.md',
30
+ agents: '.ralph/AGENTS.md',
31
+ },
32
+ loop: {
33
+ maxIterations: 10,
34
+ maxE2eAttempts: 5,
35
+ defaultModel: 'sonnet',
36
+ planningModel: 'opus',
37
+ },
38
+ };
@@ -58,4 +58,4 @@ Add new feature specs to `.ralph/specs/`:
58
58
 
59
59
  ## Configuration
60
60
 
61
- See `ralph.config.js` in project root for full configuration.
61
+ See `ralph.config.cjs` in project root for full configuration.
@@ -14,23 +14,23 @@ set -o pipefail
14
14
  # Get script directory
15
15
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
16
 
17
- # Load config from ralph.config.js if available
18
- if [ -f "$SCRIPT_DIR/../ralph.config.js" ]; then
19
- RALPH_ROOT=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.js').paths?.root || '.ralph')" 2>/dev/null || echo ".ralph")
20
- SPEC_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.js').paths?.specs || '.ralph/specs')" 2>/dev/null || echo ".ralph/specs")
21
- PROMPTS_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.js').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
22
- DEFAULT_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.js').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
23
- PLANNING_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.js').loop?.planningModel || 'opus')" 2>/dev/null || echo "opus")
24
- DEFAULT_MAX_ITERATIONS=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.js').loop?.maxIterations || 10)" 2>/dev/null || echo "10")
25
- DEFAULT_MAX_E2E=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.js').loop?.maxE2eAttempts || 5)" 2>/dev/null || echo "5")
26
- elif [ -f "$SCRIPT_DIR/../../ralph.config.js" ]; then
27
- RALPH_ROOT=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.js').paths?.root || '.ralph')" 2>/dev/null || echo ".ralph")
28
- SPEC_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.js').paths?.specs || '.ralph/specs')" 2>/dev/null || echo ".ralph/specs")
29
- PROMPTS_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.js').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
30
- DEFAULT_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.js').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
31
- PLANNING_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.js').loop?.planningModel || 'opus')" 2>/dev/null || echo "opus")
32
- DEFAULT_MAX_ITERATIONS=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.js').loop?.maxIterations || 10)" 2>/dev/null || echo "10")
33
- DEFAULT_MAX_E2E=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.js').loop?.maxE2eAttempts || 5)" 2>/dev/null || echo "5")
17
+ # Load config from ralph.config.cjs if available
18
+ if [ -f "$SCRIPT_DIR/../ralph.config.cjs" ]; then
19
+ RALPH_ROOT=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').paths?.root || '.ralph')" 2>/dev/null || echo ".ralph")
20
+ SPEC_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').paths?.specs || '.ralph/specs')" 2>/dev/null || echo ".ralph/specs")
21
+ PROMPTS_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
22
+ DEFAULT_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
23
+ PLANNING_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').loop?.planningModel || 'opus')" 2>/dev/null || echo "opus")
24
+ DEFAULT_MAX_ITERATIONS=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').loop?.maxIterations || 10)" 2>/dev/null || echo "10")
25
+ DEFAULT_MAX_E2E=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').loop?.maxE2eAttempts || 5)" 2>/dev/null || echo "5")
26
+ elif [ -f "$SCRIPT_DIR/../../ralph.config.cjs" ]; then
27
+ RALPH_ROOT=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').paths?.root || '.ralph')" 2>/dev/null || echo ".ralph")
28
+ SPEC_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').paths?.specs || '.ralph/specs')" 2>/dev/null || echo ".ralph/specs")
29
+ PROMPTS_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
30
+ DEFAULT_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
31
+ PLANNING_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').loop?.planningModel || 'opus')" 2>/dev/null || echo "opus")
32
+ DEFAULT_MAX_ITERATIONS=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').loop?.maxIterations || 10)" 2>/dev/null || echo "10")
33
+ DEFAULT_MAX_E2E=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').loop?.maxE2eAttempts || 5)" 2>/dev/null || echo "5")
34
34
  else
35
35
  # Default paths
36
36
  RALPH_ROOT=".ralph"
@@ -8,13 +8,13 @@ set -e
8
8
  # Get script directory
9
9
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
10
 
11
- # Load config from ralph.config.js if available
12
- if [ -f "$SCRIPT_DIR/../ralph.config.js" ]; then
13
- PROMPTS_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.js').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
14
- DEFAULT_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.js').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
15
- elif [ -f "$SCRIPT_DIR/../../ralph.config.js" ]; then
16
- PROMPTS_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.js').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
17
- DEFAULT_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.js').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
11
+ # Load config from ralph.config.cjs if available
12
+ if [ -f "$SCRIPT_DIR/../ralph.config.cjs" ]; then
13
+ PROMPTS_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
14
+ DEFAULT_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
15
+ elif [ -f "$SCRIPT_DIR/../../ralph.config.cjs" ]; then
16
+ PROMPTS_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
17
+ DEFAULT_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
18
18
  else
19
19
  PROMPTS_DIR=".ralph/prompts"
20
20
  DEFAULT_MODEL="sonnet"
@@ -6,11 +6,11 @@
6
6
  FEATURE="${1:?Usage: ./ralph-monitor.sh <feature-name>}"
7
7
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
8
 
9
- # Load config from ralph.config.js if available
10
- if [ -f "$SCRIPT_DIR/../ralph.config.js" ]; then
11
- SPEC_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.js').paths?.specs || '.ralph/specs')" 2>/dev/null || echo ".ralph/specs")
12
- elif [ -f "$SCRIPT_DIR/../../ralph.config.js" ]; then
13
- SPEC_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.js').paths?.specs || '.ralph/specs')" 2>/dev/null || echo ".ralph/specs")
9
+ # Load config from ralph.config.cjs if available
10
+ if [ -f "$SCRIPT_DIR/../ralph.config.cjs" ]; then
11
+ SPEC_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').paths?.specs || '.ralph/specs')" 2>/dev/null || echo ".ralph/specs")
12
+ elif [ -f "$SCRIPT_DIR/../../ralph.config.cjs" ]; then
13
+ SPEC_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').paths?.specs || '.ralph/specs')" 2>/dev/null || echo ".ralph/specs")
14
14
  else
15
15
  SPEC_DIR=".ralph/specs"
16
16
  fi
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Configuration Utilities
3
- * Load and parse ralph.config.js files
3
+ * Load and parse ralph.config.cjs files
4
4
  */
5
5
 
6
6
  import { existsSync } from 'node:fs';
@@ -9,7 +9,7 @@ import { pathToFileURL } from 'node:url';
9
9
  import { logger } from './logger.js';
10
10
 
11
11
  /**
12
- * Stack configuration in ralph.config.js
12
+ * Stack configuration in ralph.config.cjs
13
13
  */
14
14
  export interface StackConfig {
15
15
  framework: {
@@ -26,7 +26,7 @@ export interface StackConfig {
26
26
  }
27
27
 
28
28
  /**
29
- * Commands configuration in ralph.config.js
29
+ * Commands configuration in ralph.config.cjs
30
30
  */
31
31
  export interface CommandsConfig {
32
32
  dev: string;
@@ -37,7 +37,7 @@ export interface CommandsConfig {
37
37
  }
38
38
 
39
39
  /**
40
- * Paths configuration in ralph.config.js
40
+ * Paths configuration in ralph.config.cjs
41
41
  */
42
42
  export interface PathsConfig {
43
43
  root: string;
@@ -50,7 +50,7 @@ export interface PathsConfig {
50
50
  }
51
51
 
52
52
  /**
53
- * Loop configuration in ralph.config.js
53
+ * Loop configuration in ralph.config.cjs
54
54
  */
55
55
  export interface LoopConfig {
56
56
  maxIterations: number;
@@ -60,7 +60,7 @@ export interface LoopConfig {
60
60
  }
61
61
 
62
62
  /**
63
- * Full ralph.config.js structure
63
+ * Full ralph.config.cjs structure
64
64
  */
65
65
  export interface RalphConfig {
66
66
  name: string;
@@ -113,11 +113,11 @@ export const DEFAULT_CONFIG: RalphConfig = {
113
113
  };
114
114
 
115
115
  /**
116
- * Load ralph.config.js from a project directory
116
+ * Load ralph.config.cjs from a project directory
117
117
  * Returns null if config file doesn't exist
118
118
  */
119
119
  export async function loadConfig(projectRoot: string): Promise<RalphConfig | null> {
120
- const configPath = join(projectRoot, 'ralph.config.js');
120
+ const configPath = join(projectRoot, 'ralph.config.cjs');
121
121
 
122
122
  if (!existsSync(configPath)) {
123
123
  return null;
@@ -179,7 +179,7 @@ export async function loadConfigWithDefaults(projectRoot: string): Promise<Ralph
179
179
  * Check if a ralph config exists in the project
180
180
  */
181
181
  export function hasConfig(projectRoot: string): boolean {
182
- const configPath = join(projectRoot, 'ralph.config.js');
182
+ const configPath = join(projectRoot, 'ralph.config.cjs');
183
183
  return existsSync(configPath);
184
184
  }
185
185
 
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Update Check Utility
3
+ * Checks npm registry for newer versions and notifies user
4
+ */
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import { fileURLToPath } from 'node:url';
10
+ import pc from 'picocolors';
11
+
12
+ const PACKAGE_NAME = 'wiggum-cli';
13
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
14
+ const CACHE_FILE = path.join(os.homedir(), '.wiggum-update-check.json');
15
+
16
+ interface UpdateCheckCache {
17
+ lastCheck: number;
18
+ latestVersion: string | null;
19
+ }
20
+
21
+ interface UpdateInfo {
22
+ currentVersion: string;
23
+ latestVersion: string;
24
+ updateAvailable: boolean;
25
+ }
26
+
27
+ /**
28
+ * Get current package version
29
+ */
30
+ function getCurrentVersion(): string {
31
+ try {
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = path.dirname(__filename);
34
+ // Go up from utils/ to package root
35
+ const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
36
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
37
+ return packageJson.version;
38
+ } catch {
39
+ return '0.0.0';
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Read cache file
45
+ */
46
+ function readCache(): UpdateCheckCache | null {
47
+ try {
48
+ if (fs.existsSync(CACHE_FILE)) {
49
+ return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
50
+ }
51
+ } catch {
52
+ // Ignore cache read errors
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Write cache file
59
+ */
60
+ function writeCache(cache: UpdateCheckCache): void {
61
+ try {
62
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(cache), 'utf-8');
63
+ } catch {
64
+ // Ignore cache write errors
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Fetch latest version from npm registry
70
+ */
71
+ async function fetchLatestVersion(): Promise<string | null> {
72
+ try {
73
+ const controller = new AbortController();
74
+ const timeout = setTimeout(() => controller.abort(), 3000); // 3s timeout
75
+
76
+ const response = await fetch(
77
+ `https://registry.npmjs.org/${PACKAGE_NAME}/latest`,
78
+ { signal: controller.signal }
79
+ );
80
+
81
+ clearTimeout(timeout);
82
+
83
+ if (!response.ok) {
84
+ return null;
85
+ }
86
+
87
+ const data = await response.json();
88
+ return data.version || null;
89
+ } catch {
90
+ // Network error or timeout - fail silently
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Compare semantic versions
97
+ * Returns true if v2 is newer than v1
98
+ */
99
+ function isNewer(v1: string, v2: string): boolean {
100
+ const parts1 = v1.split('.').map(Number);
101
+ const parts2 = v2.split('.').map(Number);
102
+
103
+ for (let i = 0; i < 3; i++) {
104
+ const p1 = parts1[i] || 0;
105
+ const p2 = parts2[i] || 0;
106
+ if (p2 > p1) return true;
107
+ if (p2 < p1) return false;
108
+ }
109
+ return false;
110
+ }
111
+
112
+ /**
113
+ * Check for updates (with caching)
114
+ * Returns update info if check was performed, null if skipped/failed
115
+ */
116
+ export async function checkForUpdates(): Promise<UpdateInfo | null> {
117
+ const currentVersion = getCurrentVersion();
118
+ const cache = readCache();
119
+ const now = Date.now();
120
+
121
+ // Use cached result if recent enough
122
+ if (cache && (now - cache.lastCheck) < CHECK_INTERVAL_MS && cache.latestVersion) {
123
+ return {
124
+ currentVersion,
125
+ latestVersion: cache.latestVersion,
126
+ updateAvailable: isNewer(currentVersion, cache.latestVersion),
127
+ };
128
+ }
129
+
130
+ // Fetch latest version
131
+ const latestVersion = await fetchLatestVersion();
132
+
133
+ if (latestVersion) {
134
+ writeCache({ lastCheck: now, latestVersion });
135
+ return {
136
+ currentVersion,
137
+ latestVersion,
138
+ updateAvailable: isNewer(currentVersion, latestVersion),
139
+ };
140
+ }
141
+
142
+ return null;
143
+ }
144
+
145
+ /**
146
+ * Display update notification if available
147
+ * Call this at startup - it's non-blocking and fails silently
148
+ */
149
+ export async function notifyIfUpdateAvailable(): Promise<void> {
150
+ try {
151
+ const info = await checkForUpdates();
152
+
153
+ if (info?.updateAvailable) {
154
+ console.log('');
155
+ console.log(
156
+ pc.yellow('╭─────────────────────────────────────────────────────────╮')
157
+ );
158
+ console.log(
159
+ pc.yellow('│') +
160
+ pc.bold(' Update available: ') +
161
+ pc.dim(info.currentVersion) +
162
+ pc.bold(' → ') +
163
+ pc.green(info.latestVersion) +
164
+ ' ' +
165
+ pc.yellow('│')
166
+ );
167
+ console.log(
168
+ pc.yellow('│') +
169
+ ' Run ' +
170
+ pc.cyan('npm i -g wiggum-cli') +
171
+ ' to update ' +
172
+ pc.yellow('│')
173
+ );
174
+ console.log(
175
+ pc.yellow('╰─────────────────────────────────────────────────────────╯')
176
+ );
177
+ console.log('');
178
+ }
179
+ } catch {
180
+ // Fail silently - don't interrupt user workflow
181
+ }
182
+ }