claudekit-cli 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.
Files changed (53) hide show
  1. package/.github/workflows/ci.yml +2 -0
  2. package/CHANGELOG.md +12 -0
  3. package/CLAUDE.md +7 -0
  4. package/README.md +20 -2
  5. package/dist/index.js +102 -0
  6. package/package.json +1 -1
  7. package/src/commands/version.ts +135 -0
  8. package/src/index.ts +11 -0
  9. package/src/types.ts +7 -0
  10. package/tests/commands/version.test.ts +297 -0
  11. package/.opencode/agent/code-reviewer.md +0 -141
  12. package/.opencode/agent/debugger.md +0 -74
  13. package/.opencode/agent/docs-manager.md +0 -119
  14. package/.opencode/agent/git-manager.md +0 -60
  15. package/.opencode/agent/planner-researcher.md +0 -100
  16. package/.opencode/agent/planner.md +0 -87
  17. package/.opencode/agent/project-manager.md +0 -113
  18. package/.opencode/agent/researcher.md +0 -173
  19. package/.opencode/agent/solution-brainstormer.md +0 -89
  20. package/.opencode/agent/system-architecture.md +0 -192
  21. package/.opencode/agent/tester.md +0 -96
  22. package/.opencode/agent/ui-ux-designer.md +0 -203
  23. package/.opencode/agent/ui-ux-developer.md +0 -97
  24. package/.opencode/command/cook.md +0 -7
  25. package/.opencode/command/debug.md +0 -10
  26. package/.opencode/command/design/3d.md +0 -65
  27. package/.opencode/command/design/fast.md +0 -18
  28. package/.opencode/command/design/good.md +0 -21
  29. package/.opencode/command/design/screenshot.md +0 -22
  30. package/.opencode/command/design/video.md +0 -22
  31. package/.opencode/command/fix/ci.md +0 -8
  32. package/.opencode/command/fix/fast.md +0 -11
  33. package/.opencode/command/fix/hard.md +0 -15
  34. package/.opencode/command/fix/logs.md +0 -16
  35. package/.opencode/command/fix/test.md +0 -18
  36. package/.opencode/command/fix/types.md +0 -10
  37. package/.opencode/command/git/cm.md +0 -5
  38. package/.opencode/command/git/cp.md +0 -4
  39. package/.opencode/command/plan/ci.md +0 -12
  40. package/.opencode/command/plan/two.md +0 -13
  41. package/.opencode/command/plan.md +0 -10
  42. package/.opencode/command/test.md +0 -7
  43. package/.opencode/command/watzup.md +0 -8
  44. package/plans/251008-claudekit-cli-implementation-plan.md +0 -1469
  45. package/plans/reports/251008-from-code-reviewer-to-developer-review-report.md +0 -864
  46. package/plans/reports/251008-from-tester-to-developer-test-summary-report.md +0 -409
  47. package/plans/reports/251008-researcher-download-extraction-report.md +0 -1377
  48. package/plans/reports/251008-researcher-github-api-report.md +0 -1339
  49. package/plans/research/251008-cli-frameworks-bun-research.md +0 -1051
  50. package/plans/templates/bug-fix-template.md +0 -69
  51. package/plans/templates/feature-implementation-template.md +0 -84
  52. package/plans/templates/refactor-template.md +0 -82
  53. package/plans/templates/template-usage-guide.md +0 -58
@@ -5,10 +5,12 @@ on:
5
5
  branches:
6
6
  - main
7
7
  - master
8
+ - dev
8
9
  push:
9
10
  branches:
10
11
  - main
11
12
  - master
13
+ - dev
12
14
 
13
15
  jobs:
