@aicgen/aicgen 1.0.0-beta.1 → 1.0.0-beta.2

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 (136) hide show
  1. package/.vs/ProjectSettings.json +2 -2
  2. package/.vs/VSWorkspaceState.json +15 -15
  3. package/.vs/aicgen.slnx/v18/DocumentLayout.json +53 -53
  4. package/assets/icon.svg +33 -33
  5. package/bun.lock +0 -43
  6. package/data/architecture/microservices/api-gateway.md +56 -56
  7. package/data/devops/observability.md +73 -73
  8. package/dist/index.js +9299 -9299
  9. package/package.json +2 -2
  10. package/.claude/agents/architecture-reviewer.md +0 -88
  11. package/.claude/agents/guideline-checker.md +0 -73
  12. package/.claude/agents/security-auditor.md +0 -108
  13. package/.claude/guidelines/api-design.md +0 -645
  14. package/.claude/guidelines/architecture.md +0 -2503
  15. package/.claude/guidelines/best-practices.md +0 -618
  16. package/.claude/guidelines/code-style.md +0 -304
  17. package/.claude/guidelines/design-patterns.md +0 -573
  18. package/.claude/guidelines/devops.md +0 -226
  19. package/.claude/guidelines/error-handling.md +0 -413
  20. package/.claude/guidelines/language.md +0 -782
  21. package/.claude/guidelines/performance.md +0 -706
  22. package/.claude/guidelines/security.md +0 -583
  23. package/.claude/guidelines/testing.md +0 -568
  24. package/.claude/settings.json +0 -98
  25. package/.claude/settings.local.json +0 -8
  26. package/.eslintrc.json +0 -28
  27. package/.github/workflows/release.yml +0 -180
  28. package/.github/workflows/test.yml +0 -81
  29. package/CONTRIBUTING.md +0 -821
  30. package/dist/commands/init.d.ts +0 -8
  31. package/dist/commands/init.d.ts.map +0 -1
  32. package/dist/commands/init.js +0 -46
  33. package/dist/commands/init.js.map +0 -1
  34. package/dist/config/profiles.d.ts +0 -4
  35. package/dist/config/profiles.d.ts.map +0 -1
  36. package/dist/config/profiles.js +0 -30
  37. package/dist/config/profiles.js.map +0 -1
  38. package/dist/config/settings.d.ts +0 -7
  39. package/dist/config/settings.d.ts.map +0 -1
  40. package/dist/config/settings.js +0 -7
  41. package/dist/config/settings.js.map +0 -1
  42. package/dist/index.d.ts +0 -3
  43. package/dist/index.d.ts.map +0 -1
  44. package/dist/index.js.map +0 -1
  45. package/dist/models/guideline.d.ts +0 -15
  46. package/dist/models/guideline.d.ts.map +0 -1
  47. package/dist/models/guideline.js +0 -2
  48. package/dist/models/guideline.js.map +0 -1
  49. package/dist/models/preference.d.ts +0 -9
  50. package/dist/models/preference.d.ts.map +0 -1
  51. package/dist/models/preference.js +0 -2
  52. package/dist/models/preference.js.map +0 -1
  53. package/dist/models/profile.d.ts +0 -9
  54. package/dist/models/profile.d.ts.map +0 -1
  55. package/dist/models/profile.js +0 -2
  56. package/dist/models/profile.js.map +0 -1
  57. package/dist/models/project.d.ts +0 -13
  58. package/dist/models/project.d.ts.map +0 -1
  59. package/dist/models/project.js +0 -2
  60. package/dist/models/project.js.map +0 -1
  61. package/dist/services/ai/anthropic.d.ts +0 -7
  62. package/dist/services/ai/anthropic.d.ts.map +0 -1
  63. package/dist/services/ai/anthropic.js +0 -39
  64. package/dist/services/ai/anthropic.js.map +0 -1
  65. package/dist/services/generator.d.ts +0 -2
  66. package/dist/services/generator.d.ts.map +0 -1
  67. package/dist/services/generator.js +0 -4
  68. package/dist/services/generator.js.map +0 -1
  69. package/dist/services/learner.d.ts +0 -2
  70. package/dist/services/learner.d.ts.map +0 -1
  71. package/dist/services/learner.js +0 -4
  72. package/dist/services/learner.js.map +0 -1
  73. package/dist/services/scanner.d.ts +0 -3
  74. package/dist/services/scanner.d.ts.map +0 -1
  75. package/dist/services/scanner.js +0 -54
  76. package/dist/services/scanner.js.map +0 -1
  77. package/dist/utils/errors.d.ts +0 -15
  78. package/dist/utils/errors.d.ts.map +0 -1
  79. package/dist/utils/errors.js +0 -27
  80. package/dist/utils/errors.js.map +0 -1
  81. package/dist/utils/file.d.ts +0 -7
  82. package/dist/utils/file.d.ts.map +0 -1
  83. package/dist/utils/file.js +0 -32
  84. package/dist/utils/file.js.map +0 -1
  85. package/dist/utils/logger.d.ts +0 -6
  86. package/dist/utils/logger.d.ts.map +0 -1
  87. package/dist/utils/logger.js +0 -17
  88. package/dist/utils/logger.js.map +0 -1
  89. package/dist/utils/path.d.ts +0 -6
  90. package/dist/utils/path.d.ts.map +0 -1
  91. package/dist/utils/path.js +0 -14
  92. package/dist/utils/path.js.map +0 -1
  93. package/docs/planning/memory-lane.md +0 -83
  94. package/packaging/linux/aicgen.spec +0 -23
  95. package/packaging/linux/control +0 -9
  96. package/packaging/macos/scripts/postinstall +0 -12
  97. package/packaging/windows/setup.nsi +0 -92
  98. package/scripts/add-categories.ts +0 -87
  99. package/scripts/build-binary.ts +0 -46
  100. package/scripts/embed-data.ts +0 -105
  101. package/scripts/generate-version.ts +0 -150
  102. package/scripts/test-decompress.ts +0 -27
  103. package/scripts/test-extract.ts +0 -31
  104. package/src/__tests__/services/assistant-file-writer.test.ts +0 -400
  105. package/src/__tests__/services/guideline-loader.test.ts +0 -281
  106. package/src/__tests__/services/tarball-extraction.test.ts +0 -125
  107. package/src/commands/add-guideline.ts +0 -296
  108. package/src/commands/clear.ts +0 -61
  109. package/src/commands/guideline-selector.ts +0 -123
  110. package/src/commands/init.ts +0 -645
  111. package/src/commands/quick-add.ts +0 -586
  112. package/src/commands/remove-guideline.ts +0 -152
  113. package/src/commands/stats.ts +0 -49
  114. package/src/commands/update.ts +0 -240
  115. package/src/config.ts +0 -82
  116. package/src/embedded-data.ts +0 -1492
  117. package/src/index.ts +0 -67
  118. package/src/models/profile.ts +0 -24
  119. package/src/models/project.ts +0 -43
  120. package/src/services/assistant-file-writer.ts +0 -612
  121. package/src/services/config-generator.ts +0 -150
  122. package/src/services/config-manager.ts +0 -70
  123. package/src/services/data-source.ts +0 -248
  124. package/src/services/first-run-init.ts +0 -148
  125. package/src/services/guideline-loader.ts +0 -311
  126. package/src/services/hook-generator.ts +0 -178
  127. package/src/services/subagent-generator.ts +0 -310
  128. package/src/utils/banner.ts +0 -66
  129. package/src/utils/errors.ts +0 -27
  130. package/src/utils/file.ts +0 -67
  131. package/src/utils/formatting.ts +0 -172
  132. package/src/utils/logger.ts +0 -89
  133. package/src/utils/path.ts +0 -17
  134. package/src/utils/wizard-state.ts +0 -132
  135. package/tsconfig.json +0 -25
  136. /package/{CLAUDE.md → claude.md} +0 -0
