nex-code 0.3.4 → 0.3.7

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/cli/git.js DELETED
@@ -1,211 +0,0 @@
1
- /**
2
- * cli/git.js — Git Intelligence
3
- * Smart commits, PR creation, branch management, diff-aware context
4
- */
5
-
6
- const { execSync, execFileSync } = require('child_process');
7
- const { C } = require('./ui');
8
-
9
- function exec(cmd) {
10
- try {
11
- return execSync(cmd, { cwd: process.cwd(), encoding: 'utf-8', timeout: 30000, stdio: 'pipe' }).trim();
12
- } catch (e) {
13
- return null;
14
- }
15
- }
16
-
17
- function execGit(...args) {
18
- try {
19
- return execFileSync('git', args, { cwd: process.cwd(), encoding: 'utf-8', timeout: 30000, stdio: 'pipe' }).trim();
20
- } catch (e) {
21
- return null;
22
- }
23
- }
24
-
25
- /**
26
- * Check if we're in a git repository
27
- */
28
- function isGitRepo() {
29
- return exec('git rev-parse --is-inside-work-tree') === 'true';
30
- }
31
-
32
- /**
33
- * Get current branch name
34
- */
35
- function getCurrentBranch() {
36
- return exec('git branch --show-current');
37
- }
38
-
39
- /**
40
- * Get git status (short format)
41
- */
42
- function getStatus() {
43
- try {
44
- const raw = execSync('git status --porcelain', { cwd: process.cwd(), encoding: 'utf-8', timeout: 30000, stdio: 'pipe' });
45
- if (!raw || !raw.trim()) return [];
46
- return raw.split('\n').filter(Boolean).map((line) => {
47
- const status = line.substring(0, 2).trim();
48
- const file = line.substring(3);
49
- return { status, file };
50
- });
51
- } catch {
52
- return [];
53
- }
54
- }
55
-
56
- /**
57
- * Get the diff for staged + unstaged changes
58
- * @param {boolean} staged — only staged changes
59
- */
60
- function getDiff(staged = false) {
61
- const flag = staged ? '--cached' : '';
62
- return exec(`git diff ${flag}`) || '';
63
- }
64
-
65
- /**
66
- * Get list of changed files (staged + unstaged)
67
- */
68
- function getChangedFiles() {
69
- const status = getStatus();
70
- return status.map((s) => s.file);
71
- }
72
-
73
- /**
74
- * Analyze diff and generate a commit message suggestion
75
- * @returns {{ summary: string, files: string[], stats: { additions: number, deletions: number } }}
76
- */
77
- function analyzeDiff() {
78
- const files = getChangedFiles();
79
- if (files.length === 0) return null;
80
-
81
- const diff = getDiff();
82
- const stagedDiff = getDiff(true);
83
- const fullDiff = stagedDiff || diff;
84
-
85
- // Count additions/deletions from diff (if available)
86
- let additions = 0;
87
- let deletions = 0;
88
- if (fullDiff) {
89
- const lines = fullDiff.split('\n');
90
- for (const line of lines) {
91
- if (line.startsWith('+') && !line.startsWith('+++')) additions++;
92
- if (line.startsWith('-') && !line.startsWith('---')) deletions++;
93
- }
94
- } else {
95
- // Untracked files: count as additions
96
- additions = files.length;
97
- }
98
-
99
- // Determine type based on files and content
100
- let type = 'chore';
101
- const fileNames = files.join(' ').toLowerCase();
102
- if (fileNames.includes('test')) type = 'test';
103
- else if (fileNames.includes('readme') || fileNames.includes('doc')) type = 'docs';
104
- else if (additions > deletions * 2) type = 'feat';
105
- else if (deletions > additions) type = 'refactor';
106
- else type = 'fix';
107
-
108
- // Generate summary from file names
109
- const shortFiles = files.slice(0, 3).map((f) => f.split('/').pop());
110
- const summary = `${type}: update ${shortFiles.join(', ')}${files.length > 3 ? ` (+${files.length - 3} more)` : ''}`;
111
-
112
- return {
113
- summary,
114
- type,
115
- files,
116
- stats: { additions, deletions },
117
- };
118
- }
119
-
120
- /**
121
- * Create a branch from a task description
122
- * @param {string} description
123
- * @returns {string|null} branch name or null on error
124
- */
125
- function createBranch(description) {
126
- const name = description
127
- .toLowerCase()
128
- .replace(/[^a-z0-9\s-]/g, '')
129
- .replace(/\s+/g, '-')
130
- .substring(0, 50);
131
-
132
- const branchName = `feat/${name}`;
133
- const result = execGit('checkout', '-b', branchName);
134
- return result !== null ? branchName : null;
135
- }
136
-
137
- /**
138
- * Stage all changes and commit
139
- * @param {string} message
140
- * @returns {string|null} commit hash or null
141
- */
142
- function commit(message) {
143
- execGit('add', '-A');
144
- const result = execGit('commit', '-m', message);
145
- if (!result) return null;
146
- return execGit('rev-parse', '--short', 'HEAD');
147
- }
148
-
149
- /**
150
- * Show a formatted diff summary
151
- */
152
- function formatDiffSummary() {
153
- const analysis = analyzeDiff();
154
- if (!analysis) return `${C.dim}No changes${C.reset}`;
155
-
156
- const lines = [];
157
- lines.push(`\n${C.bold}${C.cyan}Git Diff Summary:${C.reset}`);
158
- lines.push(` ${C.green}+${analysis.stats.additions}${C.reset} ${C.red}-${analysis.stats.deletions}${C.reset} in ${analysis.files.length} file(s)`);
159
- lines.push(`\n${C.bold}${C.cyan}Files:${C.reset}`);
160
- for (const f of analysis.files.slice(0, 20)) {
161
- lines.push(` ${C.dim}${f}${C.reset}`);
162
- }
163
- if (analysis.files.length > 20) {
164
- lines.push(` ${C.dim}...+${analysis.files.length - 20} more${C.reset}`);
165
- }
166
- lines.push(`\n${C.bold}${C.cyan}Suggested message:${C.reset}`);
167
- lines.push(` ${C.cyan}${analysis.summary}${C.reset}\n`);
168
- return lines.join('\n');
169
- }
170
-
171
- /**
172
- * Get files with unresolved merge conflicts (UU, AA, DD)
173
- */
174
- function getMergeConflicts() {
175
- const status = getStatus();
176
- return status.filter(s => s.status === 'UU' || s.status === 'AA' || s.status === 'DD');
177
- }
178
-
179
- /**
180
- * Get diff-aware context (only changed files' content)
181
- * For use when the user is working on git-related tasks
182
- */
183
- function getDiffContext() {
184
- const files = getChangedFiles();
185
- if (files.length === 0) return '';
186
-
187
- const parts = [`CHANGED FILES (${files.length}):`];
188
- for (const f of files.slice(0, 10)) {
189
- parts.push(` ${f}`);
190
- }
191
- const diff = getDiff();
192
- if (diff) {
193
- const truncated = diff.length > 5000 ? diff.substring(0, 5000) + '\n...(truncated)' : diff;
194
- parts.push(`\nDIFF:\n${truncated}`);
195
- }
196
- return parts.join('\n');
197
- }
198
-
199
- module.exports = {
200
- isGitRepo,
201
- getCurrentBranch,
202
- getStatus,
203
- getDiff,
204
- getChangedFiles,
205
- analyzeDiff,
206
- createBranch,
207
- commit,
208
- formatDiffSummary,
209
- getDiffContext,
210
- getMergeConflicts,
211
- };
package/cli/hooks.js DELETED
@@ -1,173 +0,0 @@
1
- /**
2
- * cli/hooks.js — Hook System
3
- * Execute custom scripts in response to CLI events.
4
- * Hook scripts live in .nex/hooks/ or are configured in .nex/config.json
5
- */
6
-
7
- const { execSync } = require('child_process');
8
- const path = require('path');
9
- const fs = require('fs');
10
-
11
- /**
12
- * Valid hook events
13
- */
14
- const HOOK_EVENTS = [
15
- 'pre-tool', // Before any tool execution
16
- 'post-tool', // After tool execution
17
- 'pre-commit', // Before git commit
18
- 'post-response', // After LLM response
19
- 'session-start', // When REPL starts
20
- 'session-end', // When REPL exits
21
- ];
22
-
23
- function getHooksDir() {
24
- return path.join(process.cwd(), '.nex', 'hooks');
25
- }
26
-
27
- function getConfigPath() {
28
- return path.join(process.cwd(), '.nex', 'config.json');
29
- }
30
-
31
- /**
32
- * Load hook configuration from .nex/config.json
33
- * @returns {Object<string, string[]>} — event → array of commands
34
- */
35
- function loadHookConfig() {
36
- const configPath = getConfigPath();
37
- if (!fs.existsSync(configPath)) return {};
38
- try {
39
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
40
- return config.hooks || {};
41
- } catch {
42
- return {};
43
- }
44
- }
45
-
46
- /**
47
- * Get all hooks for a given event
48
- * Sources: .nex/hooks/{event} files + .nex/config.json hooks
49
- * @param {string} event
50
- * @returns {string[]} — array of commands to execute
51
- */
52
- function getHooksForEvent(event) {
53
- if (!HOOK_EVENTS.includes(event)) return [];
54
-
55
- const hooks = [];
56
-
57
- // 1. Check .nex/hooks/{event} script
58
- const hooksDir = getHooksDir();
59
- const scriptPath = path.join(hooksDir, event);
60
- if (fs.existsSync(scriptPath)) {
61
- hooks.push(scriptPath);
62
- }
63
-
64
- // 2. Check .nex/config.json hooks
65
- const config = loadHookConfig();
66
- if (config[event]) {
67
- const cmds = Array.isArray(config[event]) ? config[event] : [config[event]];
68
- hooks.push(...cmds);
69
- }
70
-
71
- return hooks;
72
- }
73
-
74
- /**
75
- * Execute a single hook command
76
- * @param {string} command
77
- * @param {Object} env — additional environment variables
78
- * @param {number} timeout — ms
79
- * @returns {{success: boolean, output?: string, error?: string}}
80
- */
81
- function executeHook(command, env = {}, timeout = 30000) {
82
- try {
83
- const output = execSync(command, {
84
- cwd: process.cwd(),
85
- encoding: 'utf-8',
86
- timeout,
87
- env: { ...process.env, ...env },
88
- stdio: ['pipe', 'pipe', 'pipe'],
89
- });
90
- return { success: true, output: output.trim() };
91
- } catch (err) {
92
- return {
93
- success: false,
94
- error: err.stderr ? err.stderr.trim() : err.message,
95
- };
96
- }
97
- }
98
-
99
- /**
100
- * Run all hooks for an event
101
- * @param {string} event
102
- * @param {Object} context — contextual data passed as env vars
103
- * @returns {Array<{command: string, success: boolean, output?: string, error?: string}>}
104
- */
105
- function runHooks(event, context = {}) {
106
- const hooks = getHooksForEvent(event);
107
- if (hooks.length === 0) return [];
108
-
109
- // Convert context to NEX_* env vars
110
- const env = {};
111
- for (const [key, value] of Object.entries(context)) {
112
- env[`NEX_${key.toUpperCase()}`] = String(value);
113
- }
114
-
115
- const results = [];
116
- for (const command of hooks) {
117
- const result = executeHook(command, env);
118
- results.push({ command, ...result });
119
-
120
- // Stop on failure for pre-* hooks (they can block)
121
- if (!result.success && event.startsWith('pre-')) {
122
- break;
123
- }
124
- }
125
-
126
- return results;
127
- }
128
-
129
- /**
130
- * Check if any hooks are configured for an event
131
- * @param {string} event
132
- * @returns {boolean}
133
- */
134
- function hasHooks(event) {
135
- return getHooksForEvent(event).length > 0;
136
- }
137
-
138
- /**
139
- * List all configured hooks
140
- * @returns {Array<{event: string, commands: string[]}>}
141
- */
142
- function listHooks() {
143
- const result = [];
144
- for (const event of HOOK_EVENTS) {
145
- const commands = getHooksForEvent(event);
146
- if (commands.length > 0) {
147
- result.push({ event, commands });
148
- }
149
- }
150
- return result;
151
- }
152
-
153
- /**
154
- * Initialize hooks directory
155
- */
156
- function initHooksDir() {
157
- const dir = getHooksDir();
158
- if (!fs.existsSync(dir)) {
159
- fs.mkdirSync(dir, { recursive: true });
160
- }
161
- return dir;
162
- }
163
-
164
- module.exports = {
165
- HOOK_EVENTS,
166
- loadHookConfig,
167
- getHooksForEvent,
168
- executeHook,
169
- runHooks,
170
- hasHooks,
171
- listHooks,
172
- initHooksDir,
173
- };