@agentic15.com/agentic15-claude-zen 1.0.1 → 1.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,192 @@
1
+ /**
2
+ * Copyright 2024-2025 agentic15.com
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+ import { execSync } from 'child_process';
20
+
21
+ /**
22
+ * GitHubConfig - Manages GitHub integration configuration
23
+ *
24
+ * Single Responsibility: Load and validate GitHub settings
25
+ *
26
+ * Configuration sources (in order of priority):
27
+ * 1. Environment variables (highest)
28
+ * 2. .claude/settings.local.json (user-specific, gitignored)
29
+ * 3. .claude/settings.json (defaults)
30
+ * 4. Auto-detection from git remote (fallback for owner/repo)
31
+ */
32
+ export class GitHubConfig {
33
+ /**
34
+ * Initialize configuration loader
35
+ *
36
+ * @param {string} projectRoot - Project root directory
37
+ */
38
+ constructor(projectRoot = process.cwd()) {
39
+ this.projectRoot = projectRoot;
40
+ this.config = this.loadConfig();
41
+ }
42
+
43
+ /**
44
+ * Load configuration from all sources
45
+ *
46
+ * @returns {Object} Merged configuration object
47
+ */
48
+ loadConfig() {
49
+ const config = {
50
+ enabled: true,
51
+ token: null,
52
+ owner: null,
53
+ repo: null,
54
+ autoCreate: true,
55
+ autoUpdate: true,
56
+ autoClose: true
57
+ };
58
+
59
+ // Load from settings.json (defaults)
60
+ const settingsPath = path.join(this.projectRoot, '.claude', 'settings.json');
61
+ if (fs.existsSync(settingsPath)) {
62
+ try {
63
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
64
+ if (settings.github) {
65
+ Object.assign(config, settings.github);
66
+ }
67
+ } catch (error) {
68
+ console.warn('⚠ Failed to load .claude/settings.json:', error.message);
69
+ }
70
+ }
71
+
72
+ // Load from settings.local.json (user overrides)
73
+ const localSettingsPath = path.join(this.projectRoot, '.claude', 'settings.local.json');
74
+ if (fs.existsSync(localSettingsPath)) {
75
+ try {
76
+ const localSettings = JSON.parse(fs.readFileSync(localSettingsPath, 'utf8'));
77
+ if (localSettings.github) {
78
+ Object.assign(config, localSettings.github);
79
+ }
80
+ } catch (error) {
81
+ console.warn('⚠ Failed to load .claude/settings.local.json:', error.message);
82
+ }
83
+ }
84
+
85
+ // Override with environment variables (highest priority)
86
+ if (process.env.GITHUB_TOKEN) {
87
+ config.token = process.env.GITHUB_TOKEN;
88
+ }
89
+ if (process.env.GITHUB_OWNER) {
90
+ config.owner = process.env.GITHUB_OWNER;
91
+ }
92
+ if (process.env.GITHUB_REPO) {
93
+ config.repo = process.env.GITHUB_REPO;
94
+ }
95
+ if (process.env.GITHUB_ENABLED !== undefined) {
96
+ config.enabled = process.env.GITHUB_ENABLED === 'true';
97
+ }
98
+ if (process.env.GITHUB_AUTO_CREATE !== undefined) {
99
+ config.autoCreate = process.env.GITHUB_AUTO_CREATE === 'true';
100
+ }
101
+ if (process.env.GITHUB_AUTO_UPDATE !== undefined) {
102
+ config.autoUpdate = process.env.GITHUB_AUTO_UPDATE === 'true';
103
+ }
104
+ if (process.env.GITHUB_AUTO_CLOSE !== undefined) {
105
+ config.autoClose = process.env.GITHUB_AUTO_CLOSE === 'true';
106
+ }
107
+
108
+ // Auto-detect owner/repo from git remote if not configured
109
+ if (!config.owner || !config.repo) {
110
+ try {
111
+ const remote = execSync('git remote get-url origin', {
112
+ cwd: this.projectRoot,
113
+ encoding: 'utf8'
114
+ }).trim();
115
+
116
+ // Parse GitHub URL formats:
117
+ // - https://github.com/owner/repo.git
118
+ // - git@github.com:owner/repo.git
119
+ const match = remote.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
120
+ if (match) {
121
+ config.owner = config.owner || match[1];
122
+ config.repo = config.repo || match[2];
123
+ }
124
+ } catch (error) {
125
+ // Git remote not configured or not a git repository
126
+ // This is okay - GitHub integration will be disabled
127
+ }
128
+ }
129
+
130
+ return config;
131
+ }
132
+
133
+ /**
134
+ * Get GitHub Personal Access Token
135
+ *
136
+ * @returns {string|null} Token or null if not configured
137
+ */
138
+ getToken() {
139
+ return this.config.token;
140
+ }
141
+
142
+ /**
143
+ * Get repository owner and name
144
+ *
145
+ * @returns {Object} { owner: string|null, repo: string|null }
146
+ */
147
+ getRepoInfo() {
148
+ return {
149
+ owner: this.config.owner,
150
+ repo: this.config.repo
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Check if GitHub integration is fully enabled and configured
156
+ *
157
+ * @returns {boolean} True if ready to use, false otherwise
158
+ */
159
+ isEnabled() {
160
+ return this.config.enabled &&
161
+ this.config.token !== null &&
162
+ this.config.owner !== null &&
163
+ this.config.repo !== null;
164
+ }
165
+
166
+ /**
167
+ * Check if auto-create issues feature is enabled
168
+ *
169
+ * @returns {boolean} True if enabled, false otherwise
170
+ */
171
+ isAutoCreateEnabled() {
172
+ return this.isEnabled() && this.config.autoCreate;
173
+ }
174
+
175
+ /**
176
+ * Check if auto-update issues feature is enabled
177
+ *
178
+ * @returns {boolean} True if enabled, false otherwise
179
+ */
180
+ isAutoUpdateEnabled() {
181
+ return this.isEnabled() && this.config.autoUpdate;
182
+ }
183
+
184
+ /**
185
+ * Check if auto-close issues feature is enabled
186
+ *
187
+ * @returns {boolean} True if enabled, false otherwise
188
+ */
189
+ isAutoCloseEnabled() {
190
+ return this.isEnabled() && this.config.autoClose;
191
+ }
192
+ }
@@ -1,54 +1,71 @@
1
- /**
2
- * Copyright 2024-2025 agentic15.com
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- import { writeFileSync } from 'fs';
18
- import { join } from 'path';
19
-
20
- /**
21
- * HookInstaller - Installs git hooks
22
- *
23
- * Single Responsibility: Setup git hooks in project
24
- */
25
- export class HookInstaller {
26
- /**
27
- * Install git hooks
28
- *
29
- * @param {string} targetDir - Target directory path
30
- */
31
- async install(targetDir) {
32
- console.log('\n🔗 Setting up Git hooks...');
33
- try {
34
- const gitHooksDir = join(targetDir, '.git', 'hooks');
35
-
36
- // Create pre-commit hook (reminder that hooks run via Claude Code)
37
- const preCommitHook = `#!/bin/sh
38
- # agentic15-claude-zen pre-commit hook
39
- # This hook is CRITICAL for enforcement
40
-
41
- echo "⚠️ agentic15-claude-zen git hooks are NOT yet active"
42
- echo " Hooks run via Claude Code settings.json, not git hooks"
43
- echo " This is just a reminder"
44
- exit 0
45
- `;
46
-
47
- writeFileSync(join(gitHooksDir, 'pre-commit'), preCommitHook, { mode: 0o755 });
48
- console.log('✅ Git hooks configured');
49
- } catch (error) {
50
- console.log('⚠️ Warning: Git hooks setup failed');
51
- console.log(` Error: ${error.message}`);
52
- }
53
- }
54
- }
1
+ /**
2
+ * Copyright 2024-2025 agentic15.com
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { writeFileSync, readFileSync, existsSync, chmodSync } from 'fs';
18
+ import { join, dirname } from 'path';
19
+ import { fileURLToPath } from 'url';
20
+
21
+ /**
22
+ * HookInstaller - Installs git hooks
23
+ *
24
+ * Single Responsibility: Setup git hooks in project
25
+ */
26
+ export class HookInstaller {
27
+ /**
28
+ * Install git hooks
29
+ *
30
+ * @param {string} targetDir - Target directory path
31
+ */
32
+ async install(targetDir) {
33
+ console.log('\n🔗 Setting up Git hooks...');
34
+ try {
35
+ const gitHooksDir = join(targetDir, '.git', 'hooks');
36
+ const claudeHooksDir = join(targetDir, '.claude', 'hooks');
37
+
38
+ // Create pre-commit hook (reminder that hooks run via Claude Code)
39
+ const preCommitHook = `#!/bin/sh
40
+ # agentic15-claude-zen pre-commit hook
41
+ # This hook is CRITICAL for enforcement
42
+
43
+ echo "⚠️ agentic15-claude-zen git hooks are NOT yet active"
44
+ echo " Hooks run via Claude Code settings.json, not git hooks"
45
+ echo " This is just a reminder"
46
+ exit 0
47
+ `;
48
+
49
+ writeFileSync(join(gitHooksDir, 'pre-commit'), preCommitHook, { mode: 0o755 });
50
+
51
+ // Install post-merge hook for GitHub issue auto-close
52
+ const postMergeSource = join(claudeHooksDir, 'post-merge.js');
53
+ if (existsSync(postMergeSource)) {
54
+ const postMergeContent = readFileSync(postMergeSource, 'utf8');
55
+ const postMergeHook = join(gitHooksDir, 'post-merge');
56
+ writeFileSync(postMergeHook, postMergeContent, { mode: 0o755 });
57
+ try {
58
+ chmodSync(postMergeHook, 0o755);
59
+ } catch (chmodError) {
60
+ // chmod might fail on some systems, but file should still be executable
61
+ }
62
+ console.log('✅ Git hooks configured (including post-merge for GitHub integration)');
63
+ } else {
64
+ console.log('✅ Git hooks configured');
65
+ }
66
+ } catch (error) {
67
+ console.log('⚠️ Warning: Git hooks setup failed');
68
+ console.log(` Error: ${error.message}`);
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Copyright 2024-2025 agentic15.com
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ /**
18
+ * TaskIssueMapper - Maps task data to GitHub issue format
19
+ *
20
+ * Single Responsibility: Convert between task and issue representations
21
+ *
22
+ * This class provides static methods for mapping task JSON structure
23
+ * to GitHub issue format (title, body, labels).
24
+ */
25
+ export class TaskIssueMapper {
26
+ /**
27
+ * Generate GitHub issue title from task
28
+ *
29
+ * @param {Object} task - Task object
30
+ * @returns {string} Issue title
31
+ */
32
+ static taskToIssueTitle(task) {
33
+ return `[${task.id}] ${task.title}`;
34
+ }
35
+
36
+ /**
37
+ * Generate GitHub issue body from task
38
+ *
39
+ * @param {Object} task - Task object
40
+ * @returns {string} Issue body in markdown format
41
+ */
42
+ static taskToIssueBody(task) {
43
+ let body = `## Task: ${task.title}\n\n`;
44
+
45
+ if (task.description) {
46
+ body += `${task.description}\n\n`;
47
+ }
48
+
49
+ if (task.phase) {
50
+ body += `**Phase:** ${task.phase}\n`;
51
+ }
52
+
53
+ if (task.estimatedHours) {
54
+ body += `**Estimated Hours:** ${task.estimatedHours}h\n`;
55
+ }
56
+
57
+ if (task.completionCriteria && task.completionCriteria.length > 0) {
58
+ body += `\n### Completion Criteria\n\n`;
59
+ task.completionCriteria.forEach(criteria => {
60
+ body += `- [ ] ${criteria}\n`;
61
+ });
62
+ }
63
+
64
+ if (task.dependencies && task.dependencies.length > 0) {
65
+ body += `\n### Dependencies\n\n`;
66
+ task.dependencies.forEach(dep => {
67
+ body += `- ${dep}\n`;
68
+ });
69
+ }
70
+
71
+ if (task.testCases && task.testCases.length > 0) {
72
+ body += `\n### Test Cases\n\n`;
73
+ task.testCases.forEach(test => {
74
+ body += `- ${test}\n`;
75
+ });
76
+ }
77
+
78
+ body += `\n---\n*Auto-created by Agentic15 Claude Zen*`;
79
+
80
+ return body;
81
+ }
82
+
83
+ /**
84
+ * Map task status and phase to GitHub labels
85
+ *
86
+ * @param {string} status - Task status (pending, in_progress, completed, blocked)
87
+ * @param {string|null} phase - Task phase (design, implementation, testing, etc.)
88
+ * @returns {string[]} Array of label names
89
+ */
90
+ static taskStatusToLabels(status, phase = null) {
91
+ const labels = [];
92
+
93
+ // Status labels
94
+ switch (status) {
95
+ case 'pending':
96
+ labels.push('status: pending');
97
+ break;
98
+ case 'in_progress':
99
+ labels.push('status: in-progress');
100
+ break;
101
+ case 'completed':
102
+ labels.push('status: completed');
103
+ break;
104
+ case 'blocked':
105
+ labels.push('status: blocked');
106
+ break;
107
+ }
108
+
109
+ // Phase labels
110
+ if (phase) {
111
+ const phaseLabel = this.phaseToLabel(phase);
112
+ if (phaseLabel) {
113
+ labels.push(phaseLabel);
114
+ }
115
+ }
116
+
117
+ return labels;
118
+ }
119
+
120
+ /**
121
+ * Convert phase name to GitHub label
122
+ *
123
+ * @param {string} phase - Phase name
124
+ * @returns {string} Label name
125
+ */
126
+ static phaseToLabel(phase) {
127
+ const phaseLabels = {
128
+ 'design': 'phase: design',
129
+ 'implementation': 'phase: implementation',
130
+ 'testing': 'phase: testing',
131
+ 'deployment': 'phase: deployment',
132
+ 'documentation': 'phase: documentation'
133
+ };
134
+
135
+ return phaseLabels[phase] || `phase: ${phase}`;
136
+ }
137
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Copyright 2024-2025 agentic15.com
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { GitHubConfig } from '../core/GitHubConfig.js';
18
+ import { GitHubClient } from '../core/GitHubClient.js';
19
+ import { TaskIssueMapper } from '../core/TaskIssueMapper.js';
20
+
21
+ /**
22
+ * Update GitHub issue status when task state changes
23
+ *
24
+ * This utility function is used by various hooks to keep GitHub issues
25
+ * in sync with local task status changes.
26
+ *
27
+ * @param {Object} taskData - Task JSON object
28
+ * @param {string} projectRoot - Path to project root directory
29
+ * @param {string} newStatus - New task status (pending, in_progress, completed, blocked)
30
+ * @param {Object} options - Optional parameters
31
+ * @param {string} options.comment - Optional comment to add to the issue
32
+ * @returns {Promise<boolean>} - Success status
33
+ */
34
+ export async function updateTaskGitHubStatus(taskData, projectRoot, newStatus, options = {}) {
35
+ try {
36
+ // Load GitHub configuration
37
+ const githubConfig = new GitHubConfig(projectRoot);
38
+
39
+ // Check if auto-update is enabled
40
+ if (!githubConfig.isAutoUpdateEnabled()) {
41
+ return false;
42
+ }
43
+
44
+ // Check if task has associated GitHub issue
45
+ if (!taskData.githubIssue) {
46
+ return false;
47
+ }
48
+
49
+ // Initialize GitHub client
50
+ const { owner, repo } = githubConfig.getRepoInfo();
51
+ const githubClient = new GitHubClient(
52
+ githubConfig.getToken(),
53
+ owner,
54
+ repo
55
+ );
56
+
57
+ // Update issue labels based on new status
58
+ const labels = TaskIssueMapper.taskStatusToLabels(newStatus, taskData.phase);
59
+ const labelsUpdated = await githubClient.updateIssueLabels(taskData.githubIssue, labels);
60
+
61
+ if (!labelsUpdated) {
62
+ console.warn(`⚠ Failed to update GitHub issue #${taskData.githubIssue} labels`);
63
+ return false;
64
+ }
65
+
66
+ // Add comment if provided
67
+ if (options.comment) {
68
+ const commentAdded = await githubClient.addIssueComment(taskData.githubIssue, options.comment);
69
+ if (!commentAdded) {
70
+ console.warn(`⚠ Failed to add comment to GitHub issue #${taskData.githubIssue}`);
71
+ }
72
+ }
73
+
74
+ console.log(`✓ Updated GitHub issue #${taskData.githubIssue} to ${newStatus}`);
75
+ return true;
76
+
77
+ } catch (error) {
78
+ console.warn('⚠ Failed to update GitHub issue status:', error.message);
79
+ return false;
80
+ }
81
+ }