@@ -1,150 +0,0 @@
1
- import { join } from 'path';
2
- import { exists, readJSON } from '../utils/file.js';
3
- import { GuidelineLoader } from './guideline-loader.js';
4
- import { AssistantFileWriter } from './assistant-file-writer.js';
5
- import { DetectedProject, Language } from '../models/project.js';
6
- import { ProfileSelection } from '../models/profile.js';
7
-
8
- export interface GenerationOptions {
9
- projectPath: string;
10
- selection: ProfileSelection;
11
- customGuidelineIds?: string[];
12
- dryRun?: boolean;
13
- }
14
-
15
- export interface GenerationResult {
16
- success: boolean;
17
- filesGenerated: string[];
18
- errors: string[];
19
- }
20
-
21
- export class ConfigGenerator {
22
- private guidelineLoader: GuidelineLoader;
23
- private fileWriter: AssistantFileWriter;
24
-
25
- static async create(): Promise<ConfigGenerator> {
26
- const guidelineLoader = await GuidelineLoader.create();
27
- const fileWriter = await AssistantFileWriter.create();
28
- return new ConfigGenerator(guidelineLoader, fileWriter);
29
- }
30
-
31
- private constructor(guidelineLoader: GuidelineLoader, fileWriter: AssistantFileWriter) {
32
- this.guidelineLoader = guidelineLoader;
33
- this.fileWriter = fileWriter;
34
- }
35
-
36
- async detectProject(projectPath: string): Promise<DetectedProject> {
37
- const name = await this.getProjectName(projectPath);
38
- const language = await this.detectLanguage(projectPath);
39
- const hasExistingConfig = await this.hasExistingConfig(projectPath);
40
-
41
- return { name, language, hasExistingConfig };
42
- }
43
-
44
- async generate(options: GenerationOptions): Promise<GenerationResult> {
45
- const errors: string[] = [];
46
- const filesGenerated: string[] = [];
47
-
48
- try {
49
- const guidelineIds = options.customGuidelineIds || this.guidelineLoader.getGuidelinesForProfile(
50
- options.selection.language,
51
- options.selection.level,
52
- options.selection.architecture,
53
- options.selection.datasource
54
- );
55
-
56
- if (guidelineIds.length === 0) {
57
- throw new Error(`No guidelines found for profile: ${options.selection.language}-${options.selection.level}-${options.selection.architecture}`);
58
- }
59
-
60
- const files = await this.fileWriter.generateFiles(
61
- options.selection.assistant,
62
- guidelineIds,
63
- options.selection,
64
- options.projectPath
65
- );
66
-
67
- if (options.dryRun) {
68
- return {
69
- success: true,
70
- filesGenerated: files.map(f => f.path),
71
- errors: []
72
- };
73
- }
74
-
75
- await this.fileWriter.writeFiles(files);
76
- files.forEach(f => filesGenerated.push(f.path));
77
-
78
- return {
79
- success: true,
80
- filesGenerated,
81
- errors: []
82
- };
83
- } catch (error) {
84
- errors.push((error as Error).message);
85
- return {
86
- success: false,
87
- filesGenerated,
88
- errors
89
- };
90
- }
91
- }
92
-
93
- getStats() {
94
- return this.guidelineLoader.getStats();
95
- }
96
-
97
- private async getProjectName(projectPath: string): Promise<string> {
98
- const pkgPath = join(projectPath, 'package.json');
99
- if (await exists(pkgPath)) {
100
- try {
101
- const pkg = await readJSON<{ name?: string }>(pkgPath);
102
- if (pkg.name) return pkg.name;
103
- } catch {
104
- // Ignore
105
- }
106
- }
107
-
108
- const parts = projectPath.split(/[/\\]/);
109
- return parts[parts.length - 1] || 'project';
110
- }
111
-
112
- private async detectLanguage(projectPath: string): Promise<Language> {
113
- if (await exists(join(projectPath, 'tsconfig.json'))) {
114
- return 'typescript';
115
- }
116
- if (await exists(join(projectPath, 'package.json'))) {
117
- return 'javascript';
118
- }
119
- if (await exists(join(projectPath, 'requirements.txt')) ||
120
- await exists(join(projectPath, 'pyproject.toml')) ||
121
- await exists(join(projectPath, 'Pipfile'))) {
122
- return 'python';
123
- }
124
- if (await exists(join(projectPath, 'go.mod'))) {
125
- return 'go';
126
- }
127
- if (await exists(join(projectPath, 'Cargo.toml'))) {
128
- return 'rust';
129
- }
130
- if (await exists(join(projectPath, 'pom.xml')) ||
131
- await exists(join(projectPath, 'build.gradle'))) {
132
- return 'java';
133
- }
134
- if (await exists(join(projectPath, 'Gemfile'))) {
135
- return 'ruby';
136
- }
137
- return 'unknown';
138
- }
139
-
140
- private async hasExistingConfig(projectPath: string): Promise<boolean> {
141
- return (
142
- await exists(join(projectPath, 'claude.md')) ||
143
- await exists(join(projectPath, '.claude')) ||
144
- await exists(join(projectPath, '.github', 'copilot-instructions.md')) ||
145
- await exists(join(projectPath, '.gemini')) ||
146
- await exists(join(projectPath, '.agent')) ||
147
- await exists(join(projectPath, '.codex'))
148
- );
149
- }
150
- }
@@ -1,70 +0,0 @@
1
- import { readFile, writeFile, mkdir, access } from 'fs/promises';
2
- import { join } from 'path';
3
- import { homedir } from 'os';
4
- import YAML from 'yaml';
5
- import { CONFIG } from '../config.js';
6
-
7
- export interface UserConfig {
8
- initialized?: boolean;
9
- lastUpdate?: string;
10
- dataVersion?: string;
11
- github?: {
12
- owner?: string;
13
- repo?: string;
14
- };
15
- }
16
-
17
- export class ConfigManager {
18
- private configPath: string;
19
- private config: UserConfig = {};
20
-
21
- constructor() {
22
- this.configPath = join(homedir(), CONFIG.CACHE_DIR_NAME, 'config.yml');
23
- }
24
-
25
- async load(): Promise<UserConfig> {
26
- try {
27
- await access(this.configPath);
28
- const content = await readFile(this.configPath, 'utf-8');
29
- this.config = YAML.parse(content) || {};
30
- } catch {
31
- // Config doesn't exist yet, use defaults
32
- this.config = {
33
- initialized: false
34
- };
35
- }
36
- return this.config;
37
- }
38
-
39
- async save(): Promise<void> {
40
- const dir = join(homedir(), CONFIG.CACHE_DIR_NAME);
41
- await mkdir(dir, { recursive: true });
42
- await writeFile(this.configPath, YAML.stringify(this.config), 'utf-8');
43
- }
44
-
45
- async markInitialized(version: string): Promise<void> {
46
- this.config.initialized = true;
47
- this.config.dataVersion = version;
48
- this.config.lastUpdate = new Date().toISOString();
49
- await this.save();
50
- }
51
-
52
- async updateDataVersion(version: string): Promise<void> {
53
- this.config.dataVersion = version;
54
- this.config.lastUpdate = new Date().toISOString();
55
- await this.save();
56
- }
57
-
58
- isInitialized(): boolean {
59
- return this.config.initialized === true;
60
- }
61
-
62
- getDataVersion(): string | undefined {
63
- return this.config.dataVersion;
64
- }
65
-
66
- async setGitHubRepo(owner: string, repo: string): Promise<void> {
67
- this.config.github = { owner, repo };
68
- await this.save();
69
- }
70
- }
@@ -1,248 +0,0 @@
1
- import { readFile, readdir, access } from 'fs/promises';
2
- import { join } from 'path';
3
- import YAML from 'yaml';
4
- import { EMBEDDED_DATA, GuidelineMapping } from '../embedded-data.js';
5
-
6
- export interface GuidelineData {
7
- mappings: Record<string, GuidelineMapping>;
8
- guidelines: Record<string, string>;
9
- version?: string;
10
- }
11
-
12
- export interface DataSource {
13
- load(): Promise<GuidelineData>;
14
- exists(): Promise<boolean>;
15
- getVersion(): Promise<string>;
16
- }
17
-
18
- export class EmbeddedDataSource implements DataSource {
19
- async load(): Promise<GuidelineData> {
20
- return {
21
- mappings: EMBEDDED_DATA.mappings,
22
- guidelines: EMBEDDED_DATA.guidelines,
23
- version: 'embedded'
24
- };
25
- }
26
-
27
- async exists(): Promise<boolean> {
28
- return true;
29
- }
30
-
31
- async getVersion(): Promise<string> {
32
- return 'embedded';
33
- }
34
- }
35
-
36
- export class FileSystemDataSource implements DataSource {
37
- constructor(private basePath: string) {}
38
-
39
- async load(): Promise<GuidelineData> {
40
- const mappingsPath = join(this.basePath, 'guideline-mappings.yml');
41
- const mappingsContent = await readFile(mappingsPath, 'utf-8');
42
- const mappings = YAML.parse(mappingsContent) as Record<string, GuidelineMapping>;
43
-
44
- const guidelines: Record<string, string> = {};
45
- const guidelinesDir = join(this.basePath, 'guidelines');
46
-
47
- try {
48
- await this.loadGuidelinesRecursive(guidelinesDir, '', guidelines);
49
- } catch {
50
- // Guidelines directory might not exist yet
51
- }
52
-
53
- const version = await this.readVersion();
54
-
55
- return { mappings, guidelines, version };
56
- }
57
-
58
- async exists(): Promise<boolean> {
59
- try {
60
- await access(join(this.basePath, 'guideline-mappings.yml'));
61
- return true;
62
- } catch {
63
- return false;
64
- }
65
- }
66
-
67
- async getVersion(): Promise<string> {
68
- return await this.readVersion();
69
- }
70
-
71
- private async readVersion(): Promise<string> {
72
- try {
73
- const versionPath = join(this.basePath, 'version.json');
74
- const versionContent = await readFile(versionPath, 'utf-8');
75
- const versionData = JSON.parse(versionContent);
76
- return versionData.version || 'unknown';
77
- } catch {
78
- return 'unknown';
79
- }
80
- }
81
-
82
- private async loadGuidelinesRecursive(
83
- dir: string,
84
- relativePath: string,
85
- guidelines: Record<string, string>
86
- ): Promise<void> {
87
- try {
88
- const entries = await readdir(dir, { withFileTypes: true });
89
-
90
- for (const entry of entries) {
91
- const fullPath = join(dir, entry.name);
92
- const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
93
-
94
- if (entry.isDirectory()) {
95
- await this.loadGuidelinesRecursive(fullPath, relPath, guidelines);
96
- } else if (entry.name.endsWith('.md')) {
97
- const content = await readFile(fullPath, 'utf-8');
98
- guidelines[relPath] = content;
99
- }
100
- }
101
- } catch {
102
- // Directory might not exist
103
- }
104
- }
105
- }
106
-
107
- export class CustomMappingsDataSource implements DataSource {
108
- constructor(private basePath: string) {}
109
-
110
- async load(): Promise<GuidelineData> {
111
- const mappingsPath = join(this.basePath, 'custom-mappings.yml');
112
-
113
- let mappings: Record<string, GuidelineMapping> = {};
114
- try {
115
- const mappingsContent = await readFile(mappingsPath, 'utf-8');
116
- mappings = YAML.parse(mappingsContent) as Record<string, GuidelineMapping>;
117
- } catch {
118
- // Custom mappings don't exist yet
119
- }
120
-
121
- const guidelines: Record<string, string> = {};
122
- const guidelinesDir = join(this.basePath, 'guidelines');
123
-
124
- try {
125
- await this.loadGuidelinesRecursive(guidelinesDir, '', guidelines);
126
- } catch {
127
- // Guidelines directory might not exist yet
128
- }
129
-
130
- return { mappings, guidelines, version: 'custom' };
131
- }
132
-
133
- async exists(): Promise<boolean> {
134
- try {
135
- await access(join(this.basePath, 'custom-mappings.yml'));
136
- return true;
137
- } catch {
138
- // Check if guidelines directory exists
139
- try {
140
- await access(join(this.basePath, 'guidelines'));
141
- return true;
142
- } catch {
143
- return false;
144
- }
145
- }
146
- }
147
-
148
- async getVersion(): Promise<string> {
149
- return 'custom';
150
- }
151
-
152
- private async loadGuidelinesRecursive(
153
- dir: string,
154
- relativePath: string,
155
- guidelines: Record<string, string>
156
- ): Promise<void> {
157
- try {
158
- const entries = await readdir(dir, { withFileTypes: true });
159
-
160
- for (const entry of entries) {
161
- const fullPath = join(dir, entry.name);
162
- const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
163
-
164
- if (entry.isDirectory()) {
165
- await this.loadGuidelinesRecursive(fullPath, relPath, guidelines);
166
- } else if (entry.name.endsWith('.md')) {
167
- const content = await readFile(fullPath, 'utf-8');
168
- guidelines[relPath] = content;
169
- }
170
- }
171
- } catch {
172
- // Directory might not exist
173
- }
174
- }
175
- }
176
-
177
- export class HybridDataSource implements DataSource {
178
- constructor(private sources: DataSource[]) {}
179
-
180
- async load(): Promise<GuidelineData> {
181
- const results = await Promise.all(
182
- this.sources.map(async (source) => {
183
- if (await source.exists()) {
184
- try {
185
- return await source.load();
186
- } catch (error) {
187
- console.warn(`Failed to load from data source: ${error}`);
188
- return null;
189
- }
190
- }
191
- return null;
192
- })
193
- );
194
-
195
- const validResults = results.filter((r): r is GuidelineData => r !== null);
196
-
197
- if (validResults.length === 0) {
198
- throw new Error('No data sources available');
199
- }
200
-
201
- return this.merge(validResults);
202
- }
203
-
204
- async exists(): Promise<boolean> {
205
- const exists = await Promise.all(this.sources.map((s) => s.exists()));
206
- return exists.some((e) => e);
207
- }
208
-
209
- async getVersion(): Promise<string> {
210
- for (const source of this.sources) {
211
- if (await source.exists()) {
212
- const version = await source.getVersion();
213
- if (version !== 'custom' && version !== 'unknown') {
214
- return version;
215
- }
216
- }
217
- }
218
- return 'embedded';
219
- }
220
-
221
- private merge(dataArray: GuidelineData[]): GuidelineData {
222
- // Merge with priority: first source has highest priority
223
- const merged: GuidelineData = {
224
- mappings: {},
225
- guidelines: {},
226
- version: dataArray[0]?.version || 'unknown'
227
- };
228
-
229
- // Merge in reverse order so earlier sources override later ones
230
- for (let i = dataArray.length - 1; i >= 0; i--) {
231
- const data = dataArray[i];
232
- Object.assign(merged.mappings, data.mappings);
233
- Object.assign(merged.guidelines, data.guidelines);
234
-
235
- // Use the first non-custom, non-unknown version
236
- if (
237
- data.version &&
238
- data.version !== 'custom' &&
239
- data.version !== 'unknown' &&
240
- (merged.version === 'unknown' || merged.version === 'custom')
241
- ) {
242
- merged.version = data.version;
243
- }
244
- }
245
-
246
- return merged;
247
- }
248
- }
@@ -1,148 +0,0 @@
1
- import { mkdir, writeFile, readdir, cp, rm } from 'fs/promises';
2
- import { join } from 'path';
3
- import { homedir } from 'os';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import { CONFIG, GITHUB_RELEASES_URL } from '../config.js';
7
- import { ConfigManager } from './config-manager.js';
8
-
9
- const DOWNLOAD_TIMEOUT_MS = 30000; // 30 seconds
10
- const MAX_TARBALL_SIZE_BYTES = 10 * 1024 * 1024; // 10MB max
11
-
12
- export async function ensureDataInitialized(): Promise<void> {
13
- const configManager = new ConfigManager();
14
- await configManager.load();
15
-
16
- if (configManager.isInitialized()) {
17
- return; // Already initialized
18
- }
19
-
20
- console.log(chalk.cyan('\nšŸš€ First-time setup...\n'));
21
-
22
- const spinner = ora('Downloading latest guidelines from GitHub...').start();
23
-
24
- try {
25
- await downloadFromGitHub();
26
- spinner.succeed('Downloaded latest guidelines from GitHub');
27
- await configManager.markInitialized('latest');
28
- console.log(chalk.green('āœ“ Using latest guidelines from GitHub\n'));
29
- } catch {
30
- spinner.info('Could not reach GitHub, using bundled guidelines');
31
- await configManager.markInitialized('embedded');
32
- console.log(chalk.green('āœ“ Using bundled guidelines\n'));
33
- }
34
-
35
- console.log(chalk.gray(' Tip: Run `aicgen update` anytime to sync with latest guidelines\n'));
36
- }
37
-
38
- async function downloadFromGitHub(): Promise<void> {
39
- const controller = new AbortController();
40
- const timeout = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
41
-
42
- try {
43
- const response = await fetch(GITHUB_RELEASES_URL, {
44
- headers: {
45
- 'Accept': 'application/vnd.github+json',
46
- 'User-Agent': CONFIG.USER_AGENT
47
- },
48
- signal: controller.signal
49
- });
50
-
51
- if (!response.ok) {
52
- throw new Error(`GitHub API returned ${response.status}`);
53
- }
54
-
55
- const data = await response.json() as { tag_name: string; tarball_url: string };
56
- const version = data.tag_name.replace(/^v/, '');
57
-
58
- const cacheDir = join(homedir(), CONFIG.CACHE_DIR_NAME, CONFIG.CACHE_DIR);
59
- await mkdir(cacheDir, { recursive: true });
60
-
61
- // Download tarball with timeout and size limit
62
- const tarballController = new AbortController();
63
- const tarballTimeout = setTimeout(() => tarballController.abort(), DOWNLOAD_TIMEOUT_MS);
64
-
65
- try {
66
- const tarballResponse = await fetch(data.tarball_url, {
67
- signal: tarballController.signal
68
- });
69
-
70
- if (!tarballResponse.ok) {
71
- throw new Error(`Failed to download tarball: ${tarballResponse.status}`);
72
- }
73
-
74
- // Check content-length if available
75
- const contentLength = tarballResponse.headers.get('content-length');
76
- if (contentLength && parseInt(contentLength, 10) > MAX_TARBALL_SIZE_BYTES) {
77
- throw new Error(`Tarball too large: ${contentLength} bytes (max ${MAX_TARBALL_SIZE_BYTES})`);
78
- }
79
-
80
- const tarballBuffer = Buffer.from(await tarballResponse.arrayBuffer());
81
-
82
- // Verify size after download
83
- if (tarballBuffer.length > MAX_TARBALL_SIZE_BYTES) {
84
- throw new Error(`Downloaded tarball too large: ${tarballBuffer.length} bytes`);
85
- }
86
-
87
- const tempDir = join(cacheDir, '.temp-extract');
88
- await mkdir(tempDir, { recursive: true });
89
-
90
- try {
91
- // Write tarball to temp file
92
- const tarballPath = join(tempDir, 'archive.tar.gz');
93
- await writeFile(tarballPath, tarballBuffer);
94
-
95
- // Extract tarball using decompress (Windows-compatible)
96
- const decompress = (await import('decompress')).default;
97
- await decompress(tarballPath, tempDir);
98
-
99
- // Find extracted directory - derive from CONFIG instead of hardcoding
100
- const entries = await readdir(tempDir);
101
- const expectedPrefix = `${CONFIG.GITHUB_REPO_OWNER}-${CONFIG.GITHUB_REPO_NAME}-`;
102
- const rootDir = entries.find(entry => entry.startsWith(expectedPrefix));
103
-
104
- if (!rootDir) {
105
- throw new Error(`Could not find extracted repository directory (expected ${expectedPrefix}*)`);
106
- }
107
-
108
- const extractedPath = join(tempDir, rootDir);
109
-
110
- // Copy contents to cache directory with proper structure
111
- const guidelinesTarget = join(cacheDir, 'guidelines');
112
- await mkdir(guidelinesTarget, { recursive: true });
113
-
114
- // Copy extracted contents with correct structure
115
- const extractedEntries = await readdir(extractedPath, { withFileTypes: true });
116
- for (const entry of extractedEntries) {
117
- const sourcePath = join(extractedPath, entry.name);
118
- const targetPath = join(guidelinesTarget, entry.name);
119
-
120
- if (entry.name === 'guideline-mappings.yml') {
121
- await cp(sourcePath, join(cacheDir, entry.name));
122
- } else if (entry.isDirectory() || entry.name.endsWith('.md')) {
123
- await cp(sourcePath, targetPath, { recursive: true });
124
- }
125
- }
126
-
127
- // Write version file
128
- await writeFile(
129
- join(cacheDir, 'version.json'),
130
- JSON.stringify({ version, updatedAt: new Date().toISOString(), source: 'github' }),
131
- 'utf-8'
132
- );
133
- } finally {
134
- await rm(tempDir, { recursive: true, force: true });
135
- }
136
- } finally {
137
- clearTimeout(tarballTimeout);
138
- }
139
- } finally {
140
- clearTimeout(timeout);
141
- }
142
- }
143
-
144
- export async function shouldRunFirstTimeSetup(): Promise<boolean> {
145
- const configManager = new ConfigManager();
146
- await configManager.load();
147
- return !configManager.isInitialized();
148
- }