14
16
  test:
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [1.1.0](https://github.com/mrgoonie/claudekit-cli/compare/v1.0.1...v1.1.0) (2025-10-17)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * format package.json keywords array to single line ([c8dd66f](https://github.com/mrgoonie/claudekit-cli/commit/c8dd66faa94a84188790947fe3ee6f562d63cd46))
7
+
8
+
9
+ ### Features
10
+
11
+ * **cli:** add versions command to list available releases ([27fbad1](https://github.com/mrgoonie/claudekit-cli/commit/27fbad1be3b5df90cb85ba9a3dd1b0eeb4fa6125))
12
+
1
13
  ## [1.0.1](https://github.com/mrgoonie/claudekit-cli/compare/v1.0.0...v1.0.1) (2025-10-09)
2
14
 
3
15
 
package/CLAUDE.md CHANGED
@@ -31,3 +31,10 @@ We keep all important docs in `./docs` folder and keep updating them, structure
31
31
  └── project-roadmap.md
32
32
  ```
33
33
 
34
+ ## Related directories
35
+
36
+ - ClaudeKit CLI: current directory
37
+ - ClaudeKit Engineer: `../claudekit-engineer`
38
+ - ClaudeKit Marketing: `../claudekit-marketing`
39
+ - ClaudeKit Website: `../claudekit-web`
40
+ - ClaudeKit Docs: `../claudekit-docs`
package/README.md CHANGED
@@ -94,10 +94,27 @@ ck update --kit engineer
94
94
  ck update --kit engineer --version v1.0.0
95
95
  ```
96
96
 
97
+ ### List Available Versions
98
+
99
+ ```bash
100
+ # Show all available versions for all kits
101
+ ck versions
102
+
103
+ # Filter by specific kit
104
+ ck versions --kit engineer
105
+ ck versions --kit marketing
106
+
107
+ # Show more versions (default: 30)
108
+ ck versions --limit 50
109
+
110
+ # Include prereleases and drafts
111
+ ck versions --all
112
+ ```
113
+
97
114
  ### Other Commands
98
115
 
99
116
  ```bash
100
- # Show version
117
+ # Show CLI version
101
118
  ck --version
102
119
  ck -v
103
120
 
@@ -233,7 +250,8 @@ claudekit-cli/
233
250
  ### 1. Commands
234
251
  - **`ck new`**: Create new project from release
235
252
  - **`ck update`**: Update existing project
236
- - **`ck --version`**: Show version
253
+ - **`ck versions`**: List available versions of ClaudeKit repositories
254
+ - **`ck --version`**: Show CLI version
237
255
  - **`ck --help`**: Show help
238
256
 
239
257
  ### 2. Authentication (Multi-Tier Fallback)
package/dist/index.js CHANGED
@@ -17718,6 +17718,11 @@ var UpdateCommandOptionsSchema = exports_external.object({
17718
17718
  kit: KitType.optional(),
17719
17719
  version: exports_external.string().optional()
17720
17720
  });
17721
+ var VersionCommandOptionsSchema = exports_external.object({
17722
+ kit: KitType.optional(),
17723
+ limit: exports_external.number().optional(),
17724
+ all: exports_external.boolean().optional()
17725
+ });
17721
17726
  var ConfigSchema = exports_external.object({
17722
17727
  github: exports_external.object({
17723
17728
  token: exports_external.string().optional()
@@ -19085,6 +19090,100 @@ Protected files (.env, etc.) were not modified.`, "Update complete");
19085
19090
  }
19086
19091
  }
19087
19092
 
