claudekit-cli 1.0.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.
- package/.claude/agents/brainstormer.md +96 -0
- package/.claude/agents/code-reviewer.md +141 -0
- package/.claude/agents/copywriter.md +108 -0
- package/.claude/agents/database-admin.md +86 -0
- package/.claude/agents/debugger.md +124 -0
- package/.claude/agents/docs-manager.md +115 -0
- package/.claude/agents/git-manager.md +60 -0
- package/.claude/agents/journal-writer.md +111 -0
- package/.claude/agents/planner.md +87 -0
- package/.claude/agents/project-manager.md +113 -0
- package/.claude/agents/researcher.md +173 -0
- package/.claude/agents/scout.md +123 -0
- package/.claude/agents/tester.md +95 -0
- package/.claude/agents/ui-ux-designer.md +206 -0
- package/.claude/commands/bootstrap.md +104 -0
- package/.claude/commands/brainstorm.md +67 -0
- package/.claude/commands/content/enhance.md +13 -0
- package/.claude/commands/content/fast.md +11 -0
- package/.claude/commands/content/good.md +13 -0
- package/.claude/commands/cook.md +19 -0
- package/.claude/commands/debug.md +10 -0
- package/.claude/commands/design/3d.md +65 -0
- package/.claude/commands/design/describe.md +13 -0
- package/.claude/commands/design/fast.md +19 -0
- package/.claude/commands/design/good.md +23 -0
- package/.claude/commands/design/screenshot.md +23 -0
- package/.claude/commands/design/video.md +23 -0
- package/.claude/commands/docs/init.md +13 -0
- package/.claude/commands/docs/summarize.md +10 -0
- package/.claude/commands/docs/update.md +21 -0
- package/.claude/commands/fix/ci.md +11 -0
- package/.claude/commands/fix/fast.md +12 -0
- package/.claude/commands/fix/hard.md +18 -0
- package/.claude/commands/fix/logs.md +16 -0
- package/.claude/commands/fix/test.md +18 -0
- package/.claude/commands/fix/types.md +10 -0
- package/.claude/commands/git/cm.md +5 -0
- package/.claude/commands/git/cp.md +4 -0
- package/.claude/commands/integrate/polar.md +42 -0
- package/.claude/commands/plan/ci.md +12 -0
- package/.claude/commands/plan/two.md +13 -0
- package/.claude/commands/plan.md +10 -0
- package/.claude/commands/scout.md +29 -0
- package/.claude/commands/test.md +7 -0
- package/.claude/commands/watzup.md +8 -0
- package/.claude/hooks/telegram_notify.sh +136 -0
- package/.claude/send-discord.sh +64 -0
- package/.claude/settings.json +7 -0
- package/.claude/statusline.sh +143 -0
- package/.claude/workflows/development-rules.md +80 -0
- package/.claude/workflows/documentation-management.md +28 -0
- package/.claude/workflows/orchestration-protocol.md +16 -0
- package/.claude/workflows/primary-workflow.md +41 -0
- package/.github/workflows/ci.yml +43 -0
- package/.github/workflows/release.yml +58 -0
- package/.opencode/agent/code-reviewer.md +141 -0
- package/.opencode/agent/debugger.md +74 -0
- package/.opencode/agent/docs-manager.md +119 -0
- package/.opencode/agent/git-manager.md +60 -0
- package/.opencode/agent/planner-researcher.md +100 -0
- package/.opencode/agent/planner.md +87 -0
- package/.opencode/agent/project-manager.md +113 -0
- package/.opencode/agent/researcher.md +173 -0
- package/.opencode/agent/solution-brainstormer.md +89 -0
- package/.opencode/agent/system-architecture.md +192 -0
- package/.opencode/agent/tester.md +96 -0
- package/.opencode/agent/ui-ux-designer.md +203 -0
- package/.opencode/agent/ui-ux-developer.md +97 -0
- package/.opencode/command/cook.md +7 -0
- package/.opencode/command/debug.md +10 -0
- package/.opencode/command/design/3d.md +65 -0
- package/.opencode/command/design/fast.md +18 -0
- package/.opencode/command/design/good.md +21 -0
- package/.opencode/command/design/screenshot.md +22 -0
- package/.opencode/command/design/video.md +22 -0
- package/.opencode/command/docs/init.md +11 -0
- package/.opencode/command/docs/summarize.md +10 -0
- package/.opencode/command/docs/update.md +18 -0
- package/.opencode/command/fix/ci.md +8 -0
- package/.opencode/command/fix/fast.md +11 -0
- package/.opencode/command/fix/hard.md +15 -0
- package/.opencode/command/fix/logs.md +16 -0
- package/.opencode/command/fix/test.md +18 -0
- package/.opencode/command/fix/types.md +10 -0
- package/.opencode/command/git/cm.md +5 -0
- package/.opencode/command/git/cp.md +4 -0
- package/.opencode/command/plan/ci.md +12 -0
- package/.opencode/command/plan/two.md +13 -0
- package/.opencode/command/plan.md +10 -0
- package/.opencode/command/test.md +7 -0
- package/.opencode/command/watzup.md +8 -0
- package/.releaserc.json +17 -0
- package/.repomixignore +15 -0
- package/AGENTS.md +217 -0
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +33 -0
- package/README.md +214 -0
- package/biome.json +25 -0
- package/bun.lock +1238 -0
- package/dist/index.js +19100 -0
- package/docs/code-standards.md +1128 -0
- package/docs/codebase-summary.md +821 -0
- package/docs/github-setup.md +176 -0
- package/docs/project-pdr.md +739 -0
- package/docs/system-architecture.md +950 -0
- package/docs/tech-stack.md +290 -0
- package/package.json +60 -0
- package/plans/251008-claudekit-cli-implementation-plan.md +1469 -0
- package/plans/reports/251008-from-code-reviewer-to-developer-review-report.md +864 -0
- package/plans/reports/251008-from-tester-to-developer-test-summary-report.md +409 -0
- package/plans/reports/251008-researcher-download-extraction-report.md +1377 -0
- package/plans/reports/251008-researcher-github-api-report.md +1339 -0
- package/plans/research/251008-cli-frameworks-bun-research.md +1051 -0
- package/plans/templates/bug-fix-template.md +69 -0
- package/plans/templates/feature-implementation-template.md +84 -0
- package/plans/templates/refactor-template.md +82 -0
- package/plans/templates/template-usage-guide.md +58 -0
- package/src/commands/new.ts +118 -0
- package/src/commands/update.ts +114 -0
- package/src/index.ts +44 -0
- package/src/lib/auth.ts +157 -0
- package/src/lib/download.ts +180 -0
- package/src/lib/github.ts +157 -0
- package/src/lib/merge.ts +116 -0
- package/src/lib/prompts.ts +113 -0
- package/src/types.ts +149 -0
- package/src/utils/config.ts +87 -0
- package/src/utils/logger.ts +37 -0
- package/tests/lib/auth.test.ts +116 -0
- package/tests/lib/download.test.ts +70 -0
- package/tests/lib/github.test.ts +52 -0
- package/tests/lib/merge.test.ts +138 -0
- package/tests/lib/prompts.test.ts +66 -0
- package/tests/types.test.ts +255 -0
- package/tests/utils/config.test.ts +263 -0
- package/tests/utils/logger.test.ts +124 -0
- package/tsconfig.json +30 -0
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import * as clack from "@clack/prompts";
|
|
3
|
+
import keytar from "keytar";
|
|
4
|
+
import { type AuthMethod, AuthenticationError } from "../types.js";
|
|
5
|
+
import { ConfigManager } from "../utils/config.js";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
|
|
8
|
+
const SERVICE_NAME = "claudekit-cli";
|
|
9
|
+
const ACCOUNT_NAME = "github-token";
|
|
10
|
+
|
|
11
|
+
export class AuthManager {
|
|
12
|
+
private static token: string | null = null;
|
|
13
|
+
private static authMethod: AuthMethod | null = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get GitHub token with multi-tier fallback
|
|
17
|
+
*/
|
|
18
|
+
static async getToken(): Promise<{ token: string; method: AuthMethod }> {
|
|
19
|
+
if (AuthManager.token && AuthManager.authMethod) {
|
|
20
|
+
return { token: AuthManager.token, method: AuthManager.authMethod };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Try 1: GitHub CLI
|
|
24
|
+
try {
|
|
25
|
+
const token = await AuthManager.getFromGhCli();
|
|
26
|
+
if (token) {
|
|
27
|
+
AuthManager.token = token;
|
|
28
|
+
AuthManager.authMethod = "gh-cli";
|
|
29
|
+
logger.debug("Using GitHub CLI authentication");
|
|
30
|
+
return { token, method: "gh-cli" };
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger.debug("GitHub CLI not available");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Try 2: Environment variables
|
|
37
|
+
const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
38
|
+
if (envToken) {
|
|
39
|
+
AuthManager.token = envToken;
|
|
40
|
+
AuthManager.authMethod = "env-var";
|
|
41
|
+
logger.debug("Using environment variable authentication");
|
|
42
|
+
return { token: envToken, method: "env-var" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Try 3: Config file
|
|
46
|
+
try {
|
|
47
|
+
const configToken = await ConfigManager.getToken();
|
|
48
|
+
if (configToken) {
|
|
49
|
+
AuthManager.token = configToken;
|
|
50
|
+
AuthManager.authMethod = "env-var";
|
|
51
|
+
logger.debug("Using config file authentication");
|
|
52
|
+
return { token: configToken, method: "env-var" };
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
logger.debug("No token in config file");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Try 4: OS Keychain
|
|
59
|
+
try {
|
|
60
|
+
const keychainToken = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
61
|
+
if (keychainToken) {
|
|
62
|
+
AuthManager.token = keychainToken;
|
|
63
|
+
AuthManager.authMethod = "keychain";
|
|
64
|
+
logger.debug("Using keychain authentication");
|
|
65
|
+
return { token: keychainToken, method: "keychain" };
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.debug("No token in keychain");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Try 5: Prompt user
|
|
72
|
+
const promptedToken = await AuthManager.promptForToken();
|
|
73
|
+
AuthManager.token = promptedToken;
|
|
74
|
+
AuthManager.authMethod = "prompt";
|
|
75
|
+
return { token: promptedToken, method: "prompt" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get token from GitHub CLI
|
|
80
|
+
*/
|
|
81
|
+
private static async getFromGhCli(): Promise<string | null> {
|
|
82
|
+
try {
|
|
83
|
+
const token = execSync("gh auth token", {
|
|
84
|
+
encoding: "utf-8",
|
|
85
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
86
|
+
}).trim();
|
|
87
|
+
if (token && token.length > 0) {
|
|
88
|
+
return token;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Prompt user for token
|
|
98
|
+
*/
|
|
99
|
+
private static async promptForToken(): Promise<string> {
|
|
100
|
+
const token = await clack.password({
|
|
101
|
+
message: "Enter your GitHub Personal Access Token:",
|
|
102
|
+
validate: (value) => {
|
|
103
|
+
if (!value || value.length === 0) {
|
|
104
|
+
return "Token is required";
|
|
105
|
+
}
|
|
106
|
+
if (!value.startsWith("ghp_") && !value.startsWith("github_pat_")) {
|
|
107
|
+
return 'Invalid token format. Token should start with "ghp_" or "github_pat_"';
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (clack.isCancel(token)) {
|
|
114
|
+
throw new AuthenticationError("Authentication cancelled by user");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Ask if user wants to save token
|
|
118
|
+
const save = await clack.confirm({
|
|
119
|
+
message: "Save token securely in OS keychain?",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (save && !clack.isCancel(save)) {
|
|
123
|
+
try {
|
|
124
|
+
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, token);
|
|
125
|
+
logger.success("Token saved securely in keychain");
|
|
126
|
+
} catch (error) {
|
|
127
|
+
logger.warning("Failed to save token to keychain");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return token;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Clear stored token
|
|
136
|
+
*/
|
|
137
|
+
static async clearToken(): Promise<void> {
|
|
138
|
+
// Always clear in-memory token
|
|
139
|
+
AuthManager.token = null;
|
|
140
|
+
AuthManager.authMethod = null;
|
|
141
|
+
|
|
142
|
+
// Try to clear from keychain (may fail in CI)
|
|
143
|
+
try {
|
|
144
|
+
await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
145
|
+
logger.success("Token cleared from keychain");
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.warning("Failed to clear token from keychain");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Validate token format
|
|
153
|
+
*/
|
|
154
|
+
static isValidTokenFormat(token: string): boolean {
|
|
155
|
+
return token.startsWith("ghp_") || token.startsWith("github_pat_");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { pipeline } from "node:stream";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import cliProgress from "cli-progress";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
import * as tar from "tar";
|
|
10
|
+
import unzipper from "unzipper";
|
|
11
|
+
import {
|
|
12
|
+
type ArchiveType,
|
|
13
|
+
DownloadError,
|
|
14
|
+
ExtractionError,
|
|
15
|
+
type GitHubReleaseAsset,
|
|
16
|
+
} from "../types.js";
|
|
17
|
+
import { logger } from "../utils/logger.js";
|
|
18
|
+
|
|
19
|
+
const streamPipeline = promisify(pipeline);
|
|
20
|
+
|
|
21
|
+
export class DownloadManager {
|
|
22
|
+
/**
|
|
23
|
+
* Download asset from URL with progress tracking
|
|
24
|
+
*/
|
|
25
|
+
async downloadAsset(asset: GitHubReleaseAsset, destDir: string): Promise<string> {
|
|
26
|
+
try {
|
|
27
|
+
const destPath = join(destDir, asset.name);
|
|
28
|
+
|
|
29
|
+
// Ensure destination directory exists
|
|
30
|
+
await mkdir(destDir, { recursive: true });
|
|
31
|
+
|
|
32
|
+
logger.info(`Downloading ${asset.name} (${this.formatBytes(asset.size)})...`);
|
|
33
|
+
|
|
34
|
+
// Create progress bar
|
|
35
|
+
const progressBar = new cliProgress.SingleBar({
|
|
36
|
+
format: "Progress |{bar}| {percentage}% | {value}/{total} MB",
|
|
37
|
+
barCompleteChar: "\u2588",
|
|
38
|
+
barIncompleteChar: "\u2591",
|
|
39
|
+
hideCursor: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const response = await fetch(asset.browser_download_url, {
|
|
43
|
+
headers: {
|
|
44
|
+
Accept: "application/octet-stream",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new DownloadError(`Failed to download: ${response.statusText}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const totalSize = asset.size;
|
|
53
|
+
let downloadedSize = 0;
|
|
54
|
+
|
|
55
|
+
progressBar.start(Math.round(totalSize / 1024 / 1024), 0);
|
|
56
|
+
|
|
57
|
+
const fileStream = createWriteStream(destPath);
|
|
58
|
+
const reader = response.body?.getReader();
|
|
59
|
+
|
|
60
|
+
if (!reader) {
|
|
61
|
+
throw new DownloadError("Failed to get response reader");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
while (true) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
|
|
68
|
+
if (done) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fileStream.write(value);
|
|
73
|
+
downloadedSize += value.length;
|
|
74
|
+
progressBar.update(Math.round(downloadedSize / 1024 / 1024));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fileStream.end();
|
|
78
|
+
progressBar.stop();
|
|
79
|
+
|
|
80
|
+
logger.success(`Downloaded ${asset.name}`);
|
|
81
|
+
return destPath;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
fileStream.close();
|
|
84
|
+
progressBar.stop();
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new DownloadError(
|
|
89
|
+
`Failed to download ${asset.name}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract archive to destination
|
|
96
|
+
*/
|
|
97
|
+
async extractArchive(
|
|
98
|
+
archivePath: string,
|
|
99
|
+
destDir: string,
|
|
100
|
+
archiveType?: ArchiveType,
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
const spinner = ora("Extracting files...").start();
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Detect archive type from filename if not provided
|
|
106
|
+
const detectedType = archiveType || this.detectArchiveType(archivePath);
|
|
107
|
+
|
|
108
|
+
// Ensure destination directory exists
|
|
109
|
+
await mkdir(destDir, { recursive: true });
|
|
110
|
+
|
|
111
|
+
if (detectedType === "tar.gz") {
|
|
112
|
+
await this.extractTarGz(archivePath, destDir);
|
|
113
|
+
} else if (detectedType === "zip") {
|
|
114
|
+
await this.extractZip(archivePath, destDir);
|
|
115
|
+
} else {
|
|
116
|
+
throw new ExtractionError(`Unsupported archive type: ${detectedType}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
spinner.succeed("Files extracted successfully");
|
|
120
|
+
} catch (error) {
|
|
121
|
+
spinner.fail("Extraction failed");
|
|
122
|
+
throw new ExtractionError(
|
|
123
|
+
`Failed to extract archive: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Extract tar.gz archive
|
|
130
|
+
*/
|
|
131
|
+
private async extractTarGz(archivePath: string, destDir: string): Promise<void> {
|
|
132
|
+
await tar.extract({
|
|
133
|
+
file: archivePath,
|
|
134
|
+
cwd: destDir,
|
|
135
|
+
strip: 1, // Strip the root directory from the archive
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Extract zip archive
|
|
141
|
+
*/
|
|
142
|
+
private async extractZip(archivePath: string, destDir: string): Promise<void> {
|
|
143
|
+
await streamPipeline(createReadStream(archivePath), unzipper.Extract({ path: destDir }));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Detect archive type from filename
|
|
148
|
+
*/
|
|
149
|
+
private detectArchiveType(filename: string): ArchiveType {
|
|
150
|
+
if (filename.endsWith(".tar.gz") || filename.endsWith(".tgz")) {
|
|
151
|
+
return "tar.gz";
|
|
152
|
+
}
|
|
153
|
+
if (filename.endsWith(".zip")) {
|
|
154
|
+
return "zip";
|
|
155
|
+
}
|
|
156
|
+
throw new ExtractionError(`Cannot detect archive type from filename: ${filename}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create temporary download directory
|
|
161
|
+
*/
|
|
162
|
+
async createTempDir(): Promise<string> {
|
|
163
|
+
const tempDir = join(tmpdir(), `claudekit-${Date.now()}`);
|
|
164
|
+
await mkdir(tempDir, { recursive: true });
|
|
165
|
+
return tempDir;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Format bytes to human readable string
|
|
170
|
+
*/
|
|
171
|
+
private formatBytes(bytes: number): string {
|
|
172
|
+
if (bytes === 0) return "0 Bytes";
|
|
173
|
+
|
|
174
|
+
const k = 1024;
|
|
175
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
176
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
177
|
+
|
|
178
|
+
return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Octokit } from "@octokit/rest";
|
|
2
|
+
import { GitHubError, type GitHubRelease, GitHubReleaseSchema, type KitConfig } from "../types.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
import { AuthManager } from "./auth.js";
|
|
5
|
+
|
|
6
|
+
export class GitHubClient {
|
|
7
|
+
private octokit: Octokit | null = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Initialize Octokit client with authentication
|
|
11
|
+
*/
|
|
12
|
+
private async getClient(): Promise<Octokit> {
|
|
13
|
+
if (this.octokit) {
|
|
14
|
+
return this.octokit;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { token } = await AuthManager.getToken();
|
|
18
|
+
|
|
19
|
+
this.octokit = new Octokit({
|
|
20
|
+
auth: token,
|
|
21
|
+
userAgent: "claudekit-cli",
|
|
22
|
+
request: {
|
|
23
|
+
timeout: 30000, // 30 seconds
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return this.octokit;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get latest release for a kit
|
|
32
|
+
*/
|
|
33
|
+
async getLatestRelease(kit: KitConfig): Promise<GitHubRelease> {
|
|
34
|
+
try {
|
|
35
|
+
const client = await this.getClient();
|
|
36
|
+
|
|
37
|
+
logger.debug(`Fetching latest release for ${kit.owner}/${kit.repo}`);
|
|
38
|
+
|
|
39
|
+
const { data } = await client.repos.getLatestRelease({
|
|
40
|
+
owner: kit.owner,
|
|
41
|
+
repo: kit.repo,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return GitHubReleaseSchema.parse(data);
|
|
45
|
+
} catch (error: any) {
|
|
46
|
+
if (error?.status === 404) {
|
|
47
|
+
throw new GitHubError(`No releases found for ${kit.name}`, 404);
|
|
48
|
+
}
|
|
49
|
+
if (error?.status === 401) {
|
|
50
|
+
throw new GitHubError("Authentication failed. Please check your GitHub token.", 401);
|
|
51
|
+
}
|
|
52
|
+
if (error?.status === 403) {
|
|
53
|
+
throw new GitHubError(
|
|
54
|
+
"Access denied. Make sure your token has access to private repositories.",
|
|
55
|
+
403,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
throw new GitHubError(
|
|
59
|
+
`Failed to fetch release: ${error?.message || "Unknown error"}`,
|
|
60
|
+
error?.status,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get specific release by version tag
|
|
67
|
+
*/
|
|
68
|
+
async getReleaseByTag(kit: KitConfig, tag: string): Promise<GitHubRelease> {
|
|
69
|
+
try {
|
|
70
|
+
const client = await this.getClient();
|
|
71
|
+
|
|
72
|
+
logger.debug(`Fetching release ${tag} for ${kit.owner}/${kit.repo}`);
|
|
73
|
+
|
|
74
|
+
const { data } = await client.repos.getReleaseByTag({
|
|
75
|
+
owner: kit.owner,
|
|
76
|
+
repo: kit.repo,
|
|
77
|
+
tag,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return GitHubReleaseSchema.parse(data);
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
if (error?.status === 404) {
|
|
83
|
+
throw new GitHubError(`Release ${tag} not found for ${kit.name}`, 404);
|
|
84
|
+
}
|
|
85
|
+
if (error?.status === 401) {
|
|
86
|
+
throw new GitHubError("Authentication failed. Please check your GitHub token.", 401);
|
|
87
|
+
}
|
|
88
|
+
if (error?.status === 403) {
|
|
89
|
+
throw new GitHubError(
|
|
90
|
+
"Access denied. Make sure your token has access to private repositories.",
|
|
91
|
+
403,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
throw new GitHubError(
|
|
95
|
+
`Failed to fetch release: ${error?.message || "Unknown error"}`,
|
|
96
|
+
error?.status,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* List all releases for a kit
|
|
103
|
+
*/
|
|
104
|
+
async listReleases(kit: KitConfig, limit = 10): Promise<GitHubRelease[]> {
|
|
105
|
+
try {
|
|
106
|
+
const client = await this.getClient();
|
|
107
|
+
|
|
108
|
+
logger.debug(`Listing releases for ${kit.owner}/${kit.repo}`);
|
|
109
|
+
|
|
110
|
+
const { data } = await client.repos.listReleases({
|
|
111
|
+
owner: kit.owner,
|
|
112
|
+
repo: kit.repo,
|
|
113
|
+
per_page: limit,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return data.map((release) => GitHubReleaseSchema.parse(release));
|
|
117
|
+
} catch (error: any) {
|
|
118
|
+
if (error?.status === 401) {
|
|
119
|
+
throw new GitHubError("Authentication failed. Please check your GitHub token.", 401);
|
|
120
|
+
}
|
|
121
|
+
if (error?.status === 403) {
|
|
122
|
+
throw new GitHubError(
|
|
123
|
+
"Access denied. Make sure your token has access to private repositories.",
|
|
124
|
+
403,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
throw new GitHubError(
|
|
128
|
+
`Failed to list releases: ${error?.message || "Unknown error"}`,
|
|
129
|
+
error?.status,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if user has access to repository
|
|
136
|
+
*/
|
|
137
|
+
async checkAccess(kit: KitConfig): Promise<boolean> {
|
|
138
|
+
try {
|
|
139
|
+
const client = await this.getClient();
|
|
140
|
+
|
|
141
|
+
await client.repos.get({
|
|
142
|
+
owner: kit.owner,
|
|
143
|
+
repo: kit.repo,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return true;
|
|
147
|
+
} catch (error: any) {
|
|
148
|
+
if (error?.status === 404 || error?.status === 403) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
throw new GitHubError(
|
|
152
|
+
`Failed to check repository access: ${error?.message || "Unknown error"}`,
|
|
153
|
+
error?.status,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
package/src/lib/merge.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { join, relative } from "node:path";
|
|
2
|
+
import * as clack from "@clack/prompts";
|
|
3
|
+
import { copy, pathExists, readdir, stat } from "fs-extra";
|
|
4
|
+
import ignore from "ignore";
|
|
5
|
+
import { PROTECTED_PATTERNS } from "../types.js";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
|
|
8
|
+
export class FileMerger {
|
|
9
|
+
private ig = ignore().add(PROTECTED_PATTERNS);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Merge files from source to destination with conflict detection
|
|
13
|
+
*/
|
|
14
|
+
async merge(sourceDir: string, destDir: string, skipConfirmation = false): Promise<void> {
|
|
15
|
+
// Get list of files that will be affected
|
|
16
|
+
const conflicts = await this.detectConflicts(sourceDir, destDir);
|
|
17
|
+
|
|
18
|
+
if (conflicts.length > 0 && !skipConfirmation) {
|
|
19
|
+
logger.warning(`Found ${conflicts.length} file(s) that will be overwritten:`);
|
|
20
|
+
conflicts.slice(0, 10).forEach((file) => logger.info(` - ${file}`));
|
|
21
|
+
if (conflicts.length > 10) {
|
|
22
|
+
logger.info(` ... and ${conflicts.length - 10} more`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const confirm = await clack.confirm({
|
|
26
|
+
message: "Do you want to continue?",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (clack.isCancel(confirm) || !confirm) {
|
|
30
|
+
throw new Error("Merge cancelled by user");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Copy files
|
|
35
|
+
await this.copyFiles(sourceDir, destDir);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Detect files that will be overwritten
|
|
40
|
+
*/
|
|
41
|
+
private async detectConflicts(sourceDir: string, destDir: string): Promise<string[]> {
|
|
42
|
+
const conflicts: string[] = [];
|
|
43
|
+
const files = await this.getFiles(sourceDir);
|
|
44
|
+
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const relativePath = relative(sourceDir, file);
|
|
47
|
+
|
|
48
|
+
// Skip protected files
|
|
49
|
+
if (this.ig.ignores(relativePath)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const destPath = join(destDir, relativePath);
|
|
54
|
+
if (await pathExists(destPath)) {
|
|
55
|
+
conflicts.push(relativePath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return conflicts;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Copy files from source to destination, skipping protected patterns
|
|
64
|
+
*/
|
|
65
|
+
private async copyFiles(sourceDir: string, destDir: string): Promise<void> {
|
|
66
|
+
const files = await this.getFiles(sourceDir);
|
|
67
|
+
let copiedCount = 0;
|
|
68
|
+
let skippedCount = 0;
|
|
69
|
+
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const relativePath = relative(sourceDir, file);
|
|
72
|
+
|
|
73
|
+
// Skip protected files
|
|
74
|
+
if (this.ig.ignores(relativePath)) {
|
|
75
|
+
logger.debug(`Skipping protected file: ${relativePath}`);
|
|
76
|
+
skippedCount++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const destPath = join(destDir, relativePath);
|
|
81
|
+
await copy(file, destPath, { overwrite: true });
|
|
82
|
+
copiedCount++;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
logger.success(`Copied ${copiedCount} file(s), skipped ${skippedCount} protected file(s)`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Recursively get all files in a directory
|
|
90
|
+
*/
|
|
91
|
+
private async getFiles(dir: string): Promise<string[]> {
|
|
92
|
+
const files: string[] = [];
|
|
93
|
+
const entries = await readdir(dir);
|
|
94
|
+
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const fullPath = join(dir, entry);
|
|
97
|
+
const stats = await stat(fullPath);
|
|
98
|
+
|
|
99
|
+
if (stats.isDirectory()) {
|
|
100
|
+
const subFiles = await this.getFiles(fullPath);
|
|
101
|
+
files.push(...subFiles);
|
|
102
|
+
} else {
|
|
103
|
+
files.push(fullPath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return files;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Add custom patterns to ignore
|
|
112
|
+
*/
|
|
113
|
+
addIgnorePatterns(patterns: string[]): void {
|
|
114
|
+
this.ig.add(patterns);
|
|
115
|
+
}
|
|
116
|
+
}
|