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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as clack from "@clack/prompts";
|
|
2
|
+
import { AVAILABLE_KITS, type KitType } from "../types.js";
|
|
3
|
+
|
|
4
|
+
export class PromptsManager {
|
|
5
|
+
/**
|
|
6
|
+
* Prompt user to select a kit
|
|
7
|
+
*/
|
|
8
|
+
async selectKit(defaultKit?: KitType): Promise<KitType> {
|
|
9
|
+
const kit = await clack.select({
|
|
10
|
+
message: "Select a ClaudeKit:",
|
|
11
|
+
options: Object.entries(AVAILABLE_KITS).map(([key, config]) => ({
|
|
12
|
+
value: key as KitType,
|
|
13
|
+
label: config.name,
|
|
14
|
+
hint: config.description,
|
|
15
|
+
})),
|
|
16
|
+
initialValue: defaultKit,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (clack.isCancel(kit)) {
|
|
20
|
+
throw new Error("Kit selection cancelled");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return kit as KitType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Prompt user to select a version
|
|
28
|
+
*/
|
|
29
|
+
async selectVersion(versions: string[], defaultVersion?: string): Promise<string> {
|
|
30
|
+
if (versions.length === 0) {
|
|
31
|
+
throw new Error("No versions available");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If only one version or default is latest, return first version
|
|
35
|
+
if (versions.length === 1 || !defaultVersion) {
|
|
36
|
+
return versions[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const version = await clack.select({
|
|
40
|
+
message: "Select a version:",
|
|
41
|
+
options: versions.map((v) => ({
|
|
42
|
+
value: v,
|
|
43
|
+
label: v,
|
|
44
|
+
})),
|
|
45
|
+
initialValue: defaultVersion,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (clack.isCancel(version)) {
|
|
49
|
+
throw new Error("Version selection cancelled");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return version as string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Prompt user for target directory
|
|
57
|
+
*/
|
|
58
|
+
async getDirectory(defaultDir = "."): Promise<string> {
|
|
59
|
+
const dir = await clack.text({
|
|
60
|
+
message: "Enter target directory:",
|
|
61
|
+
placeholder: defaultDir,
|
|
62
|
+
defaultValue: defaultDir,
|
|
63
|
+
validate: (value) => {
|
|
64
|
+
if (!value || value.trim().length === 0) {
|
|
65
|
+
return "Directory path is required";
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (clack.isCancel(dir)) {
|
|
72
|
+
throw new Error("Directory input cancelled");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return dir.trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Confirm action
|
|
80
|
+
*/
|
|
81
|
+
async confirm(message: string): Promise<boolean> {
|
|
82
|
+
const result = await clack.confirm({
|
|
83
|
+
message,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (clack.isCancel(result)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Show intro message
|
|
95
|
+
*/
|
|
96
|
+
intro(message: string): void {
|
|
97
|
+
clack.intro(message);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Show outro message
|
|
102
|
+
*/
|
|
103
|
+
outro(message: string): void {
|
|
104
|
+
clack.outro(message);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Show note
|
|
109
|
+
*/
|
|
110
|
+
note(message: string, title?: string): void {
|
|
111
|
+
clack.note(message, title);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Kit types
|
|
4
|
+
export const KitType = z.enum(["engineer", "marketing"]);
|
|
5
|
+
export type KitType = z.infer<typeof KitType>;
|
|
6
|
+
|
|
7
|
+
// Command options schemas
|
|
8
|
+
export const NewCommandOptionsSchema = z.object({
|
|
9
|
+
dir: z.string().default("."),
|
|
10
|
+
kit: KitType.optional(),
|
|
11
|
+
version: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
export type NewCommandOptions = z.infer<typeof NewCommandOptionsSchema>;
|
|
14
|
+
|
|
15
|
+
export const UpdateCommandOptionsSchema = z.object({
|
|
16
|
+
dir: z.string().default("."),
|
|
17
|
+
kit: KitType.optional(),
|
|
18
|
+
version: z.string().optional(),
|
|
19
|
+
});
|
|
20
|
+
export type UpdateCommandOptions = z.infer<typeof UpdateCommandOptionsSchema>;
|
|
21
|
+
|
|
22
|
+
// Config schemas
|
|
23
|
+
export const ConfigSchema = z.object({
|
|
24
|
+
github: z
|
|
25
|
+
.object({
|
|
26
|
+
token: z.string().optional(),
|
|
27
|
+
})
|
|
28
|
+
.optional(),
|
|
29
|
+
defaults: z
|
|
30
|
+
.object({
|
|
31
|
+
kit: KitType.optional(),
|
|
32
|
+
dir: z.string().optional(),
|
|
33
|
+
})
|
|
34
|
+
.optional(),
|
|
35
|
+
});
|
|
36
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
37
|
+
|
|
38
|
+
// GitHub schemas
|
|
39
|
+
export const GitHubReleaseAssetSchema = z.object({
|
|
40
|
+
id: z.number(),
|
|
41
|
+
name: z.string(),
|
|
42
|
+
browser_download_url: z.string().url(),
|
|
43
|
+
size: z.number(),
|
|
44
|
+
content_type: z.string(),
|
|
45
|
+
});
|
|
46
|
+
export type GitHubReleaseAsset = z.infer<typeof GitHubReleaseAssetSchema>;
|
|
47
|
+
|
|
48
|
+
export const GitHubReleaseSchema = z.object({
|
|
49
|
+
id: z.number(),
|
|
50
|
+
tag_name: z.string(),
|
|
51
|
+
name: z.string(),
|
|
52
|
+
draft: z.boolean(),
|
|
53
|
+
prerelease: z.boolean(),
|
|
54
|
+
assets: z.array(GitHubReleaseAssetSchema),
|
|
55
|
+
published_at: z.string().optional(),
|
|
56
|
+
});
|
|
57
|
+
export type GitHubRelease = z.infer<typeof GitHubReleaseSchema>;
|
|
58
|
+
|
|
59
|
+
// Kit configuration
|
|
60
|
+
export const KitConfigSchema = z.object({
|
|
61
|
+
name: z.string(),
|
|
62
|
+
repo: z.string(),
|
|
63
|
+
owner: z.string(),
|
|
64
|
+
description: z.string(),
|
|
65
|
+
});
|
|
66
|
+
export type KitConfig = z.infer<typeof KitConfigSchema>;
|
|
67
|
+
|
|
68
|
+
// Available kits
|
|
69
|
+
export const AVAILABLE_KITS: Record<KitType, KitConfig> = {
|
|
70
|
+
engineer: {
|
|
71
|
+
name: "ClaudeKit Engineer",
|
|
72
|
+
repo: "claudekit-engineer",
|
|
73
|
+
owner: "mrgoonie",
|
|
74
|
+
description: "Engineering toolkit for building with Claude",
|
|
75
|
+
},
|
|
76
|
+
marketing: {
|
|
77
|
+
name: "ClaudeKit Marketing",
|
|
78
|
+
repo: "claudekit-marketing",
|
|
79
|
+
owner: "mrgoonie",
|
|
80
|
+
description: "[Coming Soon] Marketing toolkit",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Protected file patterns (files to skip during update)
|
|
85
|
+
export const PROTECTED_PATTERNS = [
|
|
86
|
+
".env",
|
|
87
|
+
".env.local",
|
|
88
|
+
".env.*.local",
|
|
89
|
+
"*.key",
|
|
90
|
+
"*.pem",
|
|
91
|
+
"*.p12",
|
|
92
|
+
"node_modules/**",
|
|
93
|
+
".git/**",
|
|
94
|
+
"dist/**",
|
|
95
|
+
"build/**",
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
// Archive types
|
|
99
|
+
export type ArchiveType = "tar.gz" | "zip";
|
|
100
|
+
|
|
101
|
+
// Download progress
|
|
102
|
+
export interface DownloadProgress {
|
|
103
|
+
total: number;
|
|
104
|
+
current: number;
|
|
105
|
+
percentage: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Authentication method
|
|
109
|
+
export type AuthMethod = "gh-cli" | "env-var" | "keychain" | "prompt";
|
|
110
|
+
|
|
111
|
+
// Error types
|
|
112
|
+
export class ClaudeKitError extends Error {
|
|
113
|
+
constructor(
|
|
114
|
+
message: string,
|
|
115
|
+
public code?: string,
|
|
116
|
+
public statusCode?: number,
|
|
117
|
+
) {
|
|
118
|
+
super(message);
|
|
119
|
+
this.name = "ClaudeKitError";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class AuthenticationError extends ClaudeKitError {
|
|
124
|
+
constructor(message: string) {
|
|
125
|
+
super(message, "AUTH_ERROR", 401);
|
|
126
|
+
this.name = "AuthenticationError";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export class GitHubError extends ClaudeKitError {
|
|
131
|
+
constructor(message: string, statusCode?: number) {
|
|
132
|
+
super(message, "GITHUB_ERROR", statusCode);
|
|
133
|
+
this.name = "GitHubError";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class DownloadError extends ClaudeKitError {
|
|
138
|
+
constructor(message: string) {
|
|
139
|
+
super(message, "DOWNLOAD_ERROR");
|
|
140
|
+
this.name = "DownloadError";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class ExtractionError extends ClaudeKitError {
|
|
145
|
+
constructor(message: string) {
|
|
146
|
+
super(message, "EXTRACTION_ERROR");
|
|
147
|
+
this.name = "ExtractionError";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { type Config, ConfigSchema } from "../types.js";
|
|
6
|
+
import { logger } from "./logger.js";
|
|
7
|
+
|
|
8
|
+
const CONFIG_DIR = join(homedir(), ".claudekit");
|
|
9
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
10
|
+
|
|
11
|
+
export class ConfigManager {
|
|
12
|
+
private static config: Config | null = null;
|
|
13
|
+
|
|
14
|
+
static async load(): Promise<Config> {
|
|
15
|
+
if (ConfigManager.config) {
|
|
16
|
+
return ConfigManager.config;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
if (existsSync(CONFIG_FILE)) {
|
|
21
|
+
const content = await readFile(CONFIG_FILE, "utf-8");
|
|
22
|
+
const data = JSON.parse(content);
|
|
23
|
+
ConfigManager.config = ConfigSchema.parse(data);
|
|
24
|
+
logger.debug(`Config loaded from ${CONFIG_FILE}`);
|
|
25
|
+
return ConfigManager.config;
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
logger.warning(
|
|
29
|
+
`Failed to load config: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Return default config
|
|
34
|
+
ConfigManager.config = { github: {}, defaults: {} };
|
|
35
|
+
return ConfigManager.config;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static async save(config: Config): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
// Validate config
|
|
41
|
+
const validConfig = ConfigSchema.parse(config);
|
|
42
|
+
|
|
43
|
+
// Ensure config directory exists
|
|
44
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
45
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Write config file
|
|
49
|
+
await writeFile(CONFIG_FILE, JSON.stringify(validConfig, null, 2), "utf-8");
|
|
50
|
+
ConfigManager.config = validConfig;
|
|
51
|
+
logger.debug(`Config saved to ${CONFIG_FILE}`);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Failed to save config: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static async get(): Promise<Config> {
|
|
60
|
+
return ConfigManager.load();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static async set(key: string, value: unknown): Promise<void> {
|
|
64
|
+
const config = await ConfigManager.load();
|
|
65
|
+
const keys = key.split(".");
|
|
66
|
+
let current: any = config;
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
69
|
+
if (!(keys[i] in current)) {
|
|
70
|
+
current[keys[i]] = {};
|
|
71
|
+
}
|
|
72
|
+
current = current[keys[i]];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
current[keys[keys.length - 1]] = value;
|
|
76
|
+
await ConfigManager.save(config);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static async getToken(): Promise<string | undefined> {
|
|
80
|
+
const config = await ConfigManager.load();
|
|
81
|
+
return config.github?.token;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static async setToken(token: string): Promise<void> {
|
|
85
|
+
await ConfigManager.set("github.token", token);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
info: (message: string) => {
|
|
5
|
+
console.log(pc.blue("ℹ"), message);
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
success: (message: string) => {
|
|
9
|
+
console.log(pc.green("✔"), message);
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
warning: (message: string) => {
|
|
13
|
+
console.log(pc.yellow("⚠"), message);
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
error: (message: string) => {
|
|
17
|
+
console.error(pc.red("✖"), message);
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
debug: (message: string) => {
|
|
21
|
+
if (process.env.DEBUG) {
|
|
22
|
+
console.log(pc.gray("[DEBUG]"), message);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Sanitize sensitive data from logs
|
|
27
|
+
sanitize: (text: string): string => {
|
|
28
|
+
// Remove GitHub tokens
|
|
29
|
+
return text
|
|
30
|
+
.replace(/ghp_[a-zA-Z0-9]{36}/g, "ghp_***")
|
|
31
|
+
.replace(/github_pat_[a-zA-Z0-9_]{82}/g, "github_pat_***")
|
|
32
|
+
.replace(/gho_[a-zA-Z0-9]{36}/g, "gho_***")
|
|
33
|
+
.replace(/ghu_[a-zA-Z0-9]{36}/g, "ghu_***")
|
|
34
|
+
.replace(/ghs_[a-zA-Z0-9]{36}/g, "ghs_***")
|
|
35
|
+
.replace(/ghr_[a-zA-Z0-9]{36}/g, "ghr_***");
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { AuthManager } from "../../src/lib/auth.js";
|
|
3
|
+
import { AuthenticationError } from "../../src/types.js";
|
|
4
|
+
|
|
5
|
+
describe("AuthManager", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// Reset AuthManager state
|
|
8
|
+
(AuthManager as any).token = null;
|
|
9
|
+
(AuthManager as any).authMethod = null;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
// Clean up environment variables
|
|
14
|
+
process.env.GITHUB_TOKEN = undefined;
|
|
15
|
+
process.env.GH_TOKEN = undefined;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("isValidTokenFormat", () => {
|
|
19
|
+
test("should accept ghp_ tokens", () => {
|
|
20
|
+
expect(AuthManager.isValidTokenFormat("ghp_1234567890")).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("should accept github_pat_ tokens", () => {
|
|
24
|
+
expect(AuthManager.isValidTokenFormat("github_pat_1234567890")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("should reject invalid token formats", () => {
|
|
28
|
+
expect(AuthManager.isValidTokenFormat("invalid_token")).toBe(false);
|
|
29
|
+
expect(AuthManager.isValidTokenFormat("gho_1234567890")).toBe(false);
|
|
30
|
+
expect(AuthManager.isValidTokenFormat("")).toBe(false);
|
|
31
|
+
expect(AuthManager.isValidTokenFormat("token123")).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("should handle empty and malformed tokens", () => {
|
|
35
|
+
expect(AuthManager.isValidTokenFormat("")).toBe(false);
|
|
36
|
+
expect(AuthManager.isValidTokenFormat("ghp")).toBe(false);
|
|
37
|
+
expect(AuthManager.isValidTokenFormat("github_pat")).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("getToken - environment variables", () => {
|
|
42
|
+
test("should get token from environment (gh-cli, env-var, or cached)", async () => {
|
|
43
|
+
// Set environment variable to avoid prompting in CI
|
|
44
|
+
process.env.GITHUB_TOKEN = "ghp_test_token_ci_123";
|
|
45
|
+
|
|
46
|
+
// This test acknowledges that the token can come from multiple sources
|
|
47
|
+
// in the fallback chain: gh-cli > env-var > config > keychain > prompt
|
|
48
|
+
const result = await AuthManager.getToken();
|
|
49
|
+
|
|
50
|
+
expect(result.token).toBeDefined();
|
|
51
|
+
expect(result.token.length).toBeGreaterThan(0);
|
|
52
|
+
expect(result.method).toBeDefined();
|
|
53
|
+
// Method could be 'gh-cli', 'env-var', 'keychain', or 'prompt'
|
|
54
|
+
expect(["gh-cli", "env-var", "keychain", "prompt"]).toContain(result.method);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("should cache token after first retrieval", async () => {
|
|
58
|
+
// Set environment variable to avoid prompting in CI
|
|
59
|
+
process.env.GITHUB_TOKEN = "ghp_test_token_cache_456";
|
|
60
|
+
|
|
61
|
+
// Clear cache first
|
|
62
|
+
(AuthManager as any).token = null;
|
|
63
|
+
(AuthManager as any).authMethod = null;
|
|
64
|
+
|
|
65
|
+
const result1 = await AuthManager.getToken();
|
|
66
|
+
const result2 = await AuthManager.getToken();
|
|
67
|
+
|
|
68
|
+
expect(result1.token).toBe(result2.token);
|
|
69
|
+
expect(result1.method).toBe(result2.method);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("should handle GITHUB_TOKEN env var when gh-cli is not available", async () => {
|
|
73
|
+
// Note: If gh CLI is installed and authenticated, it will take precedence
|
|
74
|
+
// This test documents the expected behavior but may not enforce it
|
|
75
|
+
process.env.GITHUB_TOKEN = "ghp_test_token_123";
|
|
76
|
+
|
|
77
|
+
// Clear cache
|
|
78
|
+
(AuthManager as any).token = null;
|
|
79
|
+
(AuthManager as any).authMethod = null;
|
|
80
|
+
|
|
81
|
+
const result = await AuthManager.getToken();
|
|
82
|
+
|
|
83
|
+
// Token should either be from gh-cli or env-var
|
|
84
|
+
expect(result.token).toBeDefined();
|
|
85
|
+
expect(["gh-cli", "env-var"]).toContain(result.method);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("should handle GH_TOKEN env var when GITHUB_TOKEN is not set", async () => {
|
|
89
|
+
process.env.GITHUB_TOKEN = undefined;
|
|
90
|
+
process.env.GH_TOKEN = "ghp_test_token_456";
|
|
91
|
+
|
|
92
|
+
// Clear cache
|
|
93
|
+
(AuthManager as any).token = null;
|
|
94
|
+
(AuthManager as any).authMethod = null;
|
|
95
|
+
|
|
96
|
+
const result = await AuthManager.getToken();
|
|
97
|
+
|
|
98
|
+
// Token should either be from gh-cli or env-var
|
|
99
|
+
expect(result.token).toBeDefined();
|
|
100
|
+
expect(["gh-cli", "env-var"]).toContain(result.method);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("clearToken", () => {
|
|
105
|
+
test("should clear cached token", async () => {
|
|
106
|
+
// Set a cached token
|
|
107
|
+
(AuthManager as any).token = "test-token";
|
|
108
|
+
(AuthManager as any).authMethod = "env-var";
|
|
109
|
+
|
|
110
|
+
await AuthManager.clearToken();
|
|
111
|
+
|
|
112
|
+
expect((AuthManager as any).token).toBeNull();
|
|
113
|
+
expect((AuthManager as any).authMethod).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
import { DownloadManager } from "../../src/lib/download.js";
|
|
5
|
+
import { DownloadError, ExtractionError } from "../../src/types.js";
|
|
6
|
+
|
|
7
|
+
describe("DownloadManager", () => {
|
|
8
|
+
let manager: DownloadManager;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
manager = new DownloadManager();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("constructor", () => {
|
|
15
|
+
test("should create DownloadManager instance", () => {
|
|
16
|
+
expect(manager).toBeInstanceOf(DownloadManager);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("createTempDir", () => {
|
|
21
|
+
test("should create temporary directory", async () => {
|
|
22
|
+
const tempDir = await manager.createTempDir();
|
|
23
|
+
|
|
24
|
+
expect(tempDir).toBeDefined();
|
|
25
|
+
expect(typeof tempDir).toBe("string");
|
|
26
|
+
expect(tempDir).toContain("claudekit-");
|
|
27
|
+
expect(existsSync(tempDir)).toBe(true);
|
|
28
|
+
|
|
29
|
+
// Cleanup
|
|
30
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("should create unique directories", async () => {
|
|
34
|
+
const tempDir1 = await manager.createTempDir();
|
|
35
|
+
|
|
36
|
+
// Wait 1ms to ensure different timestamps
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
38
|
+
|
|
39
|
+
const tempDir2 = await manager.createTempDir();
|
|
40
|
+
|
|
41
|
+
expect(tempDir1).not.toBe(tempDir2);
|
|
42
|
+
|
|
43
|
+
// Cleanup
|
|
44
|
+
await rm(tempDir1, { recursive: true, force: true });
|
|
45
|
+
await rm(tempDir2, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("error classes", () => {
|
|
50
|
+
test("DownloadError should store message", () => {
|
|
51
|
+
const error = new DownloadError("Download failed");
|
|
52
|
+
expect(error.message).toBe("Download failed");
|
|
53
|
+
expect(error.code).toBe("DOWNLOAD_ERROR");
|
|
54
|
+
expect(error.name).toBe("DownloadError");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("ExtractionError should store message", () => {
|
|
58
|
+
const error = new ExtractionError("Extraction failed");
|
|
59
|
+
expect(error.message).toBe("Extraction failed");
|
|
60
|
+
expect(error.code).toBe("EXTRACTION_ERROR");
|
|
61
|
+
expect(error.name).toBe("ExtractionError");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Note: Testing actual download and extraction would require:
|
|
66
|
+
// 1. Mock GitHub API responses
|
|
67
|
+
// 2. Test fixture archives
|
|
68
|
+
// 3. Network mocking
|
|
69
|
+
// These are integration tests that would be better suited for e2e testing
|
|
70
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { GitHubClient } from "../../src/lib/github.js";
|
|
3
|
+
import { AVAILABLE_KITS, GitHubError } from "../../src/types.js";
|
|
4
|
+
|
|
5
|
+
describe("GitHubClient", () => {
|
|
6
|
+
let client: GitHubClient;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
client = new GitHubClient();
|
|
10
|
+
// Set environment variable to avoid auth prompts during tests
|
|
11
|
+
process.env.GITHUB_TOKEN = "ghp_test_token_for_testing";
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("constructor", () => {
|
|
15
|
+
test("should create GitHubClient instance", () => {
|
|
16
|
+
expect(client).toBeInstanceOf(GitHubClient);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("error handling", () => {
|
|
21
|
+
test("GitHubError should contain message and status code", () => {
|
|
22
|
+
const error = new GitHubError("Test error", 404);
|
|
23
|
+
expect(error.message).toBe("Test error");
|
|
24
|
+
expect(error.statusCode).toBe(404);
|
|
25
|
+
expect(error.code).toBe("GITHUB_ERROR");
|
|
26
|
+
expect(error.name).toBe("GitHubError");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("GitHubError should work without status code", () => {
|
|
30
|
+
const error = new GitHubError("Test error");
|
|
31
|
+
expect(error.message).toBe("Test error");
|
|
32
|
+
expect(error.statusCode).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("integration scenarios", () => {
|
|
37
|
+
test("should handle kit configuration correctly", () => {
|
|
38
|
+
const engineerKit = AVAILABLE_KITS.engineer;
|
|
39
|
+
expect(engineerKit.owner).toBe("mrgoonie");
|
|
40
|
+
expect(engineerKit.repo).toBe("claudekit-engineer");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("should handle marketing kit configuration", () => {
|
|
44
|
+
const marketingKit = AVAILABLE_KITS.marketing;
|
|
45
|
+
expect(marketingKit.owner).toBe("mrgoonie");
|
|
46
|
+
expect(marketingKit.repo).toBe("claudekit-marketing");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Note: Actual API tests would require mocking Octokit or using a test fixture
|
|
51
|
+
// We're keeping these tests simple to avoid external dependencies
|
|
52
|
+
});
|