github_analyzer 0.1.6
github_analyzer: ^0.1.6 copied to clipboard
Analyze GitHub repositories and generate AI context for LLMs with cross-platform support
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.1.6 - 2025-11-03 #
π₯ Breaking Changes #
Removed Automatic .env Loading
- Removed: Automatic
.envfile loading due to macOS sandbox restrictions - Changed: Users must now explicitly pass
githubTokenparameter - Reason: File system access in sandboxed environments (macOS apps) causes permission errors
- Migration: Pass tokens directly via parameters or use environment variables
Before (v0.1.5 and earlier):
// Token automatically loaded from .env file
final result = await analyzeForLLM('https://github.com/user/repo');
After (v0.1.6+):
// Token must be passed explicitly
final result = await analyzeForLLM(
'https://github.com/user/repo',
githubToken: 'ghp_your_token_here',
);
// Or load from environment variables
import 'dart:io';
final token = Platform.environment['GITHUB_TOKEN'];
final result = await analyzeForLLM(
'https://github.com/user/repo',
githubToken: token,
);
π Critical Fixes #
Fixed Cache Respecting useCache: false Parameter
- Fixed: Cache was being created even when
useCache: falsewas explicitly set - Root Cause: Cache save logic only checked
config.enableCache, ignoringuseCacheparameter - Impact: Users couldn't force fresh analysis without modifying config
Technical Details:
// Before (v0.1.5)
if (config.enableCache && cacheService != null) {
await cacheService!.set(repositoryUrl, cacheKey, result);
}
// After (v0.1.6)
if (useCache && config.enableCache && cacheService != null) {
await cacheService!.set(repositoryUrl, cacheKey, result);
}
Usage:
// Now correctly bypasses cache
final result = await analyzer.analyze(
'https://github.com/user/repo',
useCache: false, // β
Cache won't be created
);
ποΈ Removed #
- EnvLoader: Removed
src/common/env_loader.dart(no longer exported) - Auto .env Loading: Removed from
GithubAnalyzerConfig.create(),.quick(),.forLLM() - Service Locator .env: Removed automatic token loading from DI container
β¨ Added #
- Explicit Token Passing: All functions now support direct
githubTokenparameter - DartDoc Documentation: Added comprehensive English documentation to all public APIs
- Security Guidelines: Added best practices for token management in README
π Documentation #
- Updated README: Reflects new explicit token passing requirement
- Security Section: Added examples for environment variables and secure storage
- Migration Guide: Clear before/after examples for upgrading from v0.1.5
π§ Configuration Changes #
autoLoadEnvparameter: Deprecated (kept for backward compatibility, no longer functional)- Token Source: Users must provide tokens via:
- Direct parameter passing
Platform.environment['GITHUB_TOKEN']- Secure storage (flutter_secure_storage)
- Build-time environment variables
π Affected Functions #
All convenience functions now require explicit token passing:
analyze()analyzeQuick(githubToken: token)analyzeForLLM(githubToken: token)analyzeAndGenerate()
β οΈ Migration Required #
For Public Repositories: No changes needed - works without token.
For Private Repositories:
// Option 1: Environment variable
import 'dart:io';
final token = Platform.environment['GITHUB_TOKEN'];
final result = await analyzeForLLM(
'https://github.com/user/private-repo',
githubToken: token,
);
// Option 2: Secure storage (Flutter)
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final storage = FlutterSecureStorage();
final token = await storage.read(key: 'github_token');
final result = await analyzeQuick(
'https://github.com/user/private-repo',
githubToken: token,
);
// Option 3: Config object
final config = await GithubAnalyzerConfig.create(
githubToken: 'ghp_your_token',
);
final analyzer = await GithubAnalyzer.create(config: config);
π― Benefits #
- β Better Security: No file system access for sensitive data
- β Cross-Platform: Works reliably on all platforms including sandboxed environments
- β Explicit Control: Users have full control over token source
- β Flexibility: Easy integration with various secret management solutions
0.1.5 - 2025-11-03 #
π₯ Critical Fixes #
Fixed Private Repository Analysis Failure
Private repositories now work seamlessly without requiring explicit token parameters in function calls. The root cause was DI container not detecting token changes between consecutive analyzeForLLM() calls.
β¨ Key Changes #
1. HTTP Client Manager - HTTP Redirect Support
- Added
followRedirects: true - Added
maxRedirects: 5 - Why: GitHub API uses 302 redirects for ZIP downloads. Without this, all downloads failed immediately.
BaseOptions(
connectTimeout: requestTimeout,
receiveTimeout: requestTimeout,
sendTimeout: requestTimeout,
followRedirects: true, // β
NEW
maxRedirects: 5, // β
NEW
)
2. Zip Downloader - Private Repository Detection
- Added
isPrivateparameter todownloadRepositoryAsBytes() - Prevents public URL fallback for private repos
- Forces GitHub API usage when repository is private
Future<Uint8List> downloadRepositoryAsBytes({
required String owner,
required String repo,
required String ref,
String? token,
bool isPrivate = false, // β
NEW
})
3. Remote Analyzer Service - Metadata Propagation
- Now passes
metadata?.isPrivateto ZipDownloader - Communicates repository visibility status through call chain
final isPrivate = metadata?.isPrivate ?? false;
return await zipDownloader.downloadRepositoryAsBytes(
owner: owner,
repo: repo,
ref: ref,
token: githubToken,
isPrivate: isPrivate, // β
NEW
);
4. Service Locator - Token Change Detection (CRITICAL FIX) β‘
- Detects when GitHub token changes between function calls
- Automatically reinitializes DI container with new token
- This was THE ROOT CAUSE of private repository failures
// β
NEW: Detect token changes
if (config != null && getIt.isRegistered<GithubAnalyzerConfig>()) {
final existingConfig = getIt<GithubAnalyzerConfig>();
// Reinitialize only if token changed
if (existingConfig.githubToken != config.githubToken) {
await getIt.reset();
} else {
return; // Token same, skip reinitialization
}
}
π― Results #
Before: Private repositories returned 404 even with valid token
After: All repository types work automatically
| Repository Type | Status | Auto-Token | Files |
|---|---|---|---|
| Public | β | Yes | 249+ |
| Public (with token) | β | Yes | 49+ |
| Private (with token) | β | Yes | 121+ |
β User Experience #
No code changes needed. Simply create .env file:
GITHUB_TOKEN=your_token_here
Then use normally:
await analyzeForLLM(
'https://github.com/private/repo.git',
outputDir: './output',
);
// β
Token auto-loaded from .env, private repo analyzed successfully!
π§ Technical Details #
- HTTP redirects now automatically followed (eliminates 302 errors)
- Private repositories detected via GitHub API metadata
- DI container intelligently reinitializes on token changes
- Token detection prevents unnecessary container resets
- Backward compatible - all existing code continues working
π Testing #
All scenarios now pass:
- β Public repos without token
- β Public repos with token
- β Private repos with token (auto-loaded)
- β Multiple consecutive analyses with mixed repository types
0.1.4 - 2025-11-03 #
Fixed #
- Fixed EnvLoader project root detection:
EnvLoadernow automatically searches for.envfile in the project root instead of only the current working directory- Added
_findEnvFile()method that traverses up to 10 parent directories to locate.env - Validates project root by checking for
pubspec.yamlor.gitto prevent loading.envfrom unrelated parent directories - Resolves issue where Flutter apps and other platforms failed to load
.envbecause the working directory differed from the project root - Improved logging to show the full path of loaded
.envfile for better debugging
- Added
Changed #
- Enhanced
EnvLoader.load()to use the new project root detection logic - Updated log message to display:
.env file loaded successfully from: {path}instead of just.env file loaded successfully - Made
EnvLoadermore robust for multi-platform deployments (macOS, iOS, Android, web)
Impact #
Users no longer need to manually pass GitHub tokens via environment variables. The package will now automatically find and load the .env file from the project root, even when running Flutter apps that execute from different working directories.
Example:
Before: 404 error (token not loaded because .env not found)
After: β
Private repository analyzed successfully (token auto-loaded from project root)
0.1.3 - 2025-11-03 #
π₯ Critical Fixes #
-
Fixed JSON serialization/deserialization bug: Resolved
type '_RepositoryMetadata' is not a subtype of type 'Map<String, dynamic>'error inAnalysisResult.fromJson()by implementing custom serialization logic for nested Freezed models -
Fixed
toJson()method: Added manual implementation to properly serialize nested objects (RepositoryMetadata,SourceFile,AnalysisStatistics,AnalysisError)
β¨ Improvements #
- Enhanced demo coverage: Added 3 new comprehensive tests (Convenience Functions, Markdown Generation, Cache Management) bringing total test coverage to 14/14
- Updated example.dart: Migrated to v0.1.2 API including new methods (
fetchMetadataOnly(),getCacheStatistics(),clearCache()) - Improved test reliability: All 14 tests now pass consistently with 100% success rate
π οΈ Technical Details #
The serialization issue was caused by json_serializable not properly handling nested Freezed models during deserialization. The fix manually implements fromJson() and toJson() methods to explicitly call the respective methods on nested objects:
// Before (auto-generated, broken)
factory AnalysisResult.fromJson(Map<String, dynamic> json) =>
_$AnalysisResultFromJson(json);
// After (manual implementation, working)
factory AnalysisResult.fromJson(Map<String, dynamic> json) {
return AnalysisResult(
metadata: RepositoryMetadata.fromJson(json['metadata'] as Map<String, dynamic>),
files: (json['files'] as List<dynamic>)
.map((e) => SourceFile.fromJson(e as Map<String, dynamic>))
.toList(),
statistics: AnalysisStatistics.fromJson(json['statistics'] as Map<String, dynamic>),
// ... other fields
);
}
Added #
-
New
fetchMetadataOnly()method: Lightweight metadata retrieval (1-3 seconds, API-only, no file download)final metadata = await analyzer.fetchMetadataOnly('https://github.com/flutter/flutter'); -
Configuration validation system: All config values validated at creation time to prevent runtime errors
maxFileSizemust be > 0maxConcurrentRequestsrange: 1-20isolatePoolSizerange: 1-16maxRetriesrange: 0-10retryDelaymax: 60 seconds
-
New configuration options:
enableFileCache: File-level caching control (default: true)autoIsolatePoolThreshold: Auto-enable parallel processing at N files (default: 100)streamingModeThreshold: Archive size threshold for streaming mode (default: 50MB)shouldUseStreamingMode(): Dynamic streaming decision method
-
Security: Token masking: GitHub tokens automatically masked in logs
- Before:
ghp_reallyLongTokenHere123456789xyz9 - After:
ghp_real...xyz9 - Added
SensitiveLoggerutility class - Tokens no longer exposed in debug logs or
toString()output
- Before:
-
Comprehensive demo.dart: 7 example scenarios covering all features
Fixed #
- Missing config fields:
enableFileCache,autoIsolatePoolThreshold,streamingModeThreshold - Undefined
AnalyzerErrorCode.invalidInput- replaced withAnalyzerErrorCode.invalidUrl - Improved error handling in
fetchMetadataOnly()for invalid repository URLs - Enhanced validation error messages with clear constraints
Changed #
-
BREAKING: All Freezed models now require
abstractkeyword (Freezed 3.0.0 compatibility)// Before @freezed class AnalysisError with _$AnalysisError { } // After @freezed abstract class AnalysisError with _$AnalysisError { } -
Enhanced
toString()output forGithubAnalyzerConfigwith masked tokens -
GithubAnalyzernow includes progress tracking for metadata fetching -
Configuration validation throws descriptive
ArgumentErrorfor invalid values
Migration Required #
dart run build_runner clean
dart run build_runner build --delete-conflicting-outputs
Breaking Changes #
- Freezed models: All model classes require
abstractorsealedkeyword - Configuration validation: Invalid config values now throw
ArgumentErrorat creation time - Regenerate files: All
.freezed.dartand.g.dartfiles must be regenerated
Performance Improvements #
- Metadata-only fetching reduces API calls and response time (10-60s β 1-3s)
- Auto-enable isolate pool based on file count threshold
- Streaming mode for archives over 50MB reduces memory usage
Models Updated (Freezed 3.0.0) #
AnalysisErrorAnalysisProgressAnalysisResultAnalysisStatisticsRepositoryMetadataSourceFile
All models now provide:
- Automatic
copyWith() - Automatic
toJson()/fromJson() - Automatic
==andhashCode - Immutability by default
- 68% less boilerplate code
0.0.8 - 2025-10-29 #
Added #
- Explicit cache control parameter: Added
useCacheparameter to all analysis functionsanalyze()function now acceptsuseCacheparameteranalyzeQuick()function now acceptsuseCacheparameteranalyzeForLLM()function now acceptsuseCacheparameteranalyzeAndGenerate()function now acceptsuseCacheparameterGithubAnalyzer.analyze()method now acceptsuseCacheparameterGithubAnalyzer.analyzeRemote()method now acceptsuseCacheparameter
Changed #
- Cache behavior can now be explicitly controlled at the function call level
- When
useCacheis not specified, the default value fromGithubAnalyzerConfig.enableCacheis used - When
useCacheis explicitly set tofalse, cache is bypassed regardless of config settings
Usage Example #
// Disable cache for a specific analysis
final result = await analyzeQuick(
'https://github.com/flutter/flutter',
useCache: false, // Always fetch fresh data
);
// Or with advanced API
final analyzer = await GithubAnalyzer.create();
final result = await analyzer.analyzeRemote(
repositoryUrl: 'https://github.com/your/repo',
useCache: false,
);
Benefits #
- Users can now force fresh data fetching when needed
- Useful for CI/CD pipelines that require latest repository state
- Provides flexibility without requiring config changes
0.0.7 - 2025-10-19 #
Fixed #
-
Fixed Critical Caching Logic: Resolved a major bug where analyzing a repository immediately after a new push could return stale data from the previous commit.
-
The analyzer now explicitly fetches the latest commit SHA for the target branch before checking the cache or downloading.
-
This exact commitSha is now used consistently as both the cache key and the download reference, eliminating race conditions and cache pollution caused by GitHub API replication delays.
-
Improved Authentication Compatibility: Standardized all GitHub API requests to use the Authorization: Bearer $token header. This ensures compatibility with both classic Personal Access Tokens (PATs) and new fine-grained PATs.
-
Fixed HTTP Retry Bug: Corrected a bug in the HttpClientManager's retry logic that was using an incorrect URI path for retrying timed-out requests, improving overall network resilience.
0.0.6 - 2025-10-15 #
Added #
- Automatic
.envfile loading: GitHub tokens are now automatically loaded from.envfiles - EnvLoader utility: New
EnvLoaderclass for seamless environment variable management - Private repository support: Enhanced ZIP downloader with GitHub API fallback for private repositories
- Async configuration factories: All
GithubAnalyzerConfigfactory methods now support async.envloading - GithubAnalyzer.create(): New factory method with automatic dependency injection and
.envloading
Changed #
- Breaking:
GithubAnalyzerConfig.quick()andGithubAnalyzerConfig.forLLM()are now async - Breaking: Removed synchronous config factories in favor of async versions
- Improved: ZIP downloader now tries GitHub API first for private repos, then falls back to public URL
- Enhanced: Token authentication now works seamlessly with Fine-grained Personal Access Tokens
Fixed #
- Fixed private repository access with Fine-grained GitHub tokens
- Fixed 403 errors when accessing private repositories
- Fixed token not being passed correctly to ZIP download endpoints
- Improved error messages for repository access issues
Documentation #
- Added comprehensive Fine-grained Token setup guide
- Updated README with
.envfile usage examples - Added troubleshooting section for private repository access
0.0.5 - 2025-10-14 #
Added #
- Web platform support with conditional compilation
universal_iopackage integration for cross-platform compatibility- Comprehensive file system abstraction layer
Changed #
- Migrated from
dart:iotouniversal_iofor web compatibility - Improved error handling for platform-specific features
Fixed #
- Web platform compilation errors
- File system access issues on web
0.0.4 - 2025-10-13 #
0.0.3 - 2025-10-12 #
0.0.2 - 2025-10-11 #
0.0.1 - 2025-10-10 #
Added #
- Initial release
- Basic GitHub repository analysis
- Markdown generation
λ³κ²½ λ‘κ·Έ #
μ΄ νλ‘μ νΈμ λͺ¨λ μ£Όλͺ©ν λ§ν λ³κ²½μ¬νμ μ΄ νμΌμ λ¬Έμνλ©λλ€.
νμμ Keep a Changelogλ₯Ό κΈ°λ°μΌλ‘ νλ©°, μ΄ νλ‘μ νΈλ μλ―Έμλ λ²μ κ΄λ¦¬λ₯Ό λ°λ¦ λλ€.
[0.1.6] - 2025-11-03 #
π₯ μ£Όμ λ³κ²½μ¬ν (Breaking Changes) #
μλ .env λ‘λ μ κ±°
- μ κ±°λ¨: macOS μλλ°μ€ μ νμΌλ‘ μΈν μλ
.envνμΌ λ‘λ μ κ±° - λ³κ²½: μ¬μ©μλ μ΄μ
githubTokenνλΌλ―Έν°λ₯Ό λͺ μμ μΌλ‘ μ λ¬ν΄μΌ ν¨ - μ¬μ : μλλ°μ€ νκ²½(macOS μ±)μμ νμΌ μμ€ν μ κ·ΌμΌλ‘ μΈν κΆν μ€λ₯
- λ§μ΄κ·Έλ μ΄μ : νλΌλ―Έν°λ‘ ν ν°μ μ§μ μ λ¬νκ±°λ νκ²½ λ³μ μ¬μ©
μ΄μ (v0.1.5 μ΄μ ):
// .env νμΌμμ ν ν° μλ λ‘λ
final result = await analyzeForLLM('https://github.com/user/repo');
μ΄ν (v0.1.6+):
// ν ν°μ λͺ
μμ μΌλ‘ μ λ¬ν΄μΌ ν¨
final result = await analyzeForLLM(
'https://github.com/user/repo',
githubToken: 'ghp_your_token_here',
);
// λλ νκ²½ λ³μμμ λ‘λ
import 'dart:io';
final token = Platform.environment['GITHUB_TOKEN'];
final result = await analyzeForLLM(
'https://github.com/user/repo',
githubToken: token,
);
π μΉλͺ μ λ²κ·Έ μμ #
useCache: false νλΌλ―Έν° μ‘΄μ€ μμ
- μμ λ¨:
useCache: falseλ₯Ό λͺ μμ μΌλ‘ μ€μ νμ λλ μΊμκ° μμ±λλ λ¬Έμ - κ·Όλ³Έ μμΈ: μΊμ μ μ₯ λ‘μ§μ΄
config.enableCacheλ§ νμΈνκ³useCacheνλΌλ―Έν° 무μ - μν₯: μ¬μ©μκ° μ€μ μμ μμ΄ κ°μ λ‘ μ λΆμμ μ€νν μ μμ
κΈ°μ μΈλΆμ¬ν:
// μ΄μ (v0.1.5)
if (config.enableCache && cacheService != null) {
await cacheService!.set(repositoryUrl, cacheKey, result);
}
// μ΄ν (v0.1.6)
if (useCache && config.enableCache && cacheService != null) {
await cacheService!.set(repositoryUrl, cacheKey, result);
}
μ¬μ©λ²:
// μ΄μ μ¬λ°λ₯΄κ² μΊμκ° μμ±λμ§ μμ
final result = await analyzer.analyze(
'https://github.com/user/repo',
useCache: false, // β
μΊμκ° μμ±λμ§ μμ
);
ποΈ μ κ±°λ¨ #
- EnvLoader:
src/common/env_loader.dartμ κ±° (λ μ΄μ λ΄λ³΄λ΄μ§ μμ) - μλ .env λ‘λ:
GithubAnalyzerConfig.create(),.quick(),.forLLM()μμ μ κ±° - Service Locator .env: DI 컨ν μ΄λμ μλ ν ν° λ‘λ μ κ±°
β¨ μΆκ°λ¨ #
- λͺ
μμ ν ν° μ λ¬: λͺ¨λ ν¨μκ° μ΄μ μ§μ
githubTokenνλΌλ―Έν° μ§μ - DartDoc λ¬Έμν: λͺ¨λ κ³΅κ° APIμ ν¬κ΄μ μΈ μμ΄ λ¬Έμ μΆκ°
- 보μ κ°μ΄λλΌμΈ: READMEμ ν ν° κ΄λ¦¬ λͺ¨λ² μ¬λ‘ μΆκ°
π λ¬Έμν #
- README μ λ°μ΄νΈ: μλ‘μ΄ λͺ μμ ν ν° μ λ¬ μꡬμ¬ν λ°μ
- 보μ μΉμ : νκ²½ λ³μ λ° λ³΄μ μ μ₯μ μ¬μ© μμ μΆκ°
- λ§μ΄κ·Έλ μ΄μ κ°μ΄λ: v0.1.5μμ μ κ·Έλ μ΄λνκΈ° μν λͺ νν μ΄μ /μ΄ν μμ
π§ μ€μ λ³κ²½μ¬ν #
autoLoadEnvνλΌλ―Έν°: λ μ΄μ μ¬μ©λμ§ μμ (νμ νΈνμ±μ μν΄ μ μ§λμ§λ§ κΈ°λ₯ μμ)- ν ν° μμ€: μ¬μ©μλ λ€μμ ν΅ν΄ ν ν° μ 곡ν΄μΌ ν¨:
- μ§μ νλΌλ―Έν° μ λ¬
Platform.environment['GITHUB_TOKEN']- 보μ μ μ₯μ (flutter_secure_storage)
- λΉλ νμ νκ²½ λ³μ
π μν₯μ λ°λ ν¨μ #
λͺ¨λ νΈμ ν¨μλ μ΄μ λͺ μμ ν ν° μ λ¬ νμ:
analyze()analyzeQuick(githubToken: token)analyzeForLLM(githubToken: token)analyzeAndGenerate()
β οΈ λ§μ΄κ·Έλ μ΄μ νμ #
κ³΅κ° μ μ₯μμ κ²½μ°: λ³κ²½ μ¬ν μμ - ν ν° μμ΄ μλν©λλ€.
λΉκ³΅κ° μ μ₯μμ κ²½μ°:
// μ΅μ
1: νκ²½ λ³μ
import 'dart:io';
final token = Platform.environment['GITHUB_TOKEN'];
final result = await analyzeForLLM(
'https://github.com/user/private-repo',
githubToken: token,
);
// μ΅μ
2: 보μ μ μ₯μ (Flutter)
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final storage = FlutterSecureStorage();
final token = await storage.read(key: 'github_token');
final result = await analyzeQuick(
'https://github.com/user/private-repo',
githubToken: token,
);
// μ΅μ
3: μ€μ κ°μ²΄
final config = await GithubAnalyzerConfig.create(
githubToken: 'ghp_your_token',
);
final analyzer = await GithubAnalyzer.create(config: config);
π― μ₯μ #
- β λ λμ 보μ: λ―Όκ°ν λ°μ΄ν°μ λν νμΌ μμ€ν μ κ·Ό μμ
- β ν¬λ‘μ€ νλ«νΌ: μλλ°μ€ νκ²½μ ν¬ν¨ν λͺ¨λ νλ«νΌμμ μμ μ μΌλ‘ μλ
- β λͺ μμ μ μ΄: μ¬μ©μκ° ν ν° μμ€μ λν μμ ν μ μ΄ κ°λ₯
- β μ μ°μ±: λ€μν λΉλ° κ΄λ¦¬ μ루μ κ³Ό μ¬μ΄ ν΅ν©
[0.1.5] - 2025-11-03 #
π₯ μΉλͺ μ λ²κ·Έ μμ #
λΉκ³΅κ° μ μ₯μ λΆμ μ€ν¨ μμ
λΉκ³΅κ° μ μ₯μκ° μ΄μ ν¨μ νΈμΆ κ° analyzeForLLM() μ°μ νΈμΆ μ DI 컨ν
μ΄λκ° ν ν° λ³κ²½μ κ°μ§νμ§ λͺ»νλ κ·Όλ³Έ μμΈμ ν΄κ²°νμ¬ λͺ
μμ ν ν° νλΌλ―Έν° μμ΄ μννκ² μλν©λλ€.
β¨ μ£Όμ λ³κ²½μ¬ν #
1. HTTP ν΄λΌμ΄μΈνΈ κ΄λ¦¬μ - HTTP 리λ€μ΄λ νΈ μ§μ
followRedirects: trueμΆκ°maxRedirects: 5μΆκ°- μ΄μ : GitHub APIλ ZIP λ€μ΄λ‘λμ 302 리λ€μ΄λ νΈ μ¬μ©. μμΌλ©΄ λͺ¨λ λ€μ΄λ‘λ μ¦μ μ€ν¨.
BaseOptions(
connectTimeout: requestTimeout,
receiveTimeout: requestTimeout,
sendTimeout: requestTimeout,
followRedirects: true, // β
μ κ·
maxRedirects: 5, // β
μ κ·
)
2. Zip λ€μ΄λ‘λ - λΉκ³΅κ° μ μ₯μ κ°μ§
downloadRepositoryAsBytes()μisPrivateνλΌλ―Έν° μΆκ°- λΉκ³΅κ° μ μ₯μμ λν κ³΅κ° URL ν΄λ°± λ°©μ§
- μ μ₯μκ° λΉκ³΅κ°μΌ λ GitHub API μ¬μ© κ°μ
Future<Uint8List> downloadRepositoryAsBytes({
required String owner,
required String repo,
required String ref,
String? token,
bool isPrivate = false, // β
μ κ·
})
3. μ격 λΆμκΈ° μλΉμ€ - λ©νλ°μ΄ν° μ ν
- μ΄μ ZipDownloaderμ
metadata?.isPrivateμ λ¬ - νΈμΆ 체μΈμ ν΅ν΄ μ μ₯μ κ°μμ± μν ν΅μ
final isPrivate = metadata?.isPrivate ?? false;
return await zipDownloader.downloadRepositoryAsBytes(
owner: owner,
repo: repo,
ref: ref,
token: githubToken,
isPrivate: isPrivate, // β
μ κ·
);
4. Service Locator - ν ν° λ³κ²½ κ°μ§ (μΉλͺ μ λ²κ·Έ μμ ) β‘
- ν¨μ νΈμΆ κ° GitHub ν ν° λ³κ²½ κ°μ§
- μ ν ν°μΌλ‘ DI 컨ν μ΄λ μλ μ¬μ΄κΈ°ν
- μ΄κ²μ΄ λΉκ³΅κ° μ μ₯μ μ€ν¨μ κ·Όλ³Έ μμΈ
// β
μ κ·: ν ν° λ³κ²½ κ°μ§
if (config != null && getIt.isRegistered<GithubAnalyzerConfig>()) {
final existingConfig = getIt<GithubAnalyzerConfig>();
// ν ν°μ΄ λ³κ²½λ κ²½μ°μλ§ μ¬μ΄κΈ°ν
if (existingConfig.githubToken != config.githubToken) {
await getIt.reset();
} else {
return; // ν ν° λμΌ, μ¬μ΄κΈ°ν μ€ν΅
}
}
π― κ²°κ³Ό #
μ΄μ : μ ν¨ν ν ν°μλ λΆκ΅¬νκ³ λΉκ³΅κ° μ μ₯μκ° 404 λ°ν
μ΄ν: λͺ¨λ μ μ₯μ μ νμ΄ μλμΌλ‘ μλ
| μ μ₯μ μ ν | μν | μλ ν ν° | νμΌ |
|---|---|---|---|
| κ³΅κ° | β | μ | 249+ |
| κ³΅κ° (ν ν° ν¬ν¨) | β | μ | 49+ |
| λΉκ³΅κ° (ν ν° ν¬ν¨) | β | μ | 121+ |
β μ¬μ©μ κ²½ν #
μ½λ λ³κ²½ νμ μμ. κ°λ¨ν .env νμΌ μμ±:
GITHUB_TOKEN=your_token_here
κ·Έ λ€μ μ μμ μΌλ‘ μ¬μ©:
await analyzeForLLM(
'https://github.com/private/repo.git',
outputDir: './output',
);
// β
.envμμ ν ν° μλ λ‘λ, λΉκ³΅κ° μ μ₯μ μ±κ³΅μ μΌλ‘ λΆμ!
π§ κΈ°μ μΈλΆμ¬ν #
- HTTP 리λ€μ΄λ νΈ μ΄μ μλ νλ‘μ° (302 μ€λ₯ μ κ±°)
- GitHub API λ©νλ°μ΄ν°λ₯Ό ν΅ν΄ λΉκ³΅κ° μ μ₯μ κ°μ§
- DI 컨ν μ΄λκ° ν ν° λ³κ²½ μ μ§λ₯μ μΌλ‘ μ¬μ΄κΈ°ν
- ν ν° κ°μ§λ‘ λΆνμν 컨ν μ΄λ μ¬μ€μ λ°©μ§
- νμ νΈνμ± μ μ§ - κΈ°μ‘΄ μ½λ κ³μ μλ
π ν μ€ν #
λͺ¨λ μλλ¦¬μ€ μ΄μ ν΅κ³Ό:
- β ν ν° μλ κ³΅κ° μ μ₯μ
- β ν ν° μλ κ³΅κ° μ μ₯μ
- β ν ν° μλ λΉκ³΅κ° μ μ₯μ (μλ λ‘λ)
- β νΌν© μ μ₯μ μ νμ λ€μ€ μ°μ λΆμ
[0.1.4] - 2025-11-03 #
μμ λ¨ #
- EnvLoader νλ‘μ νΈ λ£¨νΈ κ°μ§ μμ :
EnvLoaderλ μ΄μ νμ¬ μμ λλ ν λ¦¬λ§ νμΈνλ λμ νλ‘μ νΈ λ£¨νΈμμ.envνμΌμ μλ κ²μν©λλ€.envλ₯Ό μ°ΎκΈ° μν΄ μ΅λ 10κ° λΆλͺ¨ λλ ν 리κΉμ§ νΈλλ²μ€νλ_findEnvFile()λ©μλ μΆκ°pubspec.yamlλλ.gitνμΈμΌλ‘ νλ‘μ νΈ λ£¨νΈ κ²μ¦νμ¬ λ¬΄κ΄ν λΆλͺ¨ λλ ν 리μ.envλ‘λ λ°©μ§- Flutter μ± λ° κΈ°ν νλ«νΌμ΄ μμ
λλ ν λ¦¬κ° νλ‘μ νΈ λ£¨νΈμ λ€λ₯Ό λ
.envλ‘λ μ€ν¨νλ λ¬Έμ ν΄κ²° - λ λμ λλ²κΉ
μ μν΄ λ‘λλ
.envνμΌμ μ 체 κ²½λ‘λ₯Ό νμνλ λ‘κ·Έ κ°μ
λ³κ²½λ¨ #
.envλ‘λ λ‘μ§μ μλ‘μ΄ νλ‘μ νΈ λ£¨νΈ κ°μ§ λ°©μ μ¬μ©νλλ‘ κ°ν- λ‘κ·Έ λ©μμ§λ₯Ό λ€μμΌλ‘ μ
λ°μ΄νΈ:
.env file loaded successfully from: {path}(κΈ°μ‘΄.env file loaded successfullyλ체) - λ©ν° νλ«νΌ λ°°ν¬(macOS, iOS, Android, μΉ)μ λν΄
EnvLoaderλ κ°ν
μν₯ #
μ¬μ©μλ λ μ΄μ νκ²½ λ³μλ₯Ό ν΅ν΄ GitHub ν ν°μ μλμΌλ‘ μ λ¬ν νμκ° μμ΅λλ€. μμ
λλ ν λ¦¬κ° νλ‘μ νΈ λ£¨νΈμ λ€λ₯Έ κ²½μ°μλ ν¨ν€μ§κ° νλ‘μ νΈ λ£¨νΈμμ .env νμΌμ μλμΌλ‘ μ°Ύκ³ λ‘λν©λλ€. μλ₯Ό λ€μ΄ Flutter μ± μ€ν μ.
μμ :
μ΄μ : 404 μ€λ₯ (.envλ₯Ό μ°Ύμ§ λͺ»ν΄ ν ν° λ‘λ μ λ¨)
μ΄ν: β
λΉκ³΅κ° μ μ₯μ μ±κ³΅μ μΌλ‘ λΆμ (νλ‘μ νΈ λ£¨νΈμμ ν ν° μλ λ‘λ)
[0.1.3] - 2025-11-03 #
π₯ μΉλͺ μ λ²κ·Έ μμ #
-
JSON μ§λ ¬ν/μμ§λ ¬ν λ²κ·Έ μμ :
AnalysisResult.fromJson()μμ μ€μ²©λ Freezed λͺ¨λΈμ λν μ¬μ©μ μ μ μ§λ ¬ν λ‘μ§μ ꡬννμ¬type '_RepositoryMetadata' is not a subtype of type 'Map<String, dynamic>'μ€λ₯ ν΄κ²° -
toJson()λ©μλ μμ : μ€μ²©λ κ°μ²΄(RepositoryMetadata,SourceFile,AnalysisStatistics,AnalysisError)λ₯Ό μ¬λ°λ₯΄κ² μ§λ ¬ννκΈ° μν μλ ꡬν μΆκ°
β¨ κ°μ μ¬ν #
- λ°λͺ¨ 컀λ²λ¦¬μ§ κ°ν: 3κ°μ μλ‘μ΄ μ’ ν© ν μ€νΈ μΆκ°(νΈμ ν¨μ, λ§ν¬λ€μ΄ μμ±, μΊμ κ΄λ¦¬)νμ¬ μ΄ ν μ€νΈ 컀λ²λ¦¬μ§λ₯Ό 14/14λ‘ ν₯μ
- example.dart μ
λ°μ΄νΈ: v0.1.2 APIλ‘ λ§μ΄κ·Έλ μ΄μ
μλ£, μλ‘μ΄ λ©μλ ν¬ν¨(
fetchMetadataOnly(),getCacheStatistics(),clearCache()) - ν μ€νΈ μ λ’°μ± κ°μ : λͺ¨λ 14κ° ν μ€νΈ μ΄μ 100% μ±κ³΅λ₯ λ‘ μΌκ΄λκ² ν΅κ³Ό
π οΈ κΈ°μ μΈλΆμ¬ν #
μ§λ ¬ν λ¬Έμ λ json_serializableμ΄ μμ§λ ¬ν μ€ μ€μ²©λ Freezed λͺ¨λΈμ μ¬λ°λ₯΄κ² μ²λ¦¬νμ§ μμ λ°μνμ΅λλ€. μμ μ¬νμ μ€μ²©λ κ°μ²΄μμ κ°κ°μ λ©μλλ₯Ό λͺ
μμ μΌλ‘ νΈμΆνκΈ° μν΄ fromJson() λ° toJson() λ©μλλ₯Ό μλ ꡬνν©λλ€:
// μ΄μ (μλ μμ±, μμλ¨)
factory AnalysisResult.fromJson(Map<String, dynamic> json) =>
_$AnalysisResultFromJson(json);
// μ΄ν (μλ ꡬν, μλν¨)
factory AnalysisResult.fromJson(Map<String, dynamic> json) {
return AnalysisResult(
metadata: RepositoryMetadata.fromJson(json['metadata'] as Map<String, dynamic>),
files: (json['files'] as List<dynamic>)
.map((e) => SourceFile.fromJson(e as Map<String, dynamic>))
.toList(),
statistics: AnalysisStatistics.fromJson(json['statistics'] as Map<String, dynamic>),
// ... κΈ°ν νλ
);
}
μΆκ°λ¨ #
-
μλ‘μ΄
fetchMetadataOnly()λ©μλ: κ°λ²Όμ΄ λ©νλ°μ΄ν° μ‘°ν (1-3μ΄, APIλ§, νμΌ λ€μ΄λ‘λ μμ)final metadata = await analyzer.fetchMetadataOnly('https://github.com/flutter/flutter'); -
μ€μ κ²μ¦ μμ€ν : λͺ¨λ μ€μ κ°μ μμ± μμ μ κ²μ¦νμ¬ λ°νμ μ€λ₯ λ°©μ§
maxFileSize> 0 μ΄μ΄μΌ ν¨maxConcurrentRequestsλ²μ: 1-20isolatePoolSizeλ²μ: 1-16maxRetriesλ²μ: 0-10retryDelayμ΅λ: 60μ΄
-
μλ‘μ΄ μ€μ μ΅μ :
enableFileCache: νμΌ μμ€ μΊμ± μ μ΄ (κΈ°λ³Έκ°: true)autoIsolatePoolThreshold: Nκ° νμΌμμ λ³λ ¬ μ²λ¦¬ μλ νμ±ν (κΈ°λ³Έκ°: 100)streamingModeThreshold: μ€νΈλ¦¬λ° λͺ¨λ μμΉ΄μ΄λΈ ν¬κΈ° μκ³κ° (κΈ°λ³Έκ°: 50MB)shouldUseStreamingMode(): λμ μ€νΈλ¦¬λ° κ²°μ λ©μλ
-
보μ: ν ν° λ§μ€νΉ: GitHub ν ν°μ΄ λ‘κ·Έμμ μλμΌλ‘ λ§μ€νΉλ¨
- μ΄μ :
ghp_reallyLongTokenHere123456789xyz9 - μ΄ν:
ghp_real...xyz9 SensitiveLoggerμ νΈλ¦¬ν° ν΄λμ€ μΆκ°- ν ν°μ΄ λ μ΄μ λλ²κ·Έ λ‘κ·Έλ
toString()μΆλ ₯μ λ ΈμΆλμ§ μμ
- μ΄μ :
-
μ’ ν© demo.dart: λͺ¨λ κΈ°λ₯μ λ€λ£¨λ 7κ°μ§ μμ μλ리μ€
μμ λ¨ #
- λλ½λ μ€μ νλ:
enableFileCache,autoIsolatePoolThreshold,streamingModeThreshold - μ μλμ§ μμ
AnalyzerErrorCode.invalidInput-AnalyzerErrorCode.invalidUrlλ‘ κ΅μ²΄ - μ ν¨νμ§ μμ μ μ₯μ URLμ λν΄
fetchMetadataOnly()μ μ€λ₯ μ²λ¦¬ κ°μ - λͺ νν μ μ½ μ‘°κ±΄μ΄ μλ κ²μ¦ μ€λ₯ λ©μμ§ ν₯μ
λ³κ²½λ¨ #
-
μ£Όμ λ³κ²½: λͺ¨λ Freezed λͺ¨λΈμ μ΄μ
abstractν€μλ νμ (Freezed 3.0.0 νΈνμ±)// μ΄μ @freezed class AnalysisError with _$AnalysisError { } // μ΄ν @freezed abstract class AnalysisError with _$AnalysisError { } -
GithubAnalyzerConfigμtoString()μΆλ ₯ ν₯μ (λ§μ€νΉλ ν ν° ν¬ν¨) -
GithubAnalyzerλ μ΄μ λ©νλ°μ΄ν° μ‘°νμ λν μ§ν μν© μΆμ ν¬ν¨ -
μ€μ κ²μ¦μ΄ μ ν¨νμ§ μμ κ°μ λν΄ μ€λͺ μ μΈ
ArgumentErrorλμ§
λ§μ΄κ·Έλ μ΄μ νμ #
dart run build_runner clean
dart run build_runner build --delete-conflicting-outputs
μ£Όμ λ³κ²½μ¬ν #
- Freezed λͺ¨λΈ: λͺ¨λ λͺ¨λΈ ν΄λμ€μ
abstractλλsealedν€μλ νμ - μ€μ κ²μ¦: μ ν¨νμ§ μμ μ€μ κ°μ΄ μμ± μμ μ
ArgumentErrorλμ§ - νμΌ μ¬μμ±: λͺ¨λ
.freezed.dartλ°.g.dartνμΌμ μ¬μμ±ν΄μΌ ν¨
μ±λ₯ κ°μ #
- λ©νλ°μ΄ν° μ μ© μ‘°νλ‘ API νΈμΆ λ° μλ΅ μκ° λ¨μΆ (10-60s β 1-3s)
- νμΌ μ μκ³κ°μ κΈ°λ°μΌλ‘ isolate ν μλ νμ±ν
- 50MB μ΄μ μμΉ΄μ΄λΈμ λν μ€νΈλ¦¬λ° λͺ¨λλ‘ λ©λͺ¨λ¦¬ μ¬μ©λ κ°μ
μ λ°μ΄νΈλ λͺ¨λΈ (Freezed 3.0.0) #
AnalysisErrorAnalysisProgressAnalysisResultAnalysisStatisticsRepositoryMetadataSourceFile
λͺ¨λ λͺ¨λΈμ΄ μ΄μ μ 곡νλ κΈ°λ₯:
- μλ
copyWith() - μλ
toJson()/fromJson() - μλ
==λ°hashCode - κΈ°λ³Έμ μΌλ‘ λΆλ³μ±
- 68% 보μΌλ¬νλ μ΄νΈ μ½λ κ°μ
[0.0.8] - 2025-10-29 #
μΆκ°λ¨ #
- λͺ
μμ μΊμ μ μ΄ νλΌλ―Έν°: λͺ¨λ λΆμ ν¨μμ
useCacheνλΌλ―Έν° μΆκ°analyze()ν¨μλ μ΄μ useCacheνλΌλ―Έν° νμ©analyzeQuick()ν¨μλ μ΄μ useCacheνλΌλ―Έν° νμ©analyzeForLLM()ν¨μλ μ΄μ useCacheνλΌλ―Έν° νμ©analyzeAndGenerate()ν¨μλ μ΄μ useCacheνλΌλ―Έν° νμ©GithubAnalyzer.analyze()λ©μλλ μ΄μ useCacheνλΌλ―Έν° νμ©GithubAnalyzer.analyzeRemote()λ©μλλ μ΄μ useCacheνλΌλ―Έν° νμ©
λ³κ²½λ¨ #
- ν¨μ νΈμΆ λ 벨μμ μΊμ λμμ λͺ μμ μΌλ‘ μ μ΄ κ°λ₯
useCacheλ₯Ό μ§μ νμ§ μμΌλ©΄GithubAnalyzerConfig.enableCacheμ κΈ°λ³Έκ° μ¬μ©useCacheλ₯Ό λͺ μμ μΌλ‘falseλ‘ μ€μ νλ©΄ μ€μ κ΄κ³μμ΄ μΊμ 무μ
μ¬μ© μμ #
// νΉμ λΆμμ λν΄ μΊμ λΉνμ±ν
final result = await analyzeQuick(
'https://github.com/flutter/flutter',
useCache: false, // νμ μ΅μ λ°μ΄ν° κ°μ Έμ€κΈ°
);
// λλ κ³ κΈ API μ¬μ©
final analyzer = await GithubAnalyzer.create();
final result = await analyzer.analyzeRemote(
repositoryUrl: 'https://github.com/your/repo',
useCache: false,
);
μ₯μ #
- μ¬μ©μλ νμν λ κ°μ λ‘ μ΅μ λ°μ΄ν° μ‘°ν κ°λ₯
- CI/CD νμ΄νλΌμΈμ΄ μ΅μ μ μ₯μ μν νμν λ μ μ©
- μ€μ λ³κ²½ μμ΄ μ μ°μ± μ 곡
[0.0.7] - 2025-10-19 #
μμ λ¨ #
-
μΉλͺ μ μΊμ± λ‘μ§ μμ : μ νΈμ μ§ν μ μ₯μλ₯Ό λΆμν λ μ΄μ 컀λ°μ μ€λλ λ°μ΄ν°λ₯Ό λ°νν μ μλ μ£Όμ λ²κ·Έ ν΄κ²°
-
λΆμκΈ°λ μ΄μ μΊμ νμΈ λλ λ€μ΄λ‘λ μ μ λμ λΈλμΉμ μ΅μ μ»€λ° SHAλ₯Ό λͺ μμ μΌλ‘ μ‘°ν
-
μ΄ μ νν commitShaλ μ΄μ μΊμ ν€ λ° λ€μ΄λ‘λ μ°Έμ‘°λ‘ μΌκ΄λκ² μ¬μ©λμ΄ GitHub API 볡μ μ§μ°μΌλ‘ μΈν λ μ΄μ€ 컨λμ λ° μΊμ μ€μΌ μ κ±°
-
μΈμ¦ νΈνμ± κ°μ : λͺ¨λ GitHub API μμ²μ Authorization: Bearer $token ν€λλ‘ νμ€ν. ν΄λμ κ°μΈ μ‘μΈμ€ ν ν°(PAT) λ° μλ‘μ΄ μΈλΆνλ PAT λͺ¨λμμ νΈνμ± λ³΄μ₯
-
HTTP μ¬μλ λ²κ·Έ μμ : HttpClientManagerμ μ¬μλ λ‘μ§μμ μκ° μ΄κ³Ό μμ²μ μ¬μλν λ μλͺ»λ URI κ²½λ‘λ₯Ό μ¬μ©νλ λ²κ·Έ μμ , μ λ°μ μΈ λ€νΈμν¬ λ³΅μλ ₯ κ°μ
[0.0.6] - 2025-10-15 #
μΆκ°λ¨ #
- μλ
.envνμΌ λ‘λ: GitHub ν ν°μ΄ μ΄μ .envνμΌμμ μλμΌλ‘ λ‘λλ¨ - EnvLoader μ νΈλ¦¬ν°: μνν νκ²½ λ³μ κ΄λ¦¬λ₯Ό μν μλ‘μ΄
EnvLoaderν΄λμ€ - λΉκ³΅κ° μ μ₯μ μ§μ: λΉκ³΅κ° μ μ₯μλ₯Ό μν GitHub API ν΄λ°±μ΄ μλ ν₯μλ ZIP λ€μ΄λ‘λ
- λΉλκΈ° μ€μ ν©ν 리: λͺ¨λ
GithubAnalyzerConfigν©ν 리 λ©μλκ° μ΄μ λΉλκΈ°.envλ‘λ μ§μ - GithubAnalyzer.create(): μλ μμ‘΄μ± μ£Όμ
λ°
.envλ‘λλ₯Ό μ¬μ©ν μλ‘μ΄ ν©ν 리 λ©μλ
λ³κ²½λ¨ #
- μ£Όμ λ³κ²½:
GithubAnalyzerConfig.quick()λ°GithubAnalyzerConfig.forLLM()μ μ΄μ λΉλκΈ° - μ£Όμ λ³κ²½: λΉλκΈ° λ²μ μ νμΌλ‘ λκΈ° μ€μ ν©ν 리 μ κ±°
- κ°μ : ZIP λ€μ΄λ‘λλ μ΄μ λΉκ³΅κ° μ μ₯μλ₯Ό μν΄ λ¨Όμ GitHub APIλ₯Ό μλν ν κ³΅κ° URLλ‘ ν΄λ°±
- ν₯μ: ν ν° μΈμ¦μ΄ μ΄μ μΈλΆνλ κ°μΈ μ‘μΈμ€ ν ν°κ³Ό μννκ² μλ
μμ λ¨ #
- μΈλΆνλ GitHub ν ν°μΌλ‘ λΉκ³΅κ° μ μ₯μ μ κ·Ό μμ
- λΉκ³΅κ° μ μ₯μ μ κ·Ό μ 403 μ€λ₯ μμ
- ZIP λ€μ΄λ‘λ μλν¬μΈνΈμ ν ν°μ΄ μ¬λ°λ₯΄κ² μ λ¬λμ§ μλ λ¬Έμ μμ
- μ μ₯μ μ κ·Ό λ¬Έμ μ λν μ€λ₯ λ©μμ§ κ°μ
λ¬Έμν #
- μ’ ν©μ μΈ μΈλΆνλ ν ν° μ€μ κ°μ΄λ μΆκ°
.envνμΌ μ¬μ© μμ λ‘ README μ λ°μ΄νΈ- λΉκ³΅κ° μ μ₯μ μ κ·Όμ λν λ¬Έμ ν΄κ²° μΉμ μΆκ°
[0.0.5] - 2025-10-14 #
μΆκ°λ¨ #
- μ‘°κ±΄λΆ μ»΄νμΌμ ν¬ν¨ν μΉ νλ«νΌ μ§μ
- ν¬λ‘μ€ νλ«νΌ νΈνμ±μ μν
universal_ioν¨ν€μ§ ν΅ν© - μ’ ν©μ μΈ νμΌ μμ€ν μΆμν κ³μΈ΅
λ³κ²½λ¨ #
- μΉ νΈνμ±μ μν΄
dart:ioμμuniversal_ioλ‘ λ§μ΄κ·Έλ μ΄μ - νλ«νΌ νΉν κΈ°λ₯μ λν μ€λ₯ μ²λ¦¬ κ°μ
μμ λ¨ #
- μΉ νλ«νΌ μ»΄νμΌ μ€λ₯
- μΉμ νμΌ μμ€ν μ κ·Ό λ¬Έμ
[0.0.4] - 2025-10-13 #
μΆκ°λ¨ #
- μ¦λΆ λΆμ μ§μ
- κ°νλ μΊμ± λ©μ»€λμ¦
- μ±λ₯ μ΅μ ν
λ³κ²½λ¨ #
- λκ·λͺ¨ μ μ₯μμ λν λΆμ μλ κ°μ
[0.0.3] - 2025-10-12 #
μΆκ°λ¨ #
- LLM μ΅μ ν μΆλ ₯ νμ
- νμΌ μ°μ μμ μ§μ μμ€ν
- κ°κ²°ν λ§ν¬λ€μ΄ μμ±
[0.0.2] - 2025-10-11 #
μΆκ°λ¨ #
- μ격 μ μ₯μ λΆμ
- λ‘컬 λλ ν 리 λΆμ
- κΈ°λ³Έ μΊμ± μμ€ν
[0.0.1] - 2025-10-10 #
μΆκ°λ¨ #
- μ΄κΈ° 릴리μ€
- κΈ°λ³Έ GitHub μ μ₯μ λΆμ
- λ§ν¬λ€μ΄ μμ±