19093
+ // src/commands/version.ts
19094
+ var import_picocolors4 = __toESM(require_picocolors(), 1);
19095
+ function formatRelativeTime(dateString) {
19096
+ if (!dateString)
19097
+ return "Unknown";
19098
+ const date = new Date(dateString);
19099
+ const now = new Date;
19100
+ const diffMs = now.getTime() - date.getTime();
19101
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
19102
+ if (diffDays === 0)
19103
+ return "Today";
19104
+ if (diffDays === 1)
19105
+ return "Yesterday";
19106
+ if (diffDays < 7)
19107
+ return `${diffDays} days ago`;
19108
+ if (diffDays < 30)
19109
+ return `${Math.floor(diffDays / 7)} weeks ago`;
19110
+ if (diffDays < 365)
19111
+ return `${Math.floor(diffDays / 30)} months ago`;
19112
+ return `${Math.floor(diffDays / 365)} years ago`;
19113
+ }
19114
+ function displayKitReleases(kitName, releases) {
19115
+ console.log(`
19116
+ ${import_picocolors4.default.bold(import_picocolors4.default.cyan(kitName))} - Available Versions:
19117
+ `);
19118
+ if (releases.length === 0) {
19119
+ console.log(import_picocolors4.default.dim(" No releases found"));
19120
+ return;
19121
+ }
19122
+ for (const release of releases) {
19123
+ const version = import_picocolors4.default.green(release.tag_name);
19124
+ const name2 = release.name || "No title";
19125
+ const publishedAt = formatRelativeTime(release.published_at);
19126
+ const assetCount = release.assets.length;
19127
+ const badges = [];
19128
+ if (release.prerelease)
19129
+ badges.push(import_picocolors4.default.yellow("[prerelease]"));
19130
+ if (release.draft)
19131
+ badges.push(import_picocolors4.default.gray("[draft]"));
19132
+ const badgeStr = badges.length > 0 ? ` ${badges.join(" ")}` : "";
19133
+ const versionPart = version.padEnd(20);
19134
+ const namePart = name2.length > 40 ? `${name2.slice(0, 37)}...` : name2.padEnd(40);
19135
+ const timePart = import_picocolors4.default.dim(publishedAt.padEnd(20));
19136
+ const assetPart = import_picocolors4.default.dim(`(${assetCount} ${assetCount === 1 ? "asset" : "assets"})`);
19137
+ console.log(` ${versionPart} ${namePart} ${timePart} ${assetPart}${badgeStr}`);
19138
+ }
19139
+ console.log(import_picocolors4.default.dim(`
19140
+ Showing ${releases.length} ${releases.length === 1 ? "release" : "releases"}`));
19141
+ }
19142
+ async function versionCommand(options) {
19143
+ const prompts = new PromptsManager;
19144
+ prompts.intro("\uD83D\uDCE6 ClaudeKit - Available Versions");
19145
+ try {
19146
+ const validOptions = VersionCommandOptionsSchema.parse(options);
19147
+ const kitsToFetch = validOptions.kit ? [validOptions.kit] : Object.keys(AVAILABLE_KITS);
19148
+ const github = new GitHubClient;
19149
+ const limit = validOptions.limit || 30;
19150
+ const releasePromises = kitsToFetch.map(async (kitType) => {
19151
+ const kitConfig = AVAILABLE_KITS[kitType];
19152
+ try {
19153
+ const releases = await github.listReleases(kitConfig, limit);
19154
+ const filteredReleases = validOptions.all ? releases : releases.filter((r2) => !r2.draft && !r2.prerelease);
19155
+ return {
19156
+ kitType,
19157
+ kitConfig,
19158
+ releases: filteredReleases,
19159
+ error: null
19160
+ };
19161
+ } catch (error2) {
19162
+ return {
19163
+ kitType,
19164
+ kitConfig,
19165
+ releases: [],
19166
+ error: error2 instanceof Error ? error2.message : "Unknown error"
19167
+ };
19168
+ }
19169
+ });
19170
+ const results = await Promise.all(releasePromises);
19171
+ for (const result of results) {
19172
+ if (result.error) {
19173
+ console.log(`
19174
+ ${import_picocolors4.default.bold(import_picocolors4.default.cyan(result.kitConfig.name))} - ${import_picocolors4.default.red("Error")}`);
19175
+ console.log(import_picocolors4.default.dim(` ${result.error}`));
19176
+ } else {
19177
+ displayKitReleases(result.kitConfig.name, result.releases);
19178
+ }
19179
+ }
19180
+ prompts.outro("✨ Done");
19181
+ } catch (error2) {
19182
+ logger.error(error2 instanceof Error ? error2.message : "Unknown error occurred");
19183
+ process.exit(1);
19184
+ }
19185
+ }
19186
+
19088
19187
  // src/index.ts
