@agile-vibe-coding/avc 0.1.0

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,218 @@
1
+ import { execSync } from 'child_process';
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import os from 'os';
6
+ import { updateLogger } from './logger.js';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ /**
12
+ * UpdateChecker - Checks npm registry for new versions
13
+ * Runs in background and updates state file
14
+ */
15
+ export class UpdateChecker {
16
+ constructor() {
17
+ // Use user's home directory for state (works across installations)
18
+ this.stateDir = path.join(os.homedir(), '.avc');
19
+ this.stateFile = path.join(this.stateDir, 'update-state.json');
20
+ this.settingsFile = path.join(this.stateDir, 'settings.json');
21
+ this.packageName = '@agile-vibe-coding/avc';
22
+ this.defaultCheckInterval = 60 * 60 * 1000; // 1 hour default
23
+ }
24
+
25
+ // Initialize state directory
26
+ initStateDir() {
27
+ if (!existsSync(this.stateDir)) {
28
+ mkdirSync(this.stateDir, { recursive: true });
29
+ }
30
+ }
31
+
32
+ // Read settings
33
+ readSettings() {
34
+ if (!existsSync(this.settingsFile)) {
35
+ return {
36
+ autoUpdate: {
37
+ enabled: true,
38
+ checkInterval: this.defaultCheckInterval,
39
+ silent: true,
40
+ notifyUser: true
41
+ }
42
+ };
43
+ }
44
+
45
+ try {
46
+ return JSON.parse(readFileSync(this.settingsFile, 'utf8'));
47
+ } catch (error) {
48
+ return {
49
+ autoUpdate: {
50
+ enabled: true,
51
+ checkInterval: this.defaultCheckInterval,
52
+ silent: true,
53
+ notifyUser: true
54
+ }
55
+ };
56
+ }
57
+ }
58
+
59
+ // Write settings
60
+ writeSettings(settings) {
61
+ this.initStateDir();
62
+ writeFileSync(this.settingsFile, JSON.stringify(settings, null, 2));
63
+ }
64
+
65
+ // Get current version from package.json
66
+ getCurrentVersion() {
67
+ try {
68
+ const packageJsonPath = path.join(__dirname, '..', 'package.json');
69
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
70
+ updateLogger.debug(`Current version: ${packageJson.version}`);
71
+ return packageJson.version;
72
+ } catch (error) {
73
+ updateLogger.error('Failed to read current version', error);
74
+ console.error('Failed to read current version:', error.message);
75
+ return null;
76
+ }
77
+ }
78
+
79
+ // Check npm registry for latest version
80
+ async getLatestVersion() {
81
+ try {
82
+ updateLogger.debug('Checking npm registry for latest version');
83
+ const result = execSync(`npm view ${this.packageName} version`, {
84
+ encoding: 'utf8',
85
+ timeout: 10000,
86
+ stdio: ['pipe', 'pipe', 'ignore'] // Suppress stderr
87
+ });
88
+ const version = result.trim();
89
+ updateLogger.debug(`Latest version on npm: ${version}`);
90
+ return version;
91
+ } catch (error) {
92
+ updateLogger.error('Failed to check npm registry', error);
93
+ // Silently fail - will retry on next check
94
+ return null;
95
+ }
96
+ }
97
+
98
+ // Compare versions (returns true if remote is newer)
99
+ isNewerVersion(current, latest) {
100
+ if (!current || !latest) return false;
101
+
102
+ const currentParts = current.split('.').map(Number);
103
+ const latestParts = latest.split('.').map(Number);
104
+
105
+ for (let i = 0; i < 3; i++) {
106
+ if (latestParts[i] > currentParts[i]) return true;
107
+ if (latestParts[i] < currentParts[i]) return false;
108
+ }
109
+ return false;
110
+ }
111
+
112
+ // Read current state
113
+ readState() {
114
+ if (!existsSync(this.stateFile)) {
115
+ const currentVersion = this.getCurrentVersion();
116
+ return {
117
+ currentVersion: currentVersion,
118
+ latestVersion: null,
119
+ lastChecked: null,
120
+ updateAvailable: false,
121
+ updateReady: false,
122
+ downloadedVersion: null,
123
+ updateStatus: 'idle',
124
+ errorMessage: null,
125
+ userDismissed: false,
126
+ dismissedAt: null
127
+ };
128
+ }
129
+
130
+ try {
131
+ return JSON.parse(readFileSync(this.stateFile, 'utf8'));
132
+ } catch (error) {
133
+ return this.readState(); // Return default if parse fails
134
+ }
135
+ }
136
+
137
+ // Write state
138
+ writeState(state) {
139
+ this.initStateDir();
140
+ writeFileSync(this.stateFile, JSON.stringify(state, null, 2));
141
+ }
142
+
143
+ // Check for updates
144
+ async checkForUpdates() {
145
+ updateLogger.info('Checking for updates');
146
+ const settings = this.readSettings();
147
+
148
+ // Don't check if auto-update disabled
149
+ if (!settings.autoUpdate.enabled) {
150
+ updateLogger.info('Auto-update disabled in settings');
151
+ return { updateAvailable: false, disabled: true };
152
+ }
153
+
154
+ const currentVersion = this.getCurrentVersion();
155
+ const latestVersion = await this.getLatestVersion();
156
+
157
+ if (!currentVersion) {
158
+ updateLogger.error('Failed to read current version');
159
+ return { updateAvailable: false, error: 'Failed to read current version' };
160
+ }
161
+
162
+ if (!latestVersion) {
163
+ updateLogger.error('Failed to check for updates from npm');
164
+ return { updateAvailable: false, error: 'Failed to check for updates' };
165
+ }
166
+
167
+ const updateAvailable = this.isNewerVersion(currentVersion, latestVersion);
168
+
169
+ updateLogger.info(`Version check: current=${currentVersion}, latest=${latestVersion}, updateAvailable=${updateAvailable}`);
170
+
171
+ const state = this.readState();
172
+ state.currentVersion = currentVersion;
173
+ state.latestVersion = latestVersion;
174
+ state.lastChecked = new Date().toISOString();
175
+ state.updateAvailable = updateAvailable;
176
+
177
+ // Reset dismissed flag if new version available
178
+ if (updateAvailable && state.downloadedVersion !== latestVersion) {
179
+ updateLogger.info(`New version available: ${latestVersion}`);
180
+ state.userDismissed = false;
181
+ state.updateReady = false;
182
+ state.updateStatus = 'pending';
183
+ }
184
+
185
+ this.writeState(state);
186
+
187
+ return { updateAvailable, currentVersion, latestVersion };
188
+ }
189
+
190
+ // Start background checker
191
+ startBackgroundChecker() {
192
+ const settings = this.readSettings();
193
+
194
+ // Don't start if disabled
195
+ if (!settings.autoUpdate.enabled) {
196
+ return;
197
+ }
198
+
199
+ // Check immediately on start (async, don't block)
200
+ this.checkForUpdates().catch(() => {
201
+ // Silently fail
202
+ });
203
+
204
+ // Check at configured interval
205
+ const interval = settings.autoUpdate.checkInterval || this.defaultCheckInterval;
206
+ setInterval(() => {
207
+ this.checkForUpdates().catch(() => {
208
+ // Silently fail
209
+ });
210
+ }, interval);
211
+ }
212
+
213
+ // Get check interval from settings
214
+ getCheckInterval() {
215
+ const settings = this.readSettings();
216
+ return settings.autoUpdate.checkInterval || this.defaultCheckInterval;
217
+ }
218
+ }
@@ -0,0 +1,184 @@
1
+ import { spawn, execSync } from 'child_process';
2
+ import { UpdateChecker } from './update-checker.js';
3
+ import { installerLogger } from './logger.js';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
7
+
8
+ /**
9
+ * UpdateInstaller - Handles npm package installation
10
+ * Runs npm install in background
11
+ */
12
+ export class UpdateInstaller {
13
+ constructor() {
14
+ this.checker = new UpdateChecker();
15
+ this.packageName = '@agile-vibe-coding/avc';
16
+ }
17
+
18
+ // Check if npm is available
19
+ checkNpmAvailable() {
20
+ try {
21
+ execSync('npm --version', { stdio: 'ignore' });
22
+ return true;
23
+ } catch (error) {
24
+ installerLogger.error('npm availability check failed', error);
25
+ return false;
26
+ }
27
+ }
28
+
29
+ // Install update in background
30
+ async installUpdate(version) {
31
+ installerLogger.info(`Starting update installation for version ${version}`);
32
+ const state = this.checker.readState();
33
+
34
+ // Don't install if already in progress
35
+ if (state.updateStatus === 'downloading') {
36
+ installerLogger.warn('Update already in progress');
37
+ return { success: false, message: 'Update already in progress' };
38
+ }
39
+
40
+ // Check if npm is available
41
+ if (!this.checkNpmAvailable()) {
42
+ installerLogger.error('npm not found on system');
43
+ state.updateStatus = 'failed';
44
+ state.errorMessage = 'npm not found. Please install npm to enable auto-updates.';
45
+ this.checker.writeState(state);
46
+ return { success: false, message: state.errorMessage };
47
+ }
48
+
49
+ // Update state to downloading
50
+ installerLogger.debug('Setting status to downloading');
51
+ state.updateStatus = 'downloading';
52
+ state.errorMessage = null;
53
+ this.checker.writeState(state);
54
+
55
+ return new Promise((resolve) => {
56
+ // Prepare log files for npm output
57
+ const logDir = path.join(os.homedir(), '.avc', 'logs');
58
+ if (!fs.existsSync(logDir)) {
59
+ fs.mkdirSync(logDir, { recursive: true });
60
+ }
61
+
62
+ const npmLogFile = path.join(logDir, 'npm-install.log');
63
+ const npmErrorLogFile = path.join(logDir, 'npm-install-error.log');
64
+
65
+ // Use shell redirection for reliable log capture with detached process
66
+ const npmCmd = `npm install -g "${this.packageName}@${version}" >> "${npmLogFile}" 2>> "${npmErrorLogFile}"`;
67
+
68
+ installerLogger.info(`Executing: npm install -g ${this.packageName}@${version}`);
69
+ installerLogger.debug(`npm output will be logged to ${npmLogFile}`);
70
+
71
+ const npmProcess = spawn(npmCmd, [], {
72
+ detached: true,
73
+ stdio: 'ignore',
74
+ shell: true
75
+ });
76
+
77
+ npmProcess.unref(); // Allow parent to exit independently
78
+
79
+ npmProcess.on('close', (code) => {
80
+ const state = this.checker.readState();
81
+ installerLogger.debug(`npm install exited with code ${code}`);
82
+
83
+ if (code === 0) {
84
+ // Success
85
+ installerLogger.info(`Update installed successfully: ${version}`);
86
+ state.updateStatus = 'ready';
87
+ state.updateReady = true;
88
+ state.downloadedVersion = version;
89
+ state.errorMessage = null;
90
+ this.checker.writeState(state);
91
+
92
+ resolve({ success: true, version });
93
+ } else if (code === 243 || code === 1) {
94
+ // Permission error (common code 243 on Unix, 1 on Windows)
95
+ const errorMsg = `Permission denied. Try: sudo npm install -g ${this.packageName}@${version}\nLogs: ${npmLogFile}`;
96
+ installerLogger.error(`Installation failed with permission error (code ${code})`, { error: errorMsg, logFile: npmLogFile });
97
+ state.updateStatus = 'failed';
98
+ state.updateReady = false;
99
+ state.errorMessage = errorMsg;
100
+ this.checker.writeState(state);
101
+
102
+ resolve({ success: false, message: state.errorMessage, needsSudo: true });
103
+ } else {
104
+ // Other error
105
+ const errorMsg = `Installation failed with code ${code}. Check logs: ${npmLogFile}`;
106
+ installerLogger.error(errorMsg, { logFile: npmLogFile, errorLogFile: npmErrorLogFile });
107
+ state.updateStatus = 'failed';
108
+ state.updateReady = false;
109
+ state.errorMessage = errorMsg;
110
+ this.checker.writeState(state);
111
+
112
+ resolve({ success: false, message: state.errorMessage });
113
+ }
114
+ });
115
+
116
+ npmProcess.on('error', (error) => {
117
+ const state = this.checker.readState();
118
+ installerLogger.error('npm process error', error);
119
+
120
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
121
+ const errorMsg = `Permission denied. Try: sudo npm install -g ${this.packageName}@${version}\nLogs: ${npmLogFile}`;
122
+ installerLogger.error(`Permission error: ${error.code}`, { error: errorMsg, logFile: npmLogFile });
123
+ state.updateStatus = 'failed';
124
+ state.updateReady = false;
125
+ state.errorMessage = errorMsg;
126
+ this.checker.writeState(state);
127
+
128
+ resolve({ success: false, message: state.errorMessage, needsSudo: true });
129
+ } else {
130
+ const errorMsg = `${error.message}. Check logs: ${npmLogFile}`;
131
+ installerLogger.error(`Unexpected npm process error: ${error.message}`, { error, logFile: npmLogFile });
132
+ state.updateStatus = 'failed';
133
+ state.updateReady = false;
134
+ state.errorMessage = errorMsg;
135
+ this.checker.writeState(state);
136
+
137
+ resolve({ success: false, message: state.errorMessage });
138
+ }
139
+ });
140
+ });
141
+ }
142
+
143
+ // Trigger update installation
144
+ async triggerUpdate() {
145
+ const state = this.checker.readState();
146
+
147
+ if (!state.updateAvailable) {
148
+ return { success: false, message: 'No update available' };
149
+ }
150
+
151
+ if (state.updateReady) {
152
+ return { success: true, message: 'Update already installed', alreadyReady: true };
153
+ }
154
+
155
+ if (state.updateStatus === 'downloading') {
156
+ return { success: false, message: 'Update already in progress', inProgress: true };
157
+ }
158
+
159
+ return await this.installUpdate(state.latestVersion);
160
+ }
161
+
162
+ // Auto-trigger update if available and not dismissed
163
+ async autoTriggerUpdate() {
164
+ const state = this.checker.readState();
165
+ const settings = this.checker.readSettings();
166
+
167
+ // Don't auto-install if disabled or user dismissed
168
+ if (!settings.autoUpdate.enabled || state.userDismissed) {
169
+ return { success: false, message: 'Auto-update disabled or dismissed' };
170
+ }
171
+
172
+ // Don't install if already ready or downloading
173
+ if (state.updateReady || state.updateStatus === 'downloading') {
174
+ return { success: false, message: 'Update already processed' };
175
+ }
176
+
177
+ // Only auto-install if update available and in idle/pending state
178
+ if (state.updateAvailable && (state.updateStatus === 'idle' || state.updateStatus === 'pending')) {
179
+ return await this.triggerUpdate();
180
+ }
181
+
182
+ return { success: false, message: 'No action needed' };
183
+ }
184
+ }
@@ -0,0 +1,170 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { UpdateChecker } from './update-checker.js';
4
+
5
+ /**
6
+ * Update notification components for Ink
7
+ */
8
+
9
+ // Update notification banner (full notification)
10
+ export const UpdateNotification = ({ onDismiss }) => {
11
+ const checker = new UpdateChecker();
12
+ const state = checker.readState();
13
+
14
+ // Don't show if no update available
15
+ if (!state.updateAvailable) return null;
16
+
17
+ // Don't show if user dismissed
18
+ if (state.userDismissed) return null;
19
+
20
+ // Update downloading
21
+ if (state.updateStatus === 'downloading') {
22
+ return React.createElement(Box, {
23
+ borderStyle: 'round',
24
+ borderColor: 'blue',
25
+ paddingX: 2,
26
+ paddingY: 1,
27
+ marginBottom: 1
28
+ },
29
+ React.createElement(Box, { flexDirection: 'column' },
30
+ React.createElement(Text, { bold: true, color: 'blue' },
31
+ `⬇️ Downloading update v${state.latestVersion}...`
32
+ ),
33
+ React.createElement(Text, { dimColor: true },
34
+ 'This happens in the background. Continue working!'
35
+ )
36
+ )
37
+ );
38
+ }
39
+
40
+ // Update ready to use
41
+ if (state.updateReady) {
42
+ return React.createElement(Box, {
43
+ borderStyle: 'round',
44
+ borderColor: 'green',
45
+ paddingX: 2,
46
+ paddingY: 1,
47
+ marginBottom: 1
48
+ },
49
+ React.createElement(Box, { flexDirection: 'column' },
50
+ React.createElement(Text, { bold: true, color: 'green' },
51
+ `✅ Update v${state.downloadedVersion} ready!`
52
+ ),
53
+ React.createElement(Text, null,
54
+ 'Restart to use the new version'
55
+ ),
56
+ React.createElement(Box, { marginTop: 1, flexDirection: 'row' },
57
+ React.createElement(Text, { dimColor: true },
58
+ 'Type '
59
+ ),
60
+ React.createElement(Text, { color: 'cyan', bold: true },
61
+ '/restart'
62
+ ),
63
+ React.createElement(Text, { dimColor: true },
64
+ ' or press '
65
+ ),
66
+ React.createElement(Text, { color: 'cyan', bold: true },
67
+ 'Ctrl+R'
68
+ )
69
+ ),
70
+ React.createElement(Box, { marginTop: 1, flexDirection: 'row' },
71
+ React.createElement(Text, { dimColor: true, italic: true },
72
+ 'Press '
73
+ ),
74
+ React.createElement(Text, { color: 'yellow' },
75
+ 'Esc'
76
+ ),
77
+ React.createElement(Text, { dimColor: true, italic: true },
78
+ ' to dismiss'
79
+ )
80
+ )
81
+ )
82
+ );
83
+ }
84
+
85
+ // Update failed
86
+ if (state.updateStatus === 'failed') {
87
+ const needsSudo = state.errorMessage && state.errorMessage.includes('sudo');
88
+
89
+ return React.createElement(Box, {
90
+ borderStyle: 'round',
91
+ borderColor: 'red',
92
+ paddingX: 2,
93
+ paddingY: 1,
94
+ marginBottom: 1
95
+ },
96
+ React.createElement(Box, { flexDirection: 'column' },
97
+ React.createElement(Text, { bold: true, color: 'red' },
98
+ '❌ Update failed'
99
+ ),
100
+ React.createElement(Text, null,
101
+ state.errorMessage || 'Unknown error'
102
+ ),
103
+ needsSudo && React.createElement(Box, { marginTop: 1 },
104
+ React.createElement(Text, { dimColor: true },
105
+ 'Manual install: '
106
+ ),
107
+ React.createElement(Text, { color: 'cyan' },
108
+ state.errorMessage.split('Try: ')[1]
109
+ )
110
+ ),
111
+ React.createElement(Text, { dimColor: true, marginTop: 1 },
112
+ 'Will retry on next check'
113
+ )
114
+ )
115
+ );
116
+ }
117
+
118
+ // Update pending (just discovered)
119
+ if (state.updateStatus === 'pending' || state.updateStatus === 'idle') {
120
+ return React.createElement(Box, {
121
+ borderStyle: 'round',
122
+ borderColor: 'yellow',
123
+ paddingX: 2,
124
+ paddingY: 1,
125
+ marginBottom: 1
126
+ },
127
+ React.createElement(Box, { flexDirection: 'column' },
128
+ React.createElement(Text, { bold: true, color: 'yellow' },
129
+ `📦 Update available: v${state.latestVersion}`
130
+ ),
131
+ React.createElement(Text, null,
132
+ 'Installing in background...'
133
+ ),
134
+ React.createElement(Text, { dimColor: true, italic: true },
135
+ 'You will be notified when ready'
136
+ )
137
+ )
138
+ );
139
+ }
140
+
141
+ return null;
142
+ };
143
+
144
+ // Update status badge (compact, for banner)
145
+ export const UpdateStatusBadge = () => {
146
+ const checker = new UpdateChecker();
147
+ const state = checker.readState();
148
+
149
+ if (!state.updateAvailable || state.userDismissed) return null;
150
+
151
+ if (state.updateReady) {
152
+ return React.createElement(Text, { color: 'green', bold: true },
153
+ ' [Update Ready!]'
154
+ );
155
+ }
156
+
157
+ if (state.updateStatus === 'downloading') {
158
+ return React.createElement(Text, { color: 'blue', bold: true },
159
+ ' [Updating...]'
160
+ );
161
+ }
162
+
163
+ if (state.updateStatus === 'pending' || state.updateStatus === 'idle') {
164
+ return React.createElement(Text, { color: 'yellow', bold: true },
165
+ ' [Update Available]'
166
+ );
167
+ }
168
+
169
+ return null;
170
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@agile-vibe-coding/avc",
3
+ "version": "0.1.0",
4
+ "description": "Agile Vibe Coding (AVC) - Framework for managing AI agent-based software development projects",
5
+ "type": "module",
6
+ "main": "cli/index.js",
7
+ "bin": {
8
+ "avc": "./cli/index.js"
9
+ },
10
+ "files": [
11
+ "cli/",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "test": "echo \"No tests specified yet\" && exit 0",
20
+ "prepublishOnly": "echo \"Running pre-publish checks...\" && npm test"
21
+ },
22
+ "keywords": [
23
+ "avc",
24
+ "agile-vibe-coding",
25
+ "ai-agents",
26
+ "llm",
27
+ "claude",
28
+ "chatgpt",
29
+ "agile",
30
+ "framework",
31
+ "project-management",
32
+ "automation",
33
+ "cli",
34
+ "workflow"
35
+ ],
36
+ "author": "Nacho Coll",
37
+ "license": "MIT",
38
+ "homepage": "https://agilevibecoding.org",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/NachoColl/agilevibecoding.git",
42
+ "directory": "src"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/NachoColl/agilevibecoding/issues"
46
+ },
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ },
50
+ "dependencies": {
51
+ "@anthropic-ai/sdk": "^0.20.0",
52
+ "dotenv": "^16.4.0",
53
+ "ink": "^5.0.1",
54
+ "ink-select-input": "^6.0.0",
55
+ "ink-spinner": "^5.0.0",
56
+ "react": "^18.3.1"
57
+ }
58
+ }