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.
Files changed (137) hide show
  1. package/.claude/agents/brainstormer.md +96 -0
  2. package/.claude/agents/code-reviewer.md +141 -0
  3. package/.claude/agents/copywriter.md +108 -0
  4. package/.claude/agents/database-admin.md +86 -0
  5. package/.claude/agents/debugger.md +124 -0
  6. package/.claude/agents/docs-manager.md +115 -0
  7. package/.claude/agents/git-manager.md +60 -0
  8. package/.claude/agents/journal-writer.md +111 -0
  9. package/.claude/agents/planner.md +87 -0
  10. package/.claude/agents/project-manager.md +113 -0
  11. package/.claude/agents/researcher.md +173 -0
  12. package/.claude/agents/scout.md +123 -0
  13. package/.claude/agents/tester.md +95 -0
  14. package/.claude/agents/ui-ux-designer.md +206 -0
  15. package/.claude/commands/bootstrap.md +104 -0
  16. package/.claude/commands/brainstorm.md +67 -0
  17. package/.claude/commands/content/enhance.md +13 -0
  18. package/.claude/commands/content/fast.md +11 -0
  19. package/.claude/commands/content/good.md +13 -0
  20. package/.claude/commands/cook.md +19 -0
  21. package/.claude/commands/debug.md +10 -0
  22. package/.claude/commands/design/3d.md +65 -0
  23. package/.claude/commands/design/describe.md +13 -0
  24. package/.claude/commands/design/fast.md +19 -0
  25. package/.claude/commands/design/good.md +23 -0
  26. package/.claude/commands/design/screenshot.md +23 -0
  27. package/.claude/commands/design/video.md +23 -0
  28. package/.claude/commands/docs/init.md +13 -0
  29. package/.claude/commands/docs/summarize.md +10 -0
  30. package/.claude/commands/docs/update.md +21 -0
  31. package/.claude/commands/fix/ci.md +11 -0
  32. package/.claude/commands/fix/fast.md +12 -0
  33. package/.claude/commands/fix/hard.md +18 -0
  34. package/.claude/commands/fix/logs.md +16 -0
  35. package/.claude/commands/fix/test.md +18 -0
  36. package/.claude/commands/fix/types.md +10 -0
  37. package/.claude/commands/git/cm.md +5 -0
  38. package/.claude/commands/git/cp.md +4 -0
  39. package/.claude/commands/integrate/polar.md +42 -0
  40. package/.claude/commands/plan/ci.md +12 -0
  41. package/.claude/commands/plan/two.md +13 -0
  42. package/.claude/commands/plan.md +10 -0
  43. package/.claude/commands/scout.md +29 -0
  44. package/.claude/commands/test.md +7 -0
  45. package/.claude/commands/watzup.md +8 -0
  46. package/.claude/hooks/telegram_notify.sh +136 -0
  47. package/.claude/send-discord.sh +64 -0
  48. package/.claude/settings.json +7 -0
  49. package/.claude/statusline.sh +143 -0
  50. package/.claude/workflows/development-rules.md +80 -0
  51. package/.claude/workflows/documentation-management.md +28 -0
  52. package/.claude/workflows/orchestration-protocol.md +16 -0
  53. package/.claude/workflows/primary-workflow.md +41 -0
  54. package/.github/workflows/ci.yml +43 -0
  55. package/.github/workflows/release.yml +58 -0
  56. package/.opencode/agent/code-reviewer.md +141 -0
  57. package/.opencode/agent/debugger.md +74 -0
  58. package/.opencode/agent/docs-manager.md +119 -0
  59. package/.opencode/agent/git-manager.md +60 -0
  60. package/.opencode/agent/planner-researcher.md +100 -0
  61. package/.opencode/agent/planner.md +87 -0
  62. package/.opencode/agent/project-manager.md +113 -0
  63. package/.opencode/agent/researcher.md +173 -0
  64. package/.opencode/agent/solution-brainstormer.md +89 -0
  65. package/.opencode/agent/system-architecture.md +192 -0
  66. package/.opencode/agent/tester.md +96 -0
  67. package/.opencode/agent/ui-ux-designer.md +203 -0
  68. package/.opencode/agent/ui-ux-developer.md +97 -0
  69. package/.opencode/command/cook.md +7 -0
  70. package/.opencode/command/debug.md +10 -0
  71. package/.opencode/command/design/3d.md +65 -0
  72. package/.opencode/command/design/fast.md +18 -0
  73. package/.opencode/command/design/good.md +21 -0
  74. package/.opencode/command/design/screenshot.md +22 -0
  75. package/.opencode/command/design/video.md +22 -0
  76. package/.opencode/command/docs/init.md +11 -0
  77. package/.opencode/command/docs/summarize.md +10 -0
  78. package/.opencode/command/docs/update.md +18 -0
  79. package/.opencode/command/fix/ci.md +8 -0
  80. package/.opencode/command/fix/fast.md +11 -0
  81. package/.opencode/command/fix/hard.md +15 -0
  82. package/.opencode/command/fix/logs.md +16 -0
  83. package/.opencode/command/fix/test.md +18 -0
  84. package/.opencode/command/fix/types.md +10 -0
  85. package/.opencode/command/git/cm.md +5 -0
  86. package/.opencode/command/git/cp.md +4 -0
  87. package/.opencode/command/plan/ci.md +12 -0
  88. package/.opencode/command/plan/two.md +13 -0
  89. package/.opencode/command/plan.md +10 -0
  90. package/.opencode/command/test.md +7 -0
  91. package/.opencode/command/watzup.md +8 -0
  92. package/.releaserc.json +17 -0
  93. package/.repomixignore +15 -0
  94. package/AGENTS.md +217 -0
  95. package/CHANGELOG.md +16 -0
  96. package/CLAUDE.md +33 -0
  97. package/README.md +214 -0
  98. package/biome.json +25 -0
  99. package/bun.lock +1238 -0
  100. package/dist/index.js +19100 -0
  101. package/docs/code-standards.md +1128 -0
  102. package/docs/codebase-summary.md +821 -0
  103. package/docs/github-setup.md +176 -0
  104. package/docs/project-pdr.md +739 -0
  105. package/docs/system-architecture.md +950 -0
  106. package/docs/tech-stack.md +290 -0
  107. package/package.json +60 -0
  108. package/plans/251008-claudekit-cli-implementation-plan.md +1469 -0
  109. package/plans/reports/251008-from-code-reviewer-to-developer-review-report.md +864 -0
  110. package/plans/reports/251008-from-tester-to-developer-test-summary-report.md +409 -0
  111. package/plans/reports/251008-researcher-download-extraction-report.md +1377 -0
  112. package/plans/reports/251008-researcher-github-api-report.md +1339 -0
  113. package/plans/research/251008-cli-frameworks-bun-research.md +1051 -0
  114. package/plans/templates/bug-fix-template.md +69 -0
  115. package/plans/templates/feature-implementation-template.md +84 -0
  116. package/plans/templates/refactor-template.md +82 -0
  117. package/plans/templates/template-usage-guide.md +58 -0
  118. package/src/commands/new.ts +118 -0
  119. package/src/commands/update.ts +114 -0
  120. package/src/index.ts +44 -0
  121. package/src/lib/auth.ts +157 -0
  122. package/src/lib/download.ts +180 -0
  123. package/src/lib/github.ts +157 -0
  124. package/src/lib/merge.ts +116 -0
  125. package/src/lib/prompts.ts +113 -0
  126. package/src/types.ts +149 -0
  127. package/src/utils/config.ts +87 -0
  128. package/src/utils/logger.ts +37 -0
  129. package/tests/lib/auth.test.ts +116 -0
  130. package/tests/lib/download.test.ts +70 -0
  131. package/tests/lib/github.test.ts +52 -0
  132. package/tests/lib/merge.test.ts +138 -0
  133. package/tests/lib/prompts.test.ts +66 -0
  134. package/tests/types.test.ts +255 -0
  135. package/tests/utils/config.test.ts +263 -0
  136. package/tests/utils/logger.test.ts +124 -0
  137. 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
+ });