19089
19188
  var __dirname2 = fileURLToPath(new URL(".", import.meta.url));
19090
19189
  var packageJson = JSON.parse(readFileSync(join5(__dirname2, "../package.json"), "utf-8"));
@@ -19095,6 +19194,9 @@ cli.command("new", "Bootstrap a new ClaudeKit project").option("--dir <dir>", "T
19095
19194
  cli.command("update", "Update existing ClaudeKit project").option("--dir <dir>", "Target directory (default: .)").option("--kit <kit>", "Kit to use (engineer, marketing)").option("--version <version>", "Specific version to download (default: latest)").action(async (options) => {
19096
19195
  await updateCommand(options);
19097
19196
  });
19197
+ cli.command("versions", "List available versions of ClaudeKit repositories").option("--kit <kit>", "Filter by specific kit (engineer, marketing)").option("--limit <limit>", "Number of releases to show (default: 30)").option("--all", "Show all releases including prereleases").action(async (options) => {
19198
+ await versionCommand(options);
19199
+ });
19098
19200
  cli.version(packageJson.version);
19099
19201
  cli.help();
19100
19202
  cli.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudekit-cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "CLI tool for bootstrapping and updating ClaudeKit projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,135 @@
1
+ import pc from "picocolors";
2
+ import { GitHubClient } from "../lib/github.js";
3
+ import { PromptsManager } from "../lib/prompts.js";
4
+ import {
5
+ AVAILABLE_KITS,
6
+ type GitHubRelease,
7
+ type VersionCommandOptions,
8
+ VersionCommandOptionsSchema,
9
+ } from "../types.js";
10
+ import { logger } from "../utils/logger.js";
11
+
12
+ /**
13
+ * Format a date as a relative time string
14
+ */
15
+ function formatRelativeTime(dateString?: string): string {
16
+ if (!dateString) return "Unknown";
17
+
18
+ const date = new Date(dateString);
19
+ const now = new Date();
20
+ const diffMs = now.getTime() - date.getTime();
21
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
22
+
23
+ if (diffDays === 0) return "Today";
24
+ if (diffDays === 1) return "Yesterday";
25
+ if (diffDays < 7) return `${diffDays} days ago`;
26
+ if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
27
+ if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
28
+ return `${Math.floor(diffDays / 365)} years ago`;
29
+ }
30
+
31
+ /**
32
+ * Display releases for a single kit
33
+ */
34
+ function displayKitReleases(kitName: string, releases: GitHubRelease[]): void {
35
+ console.log(`\n${pc.bold(pc.cyan(kitName))} - Available Versions:\n`);
36
+
37
+ if (releases.length === 0) {
38
+ console.log(pc.dim(" No releases found"));
39
+ return;
40
+ }
41
+
42
+ for (const release of releases) {
43
+ const version = pc.green(release.tag_name);
44
+ const name = release.name || "No title";
45
+ const publishedAt = formatRelativeTime(release.published_at);
46
+ const assetCount = release.assets.length;
47
+
48
+ // Add badges for prerelease and draft
49
+ const badges: string[] = [];
50
+ if (release.prerelease) badges.push(pc.yellow("[prerelease]"));
51
+ if (release.draft) badges.push(pc.gray("[draft]"));
52
+ const badgeStr = badges.length > 0 ? ` ${badges.join(" ")}` : "";
53
+
54
+ // Format: version | name | time | assets
55
+ const versionPart = version.padEnd(20);
56
+ const namePart = name.length > 40 ? `${name.slice(0, 37)}...` : name.padEnd(40);
57
+ const timePart = pc.dim(publishedAt.padEnd(20));
58
+ const assetPart = pc.dim(`(${assetCount} ${assetCount === 1 ? "asset" : "assets"})`);
59
+
60
+ console.log(` ${versionPart} ${namePart} ${timePart} ${assetPart}${badgeStr}`);
61
+ }
62
+
63
+ console.log(
64
+ pc.dim(`\nShowing ${releases.length} ${releases.length === 1 ? "release" : "releases"}`),
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Version command - List available versions of ClaudeKit repositories
70
+ */
71
+ export async function versionCommand(options: VersionCommandOptions): Promise<void> {
72
+ const prompts = new PromptsManager();
73
+
74
+ prompts.intro("📦 ClaudeKit - Available Versions");
75
+
76
+ try {
77
+ // Validate and parse options
78
+ const validOptions = VersionCommandOptionsSchema.parse(options);
79
+
80
+ // Determine which kits to fetch
81
+ const kitsToFetch = validOptions.kit
82
+ ? [validOptions.kit]
83
+ : (Object.keys(AVAILABLE_KITS) as Array<keyof typeof AVAILABLE_KITS>);
84
+
85
+ // Initialize GitHub client
86
+ const github = new GitHubClient();
87
+
88
+ // Determine limit (default to 30, similar to GitHub CLI)
89
+ const limit = validOptions.limit || 30;
90
+
91
+ // Fetch releases for all requested kits in parallel
92
+ const releasePromises = kitsToFetch.map(async (kitType) => {
93
+ const kitConfig = AVAILABLE_KITS[kitType];
94
+ try {
95
+ const releases = await github.listReleases(kitConfig, limit);
96
+
97
+ // Filter out drafts and prereleases unless --all flag is set
98
+ const filteredReleases = validOptions.all
99
+ ? releases
100
+ : releases.filter((r) => !r.draft && !r.prerelease);
101
+
102
+ return {
103
+ kitType,
104
+ kitConfig,
105
+ releases: filteredReleases,
106
+ error: null,
107
+ };
108
+ } catch (error) {
109
+ return {
110
+ kitType,
111
+ kitConfig,
112
+ releases: [],
113
+ error: error instanceof Error ? error.message : "Unknown error",
114
+ };
115
+ }
116
+ });
117
+
118
+ const results = await Promise.all(releasePromises);
119
+
120
+ // Display results
121
+ for (const result of results) {
122
+ if (result.error) {
123
+ console.log(`\n${pc.bold(pc.cyan(result.kitConfig.name))} - ${pc.red("Error")}`);
124
+ console.log(pc.dim(` ${result.error}`));
125
+ } else {
126
+ displayKitReleases(result.kitConfig.name, result.releases);
127
+ }
128
+ }
129
+
130
+ prompts.outro("✨ Done");
131
+ } catch (error) {
132
+ logger.error(error instanceof Error ? error.message : "Unknown error occurred");
133
+ process.exit(1);
134
+ }
135
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { cac } from "cac";
7
7
  import { newCommand } from "./commands/new.js";
8
8
  import { updateCommand } from "./commands/update.js";
9
+ import { versionCommand } from "./commands/version.js";
9
10
 
10
11
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
11
12
 
@@ -34,6 +35,16 @@ cli
34
35
  await updateCommand(options);
35
36
  });
36
37
 
38
+ // Versions command
39
+ cli
40
+ .command("versions", "List available versions of ClaudeKit repositories")
41
+ .option("--kit <kit>", "Filter by specific kit (engineer, marketing)")
42
+ .option("--limit <limit>", "Number of releases to show (default: 30)")
43
+ .option("--all", "Show all releases including prereleases")
44
+ .action(async (options) => {
45
+ await versionCommand(options);
46
+ });
47
+
37
48
  // Version
38
49
  cli.version(packageJson.version);
39
50
 
package/src/types.ts CHANGED
@@ -19,6 +19,13 @@ export const UpdateCommandOptionsSchema = z.object({
19
19
  });
20
20
  export type UpdateCommandOptions = z.infer<typeof UpdateCommandOptionsSchema>;
21
21
 
22
+ export const VersionCommandOptionsSchema = z.object({
23
+ kit: KitType.optional(),
24
+ limit: z.number().optional(),
25
+ all: z.boolean().optional(),
26
+ });
27
+ export type VersionCommandOptions = z.infer<typeof VersionCommandOptionsSchema>;
28
+
22
29
  // Config schemas
23
30
  export const ConfigSchema = z.object({
24
31
  github: z
@@ -0,0 +1,297 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import type { GitHubRelease } from "../../src/types.js";
3
+ import { AVAILABLE_KITS, VersionCommandOptionsSchema } from "../../src/types.js";
4
+
5
+ describe("Version Command", () => {
6
+ beforeEach(() => {
7
+ // Set environment variable to avoid auth prompts during tests
8
+ process.env.GITHUB_TOKEN = "ghp_test_token_for_testing";
9
+ });
10
+
11
+ describe("VersionCommandOptionsSchema", () => {
12
+ test("should accept valid options with kit filter", () => {
13
+ const options = { kit: "engineer" as const };
14
+ const result = VersionCommandOptionsSchema.parse(options);
15
+ expect(result.kit).toBe("engineer");
16
+ });
17
+
18
+ test("should accept valid options with limit", () => {
19
+ const options = { limit: 10 };
20
+ const result = VersionCommandOptionsSchema.parse(options);
21
+ expect(result.limit).toBe(10);
22
+ });
23
+
24
+ test("should accept valid options with all flag", () => {
25
+ const options = { all: true };
26
+ const result = VersionCommandOptionsSchema.parse(options);
27
+ expect(result.all).toBe(true);
28
+ });
29
+
30
+ test("should accept all options combined", () => {
31
+ const options = { kit: "marketing" as const, limit: 20, all: true };
32
+ const result = VersionCommandOptionsSchema.parse(options);
33
+ expect(result.kit).toBe("marketing");
34
+ expect(result.limit).toBe(20);
35
+ expect(result.all).toBe(true);
36
+ });
37
+
38
+ test("should accept empty options", () => {
39
+ const options = {};
40
+ const result = VersionCommandOptionsSchema.parse(options);
41
+ expect(result.kit).toBeUndefined();
42
+ expect(result.limit).toBeUndefined();
43
+ expect(result.all).toBeUndefined();
44
+ });
45
+
46
+ test("should reject invalid kit type", () => {
47
+ const options = { kit: "invalid" };
48
+ expect(() => VersionCommandOptionsSchema.parse(options)).toThrow();
49
+ });
50
+ });
51
+
52
+ describe("Kit Configuration", () => {
53
+ test("should have engineer kit configured", () => {
54
+ const engineerKit = AVAILABLE_KITS.engineer;
55
+ expect(engineerKit.name).toBe("ClaudeKit Engineer");
56
+ expect(engineerKit.repo).toBe("claudekit-engineer");
57
+ expect(engineerKit.owner).toBe("mrgoonie");
58
+ });
59
+
60
+ test("should have marketing kit configured", () => {
61
+ const marketingKit = AVAILABLE_KITS.marketing;
62
+ expect(marketingKit.name).toBe("ClaudeKit Marketing");
63
+ expect(marketingKit.repo).toBe("claudekit-marketing");
64
+ expect(marketingKit.owner).toBe("mrgoonie");
65
+ });
66
+ });
67
+
68
+ describe("Release Data Handling", () => {
69
+ test("should handle release with all fields", () => {
70
+ const release: GitHubRelease = {
71
+ id: 1,
72
+ tag_name: "v1.0.0",
73
+ name: "Release 1.0.0",
74
+ draft: false,
75
+ prerelease: false,
76
+ assets: [],
77
+ published_at: "2024-01-01T00:00:00Z",
78
+ };
79
+
80
+ expect(release.tag_name).toBe("v1.0.0");
81
+ expect(release.name).toBe("Release 1.0.0");
82
+ expect(release.draft).toBe(false);
83
+ expect(release.prerelease).toBe(false);
84
+ });
85
+
86
+ test("should handle release without published_at", () => {
87
+ const release: GitHubRelease = {
88
+ id: 1,
89
+ tag_name: "v1.0.0",
90
+ name: "Release 1.0.0",
91
+ draft: false,
92
+ prerelease: false,
93
+ assets: [],
94
+ };
95
+
96
+ expect(release.published_at).toBeUndefined();
97
+ });
98
+
99
+ test("should handle draft release", () => {
100
+ const release: GitHubRelease = {
101
+ id: 1,
102
+ tag_name: "v1.0.0-draft",
103
+ name: "Draft Release",
104
+ draft: true,
105
+ prerelease: false,
106
+ assets: [],
107
+ };
108
+
109
+ expect(release.draft).toBe(true);
110
+ });
111
+
112
+ test("should handle prerelease", () => {
113
+ const release: GitHubRelease = {
114
+ id: 1,
115
+ tag_name: "v1.0.0-beta.1",
116
+ name: "Beta Release",
117
+ draft: false,
118
+ prerelease: true,
119
+ assets: [],
120
+ };
121
+
122
+ expect(release.prerelease).toBe(true);
123
+ });
124
+ });
125
+
126
+ describe("Date Formatting", () => {
127
+ test("should format recent dates correctly", () => {
128
+ const now = new Date();
129
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
130
+ const dateString = yesterday.toISOString();
131
+
132
+ // The actual formatting logic is in the command file
133
+ // We just verify the date string is valid
134
+ expect(dateString).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
135
+ });
136
+
137
+ test("should handle undefined date", () => {
138
+ const dateString = undefined;
139
+ expect(dateString).toBeUndefined();
140
+ });
141
+ });
142
+
143
+ describe("Release Filtering", () => {
144
+ const releases: GitHubRelease[] = [
145
+ {
146
+ id: 1,
147
+ tag_name: "v1.0.0",
148
+ name: "Stable Release",
149
+ draft: false,
150
+ prerelease: false,
151
+ assets: [],
152
+ published_at: "2024-01-01T00:00:00Z",
153
+ },
154
+ {
155
+ id: 2,
156
+ tag_name: "v1.1.0-beta.1",
157
+ name: "Beta Release",
158
+ draft: false,
159
+ prerelease: true,
160
+ assets: [],
161
+ published_at: "2024-01-02T00:00:00Z",
162
+ },
163
+ {
164
+ id: 3,
165
+ tag_name: "v1.2.0-draft",
166
+ name: "Draft Release",
167
+ draft: true,
168
+ prerelease: false,
169
+ assets: [],
170
+ published_at: "2024-01-03T00:00:00Z",
171
+ },
172
+ ];
173
+
174
+ test("should filter out drafts by default", () => {
175
+ const stable = releases.filter((r) => !r.draft && !r.prerelease);
176
+ expect(stable).toHaveLength(1);
177
+ expect(stable[0].tag_name).toBe("v1.0.0");
178
+ });
179
+
180
+ test("should filter out prereleases by default", () => {
181
+ const stable = releases.filter((r) => !r.draft && !r.prerelease);
182
+ expect(stable.every((r) => !r.prerelease)).toBe(true);
183
+ });
184
+
185
+ test("should include all when --all flag is used", () => {
186
+ const all = releases; // No filtering when --all is true
187
+ expect(all).toHaveLength(3);
188
+ });
189
+
190
+ test("should handle empty release list", () => {
191
+ const empty: GitHubRelease[] = [];
192
+ expect(empty).toHaveLength(0);
193
+ });
194
+ });
195
+
196
+ describe("Command Options Validation", () => {
197
+ test("should validate limit as number", () => {
198
+ const validLimit = { limit: 50 };
199
+ const result = VersionCommandOptionsSchema.parse(validLimit);
200
+ expect(result.limit).toBe(50);
201
+ });
202
+
203
+ test("should validate all as boolean", () => {
204
+ const validAll = { all: false };
205
+ const result = VersionCommandOptionsSchema.parse(validAll);
206
+ expect(result.all).toBe(false);
207
+ });
208
+
209
+ test("should handle optional fields", () => {
210
+ const minimal = {};
211
+ const result = VersionCommandOptionsSchema.parse(minimal);
212
+ expect(result).toBeDefined();
213
+ });
214
+ });
215
+
216
+ describe("Error Scenarios", () => {
217
+ test("should handle invalid option types", () => {
218
+ const invalidLimit = { limit: "not-a-number" };
219
+ expect(() => VersionCommandOptionsSchema.parse(invalidLimit)).toThrow();
220
+ });
221
+
222
+ test("should handle invalid all flag type", () => {
223
+ const invalidAll = { all: "not-a-boolean" };
224
+ expect(() => VersionCommandOptionsSchema.parse(invalidAll)).toThrow();
225
+ });
226
+ });
227
+
228
+ describe("Assets Handling", () => {
229
+ test("should handle release with multiple assets", () => {
230
+ const release: GitHubRelease = {
231
+ id: 1,
232
+ tag_name: "v1.0.0",
233
+ name: "Release",
234
+ draft: false,
235
+ prerelease: false,
236
+ assets: [
237
+ {
238
+ id: 1,
239
+ name: "package.tar.gz",
240
+ browser_download_url: "https://example.com/package.tar.gz",
241
+ size: 1024,
242
+ content_type: "application/gzip",
243
+ },
244
+ {
245
+ id: 2,
246
+ name: "package.zip",
247
+ browser_download_url: "https://example.com/package.zip",
248
+ size: 2048,
249
+ content_type: "application/zip",
250
+ },
251
+ ],
252
+ };
253
+
254
+ expect(release.assets).toHaveLength(2);
255
+ });
256
+
257
+ test("should handle release with no assets", () => {
258
+ const release: GitHubRelease = {
259
+ id: 1,
260
+ tag_name: "v1.0.0",
261
+ name: "Release",
262
+ draft: false,
263
+ prerelease: false,
264
+ assets: [],
265
+ };
266
+
267
+ expect(release.assets).toHaveLength(0);
268
+ });
269
+ });
270
+
271
+ describe("Integration Scenarios", () => {
272
+ test("should handle both kits in parallel", () => {
273
+ const kits = Object.keys(AVAILABLE_KITS);
274
+ expect(kits).toContain("engineer");
275
+ expect(kits).toContain("marketing");
276
+ expect(kits).toHaveLength(2);
277
+ });
278
+
279
+ test("should support filtering by engineer kit", () => {
280
+ const options = { kit: "engineer" as const };
281
+ const result = VersionCommandOptionsSchema.parse(options);
282
+ expect(result.kit).toBe("engineer");
283
+
284
+ const kitConfig = AVAILABLE_KITS[result.kit];
285
+ expect(kitConfig.repo).toBe("claudekit-engineer");
286
+ });
287
+
288
+ test("should support filtering by marketing kit", () => {
289
+ const options = { kit: "marketing" as const };
290
+ const result = VersionCommandOptionsSchema.parse(options);
291
+ expect(result.kit).toBe("marketing");
292
+
293
+ const kitConfig = AVAILABLE_KITS[result.kit];
294
+ expect(kitConfig.repo).toBe("claudekit-marketing");
295
+ });
296
+ });
297
+ });