usemint-cli 0.2.0-beta.4 → 0.2.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -110,6 +110,174 @@ var init_config = __esm({
110
110
  }
111
111
  });
112
112
 
113
+ // src/cli/commands/auth.ts
114
+ var auth_exports = {};
115
+ __export(auth_exports, {
116
+ login: () => login,
117
+ logout: () => logout,
118
+ signup: () => signup,
119
+ whoami: () => whoami
120
+ });
121
+ import chalk from "chalk";
122
+ import boxen from "boxen";
123
+ import { createServer } from "http";
124
+ async function login() {
125
+ if (config2.isAuthenticated()) {
126
+ const email = config2.get("email");
127
+ console.log(chalk.yellow(`
128
+ Already logged in as ${email}`));
129
+ console.log(chalk.dim(" Run `mint logout` to switch accounts.\n"));
130
+ return;
131
+ }
132
+ console.log(chalk.cyan("\n Opening browser to sign in...\n"));
133
+ const token = await waitForOAuthCallback();
134
+ if (!token) {
135
+ console.log(chalk.red("\n Login failed. Try again with `mint login`.\n"));
136
+ return;
137
+ }
138
+ try {
139
+ const res = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
140
+ headers: {
141
+ "apikey": SUPABASE_ANON_KEY,
142
+ "Authorization": `Bearer ${token}`
143
+ }
144
+ });
145
+ if (!res.ok) {
146
+ console.log(chalk.red("\n Invalid token received. Try again.\n"));
147
+ return;
148
+ }
149
+ const user = await res.json();
150
+ config2.setAll({
151
+ apiKey: token,
152
+ userId: user.id,
153
+ email: user.email
154
+ });
155
+ console.log(boxen(
156
+ `${chalk.bold.green("Signed in!")}
157
+
158
+ Email: ${chalk.cyan(user.email)}
159
+ Plan: ${chalk.dim("Free \u2014 20 tasks/day")}
160
+
161
+ ${chalk.dim("Run `mint` to start coding.")}`,
162
+ { padding: 1, borderColor: "green", borderStyle: "round" }
163
+ ));
164
+ } catch (err) {
165
+ console.log(chalk.red(`
166
+ Error: ${err.message}
167
+ `));
168
+ }
169
+ }
170
+ function waitForOAuthCallback() {
171
+ return new Promise((resolve12) => {
172
+ const timeout = setTimeout(() => {
173
+ server.close();
174
+ resolve12(null);
175
+ }, 12e4);
176
+ const server = createServer(async (req, res) => {
177
+ const url = new URL(req.url ?? "/", `http://localhost:${CALLBACK_PORT}`);
178
+ if (url.pathname === "/callback") {
179
+ let token = null;
180
+ if (req.method === "POST") {
181
+ const body = await new Promise((r) => {
182
+ let data = "";
183
+ req.on("data", (chunk) => {
184
+ data += chunk.toString();
185
+ });
186
+ req.on("end", () => r(data));
187
+ });
188
+ try {
189
+ const parsed = JSON.parse(body);
190
+ token = parsed.access_token ?? parsed.token ?? null;
191
+ } catch {
192
+ token = null;
193
+ }
194
+ } else {
195
+ token = url.searchParams.get("access_token") ?? url.searchParams.get("token");
196
+ }
197
+ res.setHeader("Access-Control-Allow-Origin", "*");
198
+ res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
199
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
200
+ res.writeHead(200, { "Content-Type": "text/html" });
201
+ res.end(`
202
+ <html>
203
+ <body style="background:#07090d;color:#c8dae8;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
204
+ <div style="text-align:center">
205
+ <h1 style="color:#00d4ff">Connected!</h1>
206
+ <p>You can close this tab and return to the terminal.</p>
207
+ </div>
208
+ </body>
209
+ </html>
210
+ `);
211
+ clearTimeout(timeout);
212
+ server.close();
213
+ resolve12(token);
214
+ return;
215
+ }
216
+ if (req.method === "OPTIONS") {
217
+ res.setHeader("Access-Control-Allow-Origin", "*");
218
+ res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
219
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
220
+ res.writeHead(204);
221
+ res.end();
222
+ return;
223
+ }
224
+ res.writeHead(404);
225
+ res.end("Not found");
226
+ });
227
+ server.listen(CALLBACK_PORT, () => {
228
+ const callbackUrl = `http://localhost:${CALLBACK_PORT}/callback`;
229
+ const authUrl = `${AUTH_PAGE_URL}?callback=${encodeURIComponent(callbackUrl)}`;
230
+ import("child_process").then(({ execFile }) => {
231
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
232
+ execFile(cmd, [authUrl]);
233
+ });
234
+ });
235
+ server.on("error", () => {
236
+ clearTimeout(timeout);
237
+ resolve12(null);
238
+ });
239
+ });
240
+ }
241
+ async function signup() {
242
+ await login();
243
+ }
244
+ async function logout() {
245
+ if (!config2.isAuthenticated()) {
246
+ console.log(chalk.yellow("\n Not logged in.\n"));
247
+ return;
248
+ }
249
+ const email = config2.get("email");
250
+ config2.clear();
251
+ console.log(chalk.green(`
252
+ Logged out from ${email}
253
+ `));
254
+ }
255
+ async function whoami() {
256
+ if (!config2.isAuthenticated()) {
257
+ console.log(chalk.yellow("\n Not logged in."));
258
+ console.log(chalk.dim(" Run `mint login` to sign in.\n"));
259
+ return;
260
+ }
261
+ const email = config2.get("email");
262
+ console.log(boxen(
263
+ `${chalk.bold("Signed in")}
264
+
265
+ Email: ${chalk.cyan(email)}`,
266
+ { padding: 1, borderColor: "cyan", borderStyle: "round" }
267
+ ));
268
+ }
269
+ var SUPABASE_URL, SUPABASE_ANON_KEY, AUTH_PAGE_URL, CALLBACK_PORT;
270
+ var init_auth = __esm({
271
+ "src/cli/commands/auth.ts"() {
272
+ "use strict";
273
+ init_config();
274
+ SUPABASE_URL = process.env.MINT_SUPABASE_URL ?? "https://srhoryezzsjmjdgfoxgd.supabase.co";
275
+ SUPABASE_ANON_KEY = process.env.MINT_SUPABASE_ANON_KEY ?? "";
276
+ AUTH_PAGE_URL = "https://usemint.dev/auth";
277
+ CALLBACK_PORT = 9876;
278
+ }
279
+ });
280
+
113
281
  // src/providers/types.ts
114
282
  var types_exports = {};
115
283
  __export(types_exports, {
@@ -3409,7 +3577,550 @@ describe("projectService.create", () => {
3409
3577
  - [ ] Assertions are specific (not just "toBeTruthy")
3410
3578
  `;
3411
3579
  }
3412
- var cache, SKILL_STYLE, SKILL_PYTHON, SKILL_GENERAL;
3580
+ function SKILL_ANDROID(hasCompose) {
3581
+ const uiSection = hasCompose ? ANDROID_COMPOSE_SECTION : ANDROID_XML_SECTION;
3582
+ return `---
3583
+ applies_to: [android, mobile]
3584
+ ---
3585
+ # Android / Kotlin \u2014 Production Quality Standard
3586
+
3587
+ All generated code must match Google's best Android apps (Gmail, Drive, Maps).
3588
+ Study these reference examples \u2014 this is the bar.
3589
+
3590
+ ## Rules
3591
+ - Kotlin only \u2014 no Java in new code
3592
+ - MVVM with clean architecture: UI \u2192 ViewModel \u2192 UseCase \u2192 Repository \u2192 DataSource
3593
+ - Coroutines + Flow for all async work \u2014 no callbacks, no RxJava in new code
3594
+ - Hilt for dependency injection \u2014 no manual DI or service locators
3595
+ - Single Activity with Jetpack Navigation (or Compose Navigation)
3596
+ - Repository pattern: ViewModel never touches Room/Retrofit directly
3597
+ - Sealed classes for UI state \u2014 never nullable booleans for loading/error
3598
+
3599
+ ${uiSection}
3600
+
3601
+ ## Reference: ViewModel (Google-quality MVVM)
3602
+
3603
+ \`\`\`kotlin
3604
+ @HiltViewModel
3605
+ class ProjectListViewModel @Inject constructor(
3606
+ private val getProjects: GetProjectsUseCase,
3607
+ private val deleteProject: DeleteProjectUseCase,
3608
+ ) : ViewModel() {
3609
+
3610
+ private val _uiState = MutableStateFlow<ProjectListState>(ProjectListState.Loading)
3611
+ val uiState: StateFlow<ProjectListState> = _uiState.asStateFlow()
3612
+
3613
+ private val _events = Channel<ProjectListEvent>(Channel.BUFFERED)
3614
+ val events: Flow<ProjectListEvent> = _events.receiveAsFlow()
3615
+
3616
+ init {
3617
+ loadProjects()
3618
+ }
3619
+
3620
+ fun loadProjects() {
3621
+ viewModelScope.launch {
3622
+ _uiState.value = ProjectListState.Loading
3623
+ getProjects()
3624
+ .catch { e -> _uiState.value = ProjectListState.Error(e.toUserMessage()) }
3625
+ .collect { projects ->
3626
+ _uiState.value = if (projects.isEmpty()) {
3627
+ ProjectListState.Empty
3628
+ } else {
3629
+ ProjectListState.Success(projects)
3630
+ }
3631
+ }
3632
+ }
3633
+ }
3634
+
3635
+ fun onDeleteProject(projectId: String) {
3636
+ viewModelScope.launch {
3637
+ try {
3638
+ deleteProject(projectId)
3639
+ _events.send(ProjectListEvent.ProjectDeleted)
3640
+ loadProjects()
3641
+ } catch (e: Exception) {
3642
+ _events.send(ProjectListEvent.ShowError(e.toUserMessage()))
3643
+ }
3644
+ }
3645
+ }
3646
+ }
3647
+
3648
+ sealed interface ProjectListState {
3649
+ data object Loading : ProjectListState
3650
+ data object Empty : ProjectListState
3651
+ data class Success(val projects: List<Project>) : ProjectListState
3652
+ data class Error(val message: String) : ProjectListState
3653
+ }
3654
+
3655
+ sealed interface ProjectListEvent {
3656
+ data object ProjectDeleted : ProjectListEvent
3657
+ data class ShowError(val message: String) : ProjectListEvent
3658
+ }
3659
+ \`\`\`
3660
+
3661
+ ## Reference: Repository with offline-first caching
3662
+
3663
+ \`\`\`kotlin
3664
+ class ProjectRepositoryImpl @Inject constructor(
3665
+ private val api: ProjectApi,
3666
+ private val dao: ProjectDao,
3667
+ private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
3668
+ ) : ProjectRepository {
3669
+
3670
+ override fun getProjects(): Flow<List<Project>> = flow {
3671
+ // Emit cached data immediately
3672
+ val cached = dao.getAll().first()
3673
+ if (cached.isNotEmpty()) {
3674
+ emit(cached.map { it.toDomain() })
3675
+ }
3676
+
3677
+ // Fetch fresh data from network
3678
+ try {
3679
+ val remote = withContext(dispatcher) { api.getProjects() }
3680
+ dao.replaceAll(remote.map { it.toEntity() })
3681
+ } catch (e: IOException) {
3682
+ if (cached.isEmpty()) throw e
3683
+ // Cached data already emitted \u2014 silently use stale data
3684
+ }
3685
+
3686
+ // Emit fresh data from DB (single source of truth)
3687
+ emitAll(dao.getAll().map { entities -> entities.map { it.toDomain() } })
3688
+ }.flowOn(dispatcher)
3689
+
3690
+ override suspend fun delete(projectId: String) {
3691
+ withContext(dispatcher) {
3692
+ api.deleteProject(projectId)
3693
+ dao.deleteById(projectId)
3694
+ }
3695
+ }
3696
+ }
3697
+ \`\`\`
3698
+
3699
+ ## Reference: UseCase (clean architecture boundary)
3700
+
3701
+ \`\`\`kotlin
3702
+ class GetProjectsUseCase @Inject constructor(
3703
+ private val repository: ProjectRepository,
3704
+ ) {
3705
+ operator fun invoke(): Flow<List<Project>> = repository.getProjects()
3706
+ }
3707
+
3708
+ class DeleteProjectUseCase @Inject constructor(
3709
+ private val repository: ProjectRepository,
3710
+ private val analytics: AnalyticsTracker,
3711
+ ) {
3712
+ suspend operator fun invoke(projectId: String) {
3713
+ repository.delete(projectId)
3714
+ analytics.track("project_deleted", mapOf("id" to projectId))
3715
+ }
3716
+ }
3717
+ \`\`\`
3718
+
3719
+ ## Reference: Hilt module
3720
+
3721
+ \`\`\`kotlin
3722
+ @Module
3723
+ @InstallIn(SingletonComponent::class)
3724
+ abstract class RepositoryModule {
3725
+ @Binds
3726
+ abstract fun bindProjectRepository(impl: ProjectRepositoryImpl): ProjectRepository
3727
+ }
3728
+
3729
+ @Module
3730
+ @InstallIn(SingletonComponent::class)
3731
+ object NetworkModule {
3732
+ @Provides
3733
+ @Singleton
3734
+ fun provideRetrofit(): Retrofit = Retrofit.Builder()
3735
+ .baseUrl(BuildConfig.API_BASE_URL)
3736
+ .addConverterFactory(MoshiConverterFactory.create())
3737
+ .client(
3738
+ OkHttpClient.Builder()
3739
+ .addInterceptor(AuthInterceptor())
3740
+ .addInterceptor(HttpLoggingInterceptor().apply {
3741
+ level = if (BuildConfig.DEBUG) Level.BODY else Level.NONE
3742
+ })
3743
+ .connectTimeout(15, TimeUnit.SECONDS)
3744
+ .readTimeout(15, TimeUnit.SECONDS)
3745
+ .build()
3746
+ )
3747
+ .build()
3748
+
3749
+ @Provides
3750
+ @Singleton
3751
+ fun provideProjectApi(retrofit: Retrofit): ProjectApi =
3752
+ retrofit.create(ProjectApi::class.java)
3753
+ }
3754
+ \`\`\`
3755
+
3756
+ ## Reference: Room entity + DAO
3757
+
3758
+ \`\`\`kotlin
3759
+ @Entity(tableName = "projects")
3760
+ data class ProjectEntity(
3761
+ @PrimaryKey val id: String,
3762
+ val name: String,
3763
+ val description: String?,
3764
+ @ColumnInfo(name = "team_id") val teamId: String,
3765
+ @ColumnInfo(name = "created_at") val createdAt: Long,
3766
+ @ColumnInfo(name = "updated_at") val updatedAt: Long,
3767
+ ) {
3768
+ fun toDomain() = Project(
3769
+ id = id,
3770
+ name = name,
3771
+ description = description,
3772
+ teamId = teamId,
3773
+ createdAt = Instant.ofEpochMilli(createdAt),
3774
+ )
3775
+ }
3776
+
3777
+ @Dao
3778
+ interface ProjectDao {
3779
+ @Query("SELECT * FROM projects ORDER BY updated_at DESC")
3780
+ fun getAll(): Flow<List<ProjectEntity>>
3781
+
3782
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
3783
+ suspend fun insertAll(projects: List<ProjectEntity>)
3784
+
3785
+ @Query("DELETE FROM projects")
3786
+ suspend fun deleteAll()
3787
+
3788
+ @Transaction
3789
+ suspend fun replaceAll(projects: List<ProjectEntity>) {
3790
+ deleteAll()
3791
+ insertAll(projects)
3792
+ }
3793
+
3794
+ @Query("DELETE FROM projects WHERE id = :id")
3795
+ suspend fun deleteById(id: String)
3796
+ }
3797
+ \`\`\`
3798
+
3799
+ ## Quality checklist (reviewer must verify)
3800
+ - [ ] UI state is a sealed class/interface \u2014 no nullable boolean flags
3801
+ - [ ] ViewModel uses StateFlow (not LiveData) for new code
3802
+ - [ ] Coroutine scope is viewModelScope \u2014 never GlobalScope
3803
+ - [ ] Repository is the single source of truth (DB, not network)
3804
+ - [ ] Hilt @Inject on every constructor \u2014 no manual instantiation
3805
+ - [ ] Error messages are user-friendly \u2014 never raw exception text
3806
+ - [ ] Lifecycle-aware collection (collectAsStateWithLifecycle or repeatOnLifecycle)
3807
+ - [ ] No hardcoded strings in UI \u2014 use string resources
3808
+ - [ ] ProGuard/R8 rules for any reflection-based libraries
3809
+ `;
3810
+ }
3811
+ function SKILL_DEVOPS(hasDocker, hasCI, hasTerraform, hasK8s) {
3812
+ const sections = [];
3813
+ sections.push(`---
3814
+ applies_to: [devops, infrastructure]
3815
+ ---
3816
+ # DevOps \u2014 Production Quality Standard
3817
+
3818
+ Infrastructure as code, reproducible builds, zero-downtime deploys.`);
3819
+ sections.push(`
3820
+ ## Rules
3821
+ - Every environment is reproducible from code \u2014 no manual server config
3822
+ - Secrets in vault/env \u2014 never in code, never in Docker images, never in CI logs
3823
+ - Health checks on every service \u2014 liveness and readiness
3824
+ - Logs are structured JSON \u2014 never print() or console.log for production logging
3825
+ - Rollback plan exists for every deploy
3826
+ - Least privilege: containers run as non-root, IAM roles are scoped`);
3827
+ if (hasDocker) {
3828
+ sections.push(`
3829
+ ## Reference: Production Dockerfile (multi-stage, secure)
3830
+
3831
+ \`\`\`dockerfile
3832
+ # Build stage
3833
+ FROM node:20-alpine AS builder
3834
+ WORKDIR /app
3835
+ COPY package.json package-lock.json ./
3836
+ RUN npm ci --ignore-scripts
3837
+ COPY . .
3838
+ RUN npm run build && npm prune --production
3839
+
3840
+ # Production stage
3841
+ FROM node:20-alpine AS runner
3842
+ WORKDIR /app
3843
+
3844
+ # Security: non-root user
3845
+ RUN addgroup --system --gid 1001 appgroup && \\
3846
+ adduser --system --uid 1001 appuser
3847
+
3848
+ # Only copy what's needed
3849
+ COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
3850
+ COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
3851
+ COPY --from=builder --chown=appuser:appgroup /app/package.json ./
3852
+
3853
+ # Health check
3854
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
3855
+ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
3856
+
3857
+ USER appuser
3858
+ EXPOSE 3000
3859
+ ENV NODE_ENV=production
3860
+
3861
+ CMD ["node", "dist/server.js"]
3862
+ \`\`\`
3863
+
3864
+ ## Reference: docker-compose for local dev
3865
+
3866
+ \`\`\`yaml
3867
+ services:
3868
+ app:
3869
+ build:
3870
+ context: .
3871
+ target: builder # Use build stage for dev (has devDependencies)
3872
+ ports:
3873
+ - "3000:3000"
3874
+ volumes:
3875
+ - .:/app
3876
+ - /app/node_modules # Don't mount over node_modules
3877
+ environment:
3878
+ - DATABASE_URL=postgresql://postgres:postgres@db:5432/app_dev
3879
+ - REDIS_URL=redis://redis:6379
3880
+ - NODE_ENV=development
3881
+ depends_on:
3882
+ db:
3883
+ condition: service_healthy
3884
+ redis:
3885
+ condition: service_started
3886
+
3887
+ db:
3888
+ image: postgres:16-alpine
3889
+ environment:
3890
+ POSTGRES_DB: app_dev
3891
+ POSTGRES_USER: postgres
3892
+ POSTGRES_PASSWORD: postgres
3893
+ ports:
3894
+ - "5432:5432"
3895
+ volumes:
3896
+ - pgdata:/var/lib/postgresql/data
3897
+ healthcheck:
3898
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
3899
+ interval: 5s
3900
+ timeout: 3s
3901
+ retries: 5
3902
+
3903
+ redis:
3904
+ image: redis:7-alpine
3905
+ ports:
3906
+ - "6379:6379"
3907
+
3908
+ volumes:
3909
+ pgdata:
3910
+ \`\`\``);
3911
+ }
3912
+ if (hasCI) {
3913
+ sections.push(`
3914
+ ## Reference: GitHub Actions CI/CD pipeline
3915
+
3916
+ \`\`\`yaml
3917
+ name: CI/CD
3918
+
3919
+ on:
3920
+ push:
3921
+ branches: [main]
3922
+ pull_request:
3923
+ branches: [main]
3924
+
3925
+ concurrency:
3926
+ group: \${{ github.workflow }}-\${{ github.ref }}
3927
+ cancel-in-progress: true
3928
+
3929
+ jobs:
3930
+ test:
3931
+ runs-on: ubuntu-latest
3932
+ services:
3933
+ postgres:
3934
+ image: postgres:16-alpine
3935
+ env:
3936
+ POSTGRES_DB: test
3937
+ POSTGRES_USER: postgres
3938
+ POSTGRES_PASSWORD: postgres
3939
+ ports:
3940
+ - 5432:5432
3941
+ options: >-
3942
+ --health-cmd "pg_isready"
3943
+ --health-interval 10s
3944
+ --health-timeout 5s
3945
+ --health-retries 5
3946
+ steps:
3947
+ - uses: actions/checkout@v4
3948
+ - uses: actions/setup-node@v4
3949
+ with:
3950
+ node-version: 20
3951
+ cache: npm
3952
+ - run: npm ci
3953
+ - run: npm run typecheck
3954
+ - run: npm run lint
3955
+ - run: npm test
3956
+ env:
3957
+ DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
3958
+
3959
+ build:
3960
+ needs: test
3961
+ runs-on: ubuntu-latest
3962
+ if: github.ref == 'refs/heads/main'
3963
+ permissions:
3964
+ contents: read
3965
+ packages: write
3966
+ steps:
3967
+ - uses: actions/checkout@v4
3968
+ - uses: docker/setup-buildx-action@v3
3969
+ - uses: docker/login-action@v3
3970
+ with:
3971
+ registry: ghcr.io
3972
+ username: \${{ github.actor }}
3973
+ password: \${{ secrets.GITHUB_TOKEN }}
3974
+ - uses: docker/build-push-action@v5
3975
+ with:
3976
+ push: true
3977
+ tags: ghcr.io/\${{ github.repository }}:\${{ github.sha }}
3978
+ cache-from: type=gha
3979
+ cache-to: type=gha,mode=max
3980
+
3981
+ deploy:
3982
+ needs: build
3983
+ runs-on: ubuntu-latest
3984
+ if: github.ref == 'refs/heads/main'
3985
+ environment: production
3986
+ steps:
3987
+ - name: Deploy to production
3988
+ run: |
3989
+ # Replace with your deploy command
3990
+ echo "Deploying ghcr.io/\${{ github.repository }}:\${{ github.sha }}"
3991
+ \`\`\``);
3992
+ }
3993
+ if (hasTerraform) {
3994
+ sections.push(`
3995
+ ## Reference: Terraform module structure
3996
+
3997
+ \`\`\`hcl
3998
+ # main.tf
3999
+ terraform {
4000
+ required_version = ">= 1.5"
4001
+ required_providers {
4002
+ aws = { source = "hashicorp/aws", version = "~> 5.0" }
4003
+ }
4004
+ backend "s3" {
4005
+ bucket = "myapp-terraform-state"
4006
+ key = "prod/terraform.tfstate"
4007
+ region = "us-east-1"
4008
+ dynamodb_table = "terraform-locks"
4009
+ encrypt = true
4010
+ }
4011
+ }
4012
+
4013
+ resource "aws_ecs_service" "app" {
4014
+ name = "\${var.app_name}-\${var.environment}"
4015
+ cluster = aws_ecs_cluster.main.id
4016
+ task_definition = aws_ecs_task_definition.app.arn
4017
+ desired_count = var.min_capacity
4018
+ launch_type = "FARGATE"
4019
+
4020
+ network_configuration {
4021
+ subnets = var.private_subnet_ids
4022
+ security_groups = [aws_security_group.app.id]
4023
+ assign_public_ip = false
4024
+ }
4025
+
4026
+ load_balancer {
4027
+ target_group_arn = aws_lb_target_group.app.arn
4028
+ container_name = var.app_name
4029
+ container_port = var.container_port
4030
+ }
4031
+
4032
+ deployment_circuit_breaker {
4033
+ enable = true
4034
+ rollback = true
4035
+ }
4036
+
4037
+ lifecycle {
4038
+ ignore_changes = [desired_count] # Managed by auto-scaling
4039
+ }
4040
+ }
4041
+ \`\`\``);
4042
+ }
4043
+ if (hasK8s) {
4044
+ sections.push(`
4045
+ ## Reference: Kubernetes deployment with best practices
4046
+
4047
+ \`\`\`yaml
4048
+ apiVersion: apps/v1
4049
+ kind: Deployment
4050
+ metadata:
4051
+ name: app
4052
+ labels:
4053
+ app.kubernetes.io/name: app
4054
+ app.kubernetes.io/version: "1.0.0"
4055
+ spec:
4056
+ replicas: 3
4057
+ strategy:
4058
+ type: RollingUpdate
4059
+ rollingUpdate:
4060
+ maxSurge: 1
4061
+ maxUnavailable: 0
4062
+ selector:
4063
+ matchLabels:
4064
+ app.kubernetes.io/name: app
4065
+ template:
4066
+ metadata:
4067
+ labels:
4068
+ app.kubernetes.io/name: app
4069
+ spec:
4070
+ securityContext:
4071
+ runAsNonRoot: true
4072
+ runAsUser: 1001
4073
+ fsGroup: 1001
4074
+ containers:
4075
+ - name: app
4076
+ image: ghcr.io/org/app:latest
4077
+ ports:
4078
+ - containerPort: 3000
4079
+ protocol: TCP
4080
+ env:
4081
+ - name: DATABASE_URL
4082
+ valueFrom:
4083
+ secretKeyRef:
4084
+ name: app-secrets
4085
+ key: database-url
4086
+ resources:
4087
+ requests:
4088
+ cpu: 100m
4089
+ memory: 128Mi
4090
+ limits:
4091
+ cpu: 500m
4092
+ memory: 512Mi
4093
+ livenessProbe:
4094
+ httpGet:
4095
+ path: /health
4096
+ port: 3000
4097
+ initialDelaySeconds: 10
4098
+ periodSeconds: 30
4099
+ readinessProbe:
4100
+ httpGet:
4101
+ path: /health
4102
+ port: 3000
4103
+ initialDelaySeconds: 5
4104
+ periodSeconds: 10
4105
+ securityContext:
4106
+ allowPrivilegeEscalation: false
4107
+ readOnlyRootFilesystem: true
4108
+ \`\`\``);
4109
+ }
4110
+ sections.push(`
4111
+ ## Quality checklist (reviewer must verify)
4112
+ - [ ] No secrets in code, Dockerfiles, or CI logs \u2014 use env vars or secret stores
4113
+ - [ ] Containers run as non-root with read-only filesystem where possible
4114
+ - [ ] Health checks on every service (liveness + readiness)
4115
+ - [ ] CI runs typecheck + lint + test before deploy
4116
+ - [ ] Deploys are zero-downtime (rolling update, not recreate)
4117
+ - [ ] Resource limits set on all containers
4118
+ - [ ] State is external (DB, Redis, S3) \u2014 containers are stateless
4119
+ - [ ] Rollback is one command or automatic on failure
4120
+ `);
4121
+ return sections.join("\n");
4122
+ }
4123
+ var cache, SKILL_STYLE, SKILL_PYTHON, ANDROID_COMPOSE_SECTION, ANDROID_XML_SECTION, SKILL_FULLSTACK, SKILL_GENERAL;
3413
4124
  var init_project_rules = __esm({
3414
4125
  "src/context/project-rules.ts"() {
3415
4126
  "use strict";
@@ -3559,6 +4270,349 @@ class ProjectService:
3559
4270
  member = await self._db.team_members.find_one(user_id=user_id, team_id=team_id)
3560
4271
  return member is not None
3561
4272
  \`\`\`
4273
+ `;
4274
+ ANDROID_COMPOSE_SECTION = `
4275
+ ## Reference: Compose UI (Material3 quality)
4276
+
4277
+ \`\`\`kotlin
4278
+ @Composable
4279
+ fun ProjectListScreen(
4280
+ viewModel: ProjectListViewModel = hiltViewModel(),
4281
+ onNavigateToDetail: (String) -> Unit,
4282
+ ) {
4283
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
4284
+ val snackbarHostState = remember { SnackbarHostState() }
4285
+
4286
+ LaunchedEffect(Unit) {
4287
+ viewModel.events.collect { event ->
4288
+ when (event) {
4289
+ is ProjectListEvent.ProjectDeleted ->
4290
+ snackbarHostState.showSnackbar("Project deleted")
4291
+ is ProjectListEvent.ShowError ->
4292
+ snackbarHostState.showSnackbar(event.message)
4293
+ }
4294
+ }
4295
+ }
4296
+
4297
+ Scaffold(
4298
+ snackbarHost = { SnackbarHost(snackbarHostState) },
4299
+ topBar = {
4300
+ TopAppBar(title = { Text("Projects") })
4301
+ },
4302
+ floatingActionButton = {
4303
+ FloatingActionButton(onClick = { /* navigate to create */ }) {
4304
+ Icon(Icons.Default.Add, contentDescription = "Create project")
4305
+ }
4306
+ },
4307
+ ) { padding ->
4308
+ when (val state = uiState) {
4309
+ is ProjectListState.Loading -> {
4310
+ Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
4311
+ CircularProgressIndicator()
4312
+ }
4313
+ }
4314
+ is ProjectListState.Empty -> {
4315
+ EmptyState(
4316
+ modifier = Modifier.fillMaxSize().padding(padding),
4317
+ icon = Icons.Outlined.Folder,
4318
+ title = "No projects yet",
4319
+ subtitle = "Create your first project to get started",
4320
+ )
4321
+ }
4322
+ is ProjectListState.Error -> {
4323
+ ErrorState(
4324
+ modifier = Modifier.fillMaxSize().padding(padding),
4325
+ message = state.message,
4326
+ onRetry = viewModel::loadProjects,
4327
+ )
4328
+ }
4329
+ is ProjectListState.Success -> {
4330
+ LazyColumn(
4331
+ modifier = Modifier.fillMaxSize().padding(padding),
4332
+ contentPadding = PaddingValues(16.dp),
4333
+ verticalArrangement = Arrangement.spacedBy(8.dp),
4334
+ ) {
4335
+ items(state.projects, key = { it.id }) { project ->
4336
+ ProjectCard(
4337
+ project = project,
4338
+ onClick = { onNavigateToDetail(project.id) },
4339
+ onDelete = { viewModel.onDeleteProject(project.id) },
4340
+ )
4341
+ }
4342
+ }
4343
+ }
4344
+ }
4345
+ }
4346
+ }
4347
+
4348
+ @Composable
4349
+ private fun ProjectCard(
4350
+ project: Project,
4351
+ onClick: () -> Unit,
4352
+ onDelete: () -> Unit,
4353
+ modifier: Modifier = Modifier,
4354
+ ) {
4355
+ Card(
4356
+ onClick = onClick,
4357
+ modifier = modifier.fillMaxWidth(),
4358
+ ) {
4359
+ Row(
4360
+ modifier = Modifier.padding(16.dp),
4361
+ verticalAlignment = Alignment.CenterVertically,
4362
+ ) {
4363
+ Column(modifier = Modifier.weight(1f)) {
4364
+ Text(
4365
+ text = project.name,
4366
+ style = MaterialTheme.typography.titleMedium,
4367
+ maxLines = 1,
4368
+ overflow = TextOverflow.Ellipsis,
4369
+ )
4370
+ Text(
4371
+ text = project.description ?: stringResource(R.string.no_description),
4372
+ style = MaterialTheme.typography.bodySmall,
4373
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
4374
+ maxLines = 2,
4375
+ overflow = TextOverflow.Ellipsis,
4376
+ )
4377
+ }
4378
+ IconButton(onClick = onDelete) {
4379
+ Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.delete))
4380
+ }
4381
+ }
4382
+ }
4383
+ }
4384
+ \`\`\``;
4385
+ ANDROID_XML_SECTION = `
4386
+ ## Reference: Fragment with ViewBinding (XML UI)
4387
+
4388
+ \`\`\`kotlin
4389
+ @AndroidEntryPoint
4390
+ class ProjectListFragment : Fragment(R.layout.fragment_project_list) {
4391
+
4392
+ private val viewModel: ProjectListViewModel by viewModels()
4393
+ private var _binding: FragmentProjectListBinding? = null
4394
+ private val binding get() = _binding!!
4395
+ private lateinit var adapter: ProjectAdapter
4396
+
4397
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
4398
+ super.onViewCreated(view, savedInstanceState)
4399
+ _binding = FragmentProjectListBinding.bind(view)
4400
+
4401
+ adapter = ProjectAdapter(
4402
+ onClick = { project -> findNavController().navigate(
4403
+ ProjectListFragmentDirections.actionToDetail(project.id)
4404
+ )},
4405
+ onDelete = { project -> viewModel.onDeleteProject(project.id) },
4406
+ )
4407
+ binding.recyclerView.adapter = adapter
4408
+ binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
4409
+
4410
+ viewLifecycleOwner.lifecycleScope.launch {
4411
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
4412
+ launch {
4413
+ viewModel.uiState.collect { state ->
4414
+ binding.progressBar.isVisible = state is ProjectListState.Loading
4415
+ binding.emptyView.isVisible = state is ProjectListState.Empty
4416
+ binding.errorView.isVisible = state is ProjectListState.Error
4417
+ binding.recyclerView.isVisible = state is ProjectListState.Success
4418
+ if (state is ProjectListState.Success) adapter.submitList(state.projects)
4419
+ if (state is ProjectListState.Error) binding.errorMessage.text = state.message
4420
+ }
4421
+ }
4422
+ launch {
4423
+ viewModel.events.collect { event ->
4424
+ when (event) {
4425
+ is ProjectListEvent.ProjectDeleted ->
4426
+ Snackbar.make(binding.root, "Deleted", Snackbar.LENGTH_SHORT).show()
4427
+ is ProjectListEvent.ShowError ->
4428
+ Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG).show()
4429
+ }
4430
+ }
4431
+ }
4432
+ }
4433
+ }
4434
+
4435
+ binding.fab.setOnClickListener {
4436
+ findNavController().navigate(ProjectListFragmentDirections.actionToCreate())
4437
+ }
4438
+ binding.retryButton.setOnClickListener { viewModel.loadProjects() }
4439
+ }
4440
+
4441
+ override fun onDestroyView() {
4442
+ super.onDestroyView()
4443
+ _binding = null
4444
+ }
4445
+ }
4446
+ \`\`\``;
4447
+ SKILL_FULLSTACK = `---
4448
+ applies_to: [fullstack]
4449
+ ---
4450
+ # Fullstack \u2014 Production Quality Standard
4451
+
4452
+ For projects with both frontend and backend. Patterns from Vercel, Linear, Cal.com.
4453
+
4454
+ ## Rules
4455
+ - Shared types between frontend and backend \u2014 define once, import in both
4456
+ - API client is a typed wrapper \u2014 never raw fetch() scattered in components
4457
+ - Environment variables: NEXT_PUBLIC_ for client, plain for server \u2014 never leak server secrets
4458
+ - Validation schemas shared: same Zod schema validates on client AND server
4459
+ - Error boundaries in frontend, structured errors from backend
4460
+ - Optimistic UI updates where latency matters, with rollback on failure
4461
+
4462
+ ## Reference: Shared types (single source of truth)
4463
+
4464
+ \`\`\`ts
4465
+ // packages/shared/types.ts (or lib/types.ts)
4466
+ export interface Project {
4467
+ id: string;
4468
+ name: string;
4469
+ description: string | null;
4470
+ status: "active" | "paused" | "archived";
4471
+ createdAt: string;
4472
+ updatedAt: string;
4473
+ }
4474
+
4475
+ export interface ApiResponse<T> {
4476
+ success: boolean;
4477
+ data: T | null;
4478
+ error: string | null;
4479
+ }
4480
+
4481
+ export interface PaginatedResponse<T> extends ApiResponse<T[]> {
4482
+ meta: {
4483
+ page: number;
4484
+ limit: number;
4485
+ total: number;
4486
+ hasMore: boolean;
4487
+ };
4488
+ }
4489
+
4490
+ // Validation \u2014 used by both frontend forms and backend handlers
4491
+ export const CreateProjectSchema = z.object({
4492
+ name: z.string().min(1, "Name is required").max(100).trim(),
4493
+ description: z.string().max(500).optional(),
4494
+ });
4495
+ export type CreateProjectInput = z.infer<typeof CreateProjectSchema>;
4496
+ \`\`\`
4497
+
4498
+ ## Reference: Typed API client (frontend)
4499
+
4500
+ \`\`\`ts
4501
+ // lib/api.ts
4502
+ class ApiClient {
4503
+ private baseUrl: string;
4504
+
4505
+ constructor(baseUrl = "/api") {
4506
+ this.baseUrl = baseUrl;
4507
+ }
4508
+
4509
+ private async request<T>(path: string, opts: RequestInit = {}): Promise<T> {
4510
+ const res = await fetch(\`\${this.baseUrl}\${path}\`, {
4511
+ headers: {
4512
+ "Content-Type": "application/json",
4513
+ ...opts.headers,
4514
+ },
4515
+ ...opts,
4516
+ });
4517
+
4518
+ if (!res.ok) {
4519
+ const body = await res.json().catch(() => ({}));
4520
+ throw new ApiError(body.error ?? "Request failed", res.status);
4521
+ }
4522
+
4523
+ if (res.status === 204) return undefined as T;
4524
+ return res.json();
4525
+ }
4526
+
4527
+ projects = {
4528
+ list: (params?: { page?: number; search?: string }) =>
4529
+ this.request<PaginatedResponse<Project>>(
4530
+ \`/projects?\${new URLSearchParams(params as Record<string, string>)}\`,
4531
+ ),
4532
+
4533
+ getById: (id: string) =>
4534
+ this.request<ApiResponse<Project>>(\`/projects/\${id}\`),
4535
+
4536
+ create: (input: CreateProjectInput) =>
4537
+ this.request<ApiResponse<Project>>("/projects", {
4538
+ method: "POST",
4539
+ body: JSON.stringify(input),
4540
+ }),
4541
+
4542
+ delete: (id: string) =>
4543
+ this.request<void>(\`/projects/\${id}\`, { method: "DELETE" }),
4544
+ };
4545
+ }
4546
+
4547
+ export const api = new ApiClient();
4548
+ \`\`\`
4549
+
4550
+ ## Reference: Data fetching hook with SWR/React Query pattern
4551
+
4552
+ \`\`\`tsx
4553
+ function useProjects(params?: { page?: number; search?: string }) {
4554
+ const [state, setState] = useState<{
4555
+ data: Project[] | null;
4556
+ meta: PaginatedResponse<Project>["meta"] | null;
4557
+ loading: boolean;
4558
+ error: string | null;
4559
+ }>({ data: null, meta: null, loading: true, error: null });
4560
+
4561
+ const fetchProjects = useCallback(async () => {
4562
+ setState((s) => ({ ...s, loading: true, error: null }));
4563
+ try {
4564
+ const res = await api.projects.list(params);
4565
+ setState({ data: res.data, meta: res.meta, loading: false, error: null });
4566
+ } catch (err) {
4567
+ setState((s) => ({
4568
+ ...s,
4569
+ loading: false,
4570
+ error: err instanceof ApiError ? err.message : "Failed to load projects",
4571
+ }));
4572
+ }
4573
+ }, [params?.page, params?.search]);
4574
+
4575
+ useEffect(() => { fetchProjects(); }, [fetchProjects]);
4576
+
4577
+ return { ...state, refetch: fetchProjects };
4578
+ }
4579
+ \`\`\`
4580
+
4581
+ ## Reference: Optimistic delete with rollback
4582
+
4583
+ \`\`\`tsx
4584
+ function useDeleteProject(onSuccess?: () => void) {
4585
+ const [deleting, setDeleting] = useState<string | null>(null);
4586
+
4587
+ const deleteProject = async (id: string, projects: Project[], setProjects: (p: Project[]) => void) => {
4588
+ // Optimistic: remove from UI immediately
4589
+ const previous = projects;
4590
+ setProjects(projects.filter((p) => p.id !== id));
4591
+ setDeleting(id);
4592
+
4593
+ try {
4594
+ await api.projects.delete(id);
4595
+ onSuccess?.();
4596
+ } catch {
4597
+ // Rollback on failure
4598
+ setProjects(previous);
4599
+ toast.error("Failed to delete project");
4600
+ } finally {
4601
+ setDeleting(null);
4602
+ }
4603
+ };
4604
+
4605
+ return { deleteProject, deleting };
4606
+ }
4607
+ \`\`\`
4608
+
4609
+ ## Quality checklist (reviewer must verify)
4610
+ - [ ] Types shared between frontend and backend \u2014 not duplicated
4611
+ - [ ] API client is typed \u2014 no raw fetch() in components
4612
+ - [ ] Validation schemas used on both sides
4613
+ - [ ] Server secrets never in NEXT_PUBLIC_ or client bundle
4614
+ - [ ] Loading, error, empty states in every data-fetching component
4615
+ - [ ] API errors return structured { success, data, error } \u2014 not raw strings
3562
4616
  `;
3563
4617
  SKILL_GENERAL = `# Code Quality Standard
3564
4618
 
@@ -3849,7 +4903,7 @@ function getSkillsForSpecialist(skills, specialist) {
3849
4903
  function formatSkillsForPrompt(projectRoot) {
3850
4904
  const skills = loadSkills(projectRoot);
3851
4905
  if (skills.length === 0) return "";
3852
- const maxChars = 8e3;
4906
+ const maxChars = 24e3;
3853
4907
  let totalChars = 0;
3854
4908
  const parts = [];
3855
4909
  for (const skill of skills) {
@@ -13330,6 +14384,42 @@ function App({ initialPrompt, modelPreference, agentMode: initialAgentMode, useO
13330
14384
  }));
13331
14385
  if (useOrchestrator) {
13332
14386
  setIsRouting(false);
14387
+ if (!config2.isAuthenticated()) {
14388
+ setMessages((prev) => [
14389
+ ...prev.filter((m) => m.id !== assistantMsgIdRef.current),
14390
+ {
14391
+ id: nextId(),
14392
+ role: "assistant",
14393
+ content: "Sign in to continue \u2014 opening browser..."
14394
+ }
14395
+ ]);
14396
+ busyRef.current = false;
14397
+ setIsBusy(false);
14398
+ try {
14399
+ const { login: login2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
14400
+ await login2();
14401
+ if (config2.isAuthenticated()) {
14402
+ setMessages((prev) => [
14403
+ ...prev,
14404
+ { id: nextId(), role: "assistant", content: `Signed in as ${config2.get("email")}. Running your task...` }
14405
+ ]);
14406
+ busyRef.current = true;
14407
+ setIsBusy(true);
14408
+ } else {
14409
+ setMessages((prev) => [
14410
+ ...prev,
14411
+ { id: nextId(), role: "assistant", content: "Sign in failed. Try `/login` or run `mint login` in another terminal." }
14412
+ ]);
14413
+ return;
14414
+ }
14415
+ } catch {
14416
+ setMessages((prev) => [
14417
+ ...prev,
14418
+ { id: nextId(), role: "assistant", content: "Could not open browser. Run `mint login` in another terminal." }
14419
+ ]);
14420
+ return;
14421
+ }
14422
+ }
13333
14423
  try {
13334
14424
  const { runOrchestrator: runOrchestrator2 } = await Promise.resolve().then(() => (init_loop2(), loop_exports));
13335
14425
  let responseText = "";
@@ -13772,6 +14862,7 @@ var init_App = __esm({
13772
14862
  init_useAgentEvents();
13773
14863
  init_tiers();
13774
14864
  init_types();
14865
+ init_config();
13775
14866
  init_tracker();
13776
14867
  init_pipeline();
13777
14868
  initChalkLevel();
@@ -13912,166 +15003,12 @@ var init_dashboard = __esm({
13912
15003
  });
13913
15004
 
13914
15005
  // src/cli/index.ts
15006
+ init_auth();
13915
15007
  import { Command } from "commander";
13916
15008
  import chalk10 from "chalk";
13917
15009
  import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7 } from "fs";
13918
15010
  import { dirname as dirname7, resolve as resolve11, sep as sep9 } from "path";
13919
15011
 
13920
- // src/cli/commands/auth.ts
13921
- init_config();
13922
- import chalk from "chalk";
13923
- import boxen from "boxen";
13924
- import { createServer } from "http";
13925
- var SUPABASE_URL = process.env.MINT_SUPABASE_URL ?? "https://srhoryezzsjmjdgfoxgd.supabase.co";
13926
- var SUPABASE_ANON_KEY = process.env.MINT_SUPABASE_ANON_KEY ?? "";
13927
- var AUTH_PAGE_URL = "https://usemint.dev/auth";
13928
- var CALLBACK_PORT = 9876;
13929
- async function login() {
13930
- if (config2.isAuthenticated()) {
13931
- const email = config2.get("email");
13932
- console.log(chalk.yellow(`
13933
- Already logged in as ${email}`));
13934
- console.log(chalk.dim(" Run `mint logout` to switch accounts.\n"));
13935
- return;
13936
- }
13937
- console.log(chalk.cyan("\n Opening browser to sign in...\n"));
13938
- const token = await waitForOAuthCallback();
13939
- if (!token) {
13940
- console.log(chalk.red("\n Login failed. Try again with `mint login`.\n"));
13941
- return;
13942
- }
13943
- try {
13944
- const res = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
13945
- headers: {
13946
- "apikey": SUPABASE_ANON_KEY,
13947
- "Authorization": `Bearer ${token}`
13948
- }
13949
- });
13950
- if (!res.ok) {
13951
- console.log(chalk.red("\n Invalid token received. Try again.\n"));
13952
- return;
13953
- }
13954
- const user = await res.json();
13955
- config2.setAll({
13956
- apiKey: token,
13957
- userId: user.id,
13958
- email: user.email
13959
- });
13960
- console.log(boxen(
13961
- `${chalk.bold.green("Signed in!")}
13962
-
13963
- Email: ${chalk.cyan(user.email)}
13964
- Plan: ${chalk.dim("Free \u2014 20 tasks/day")}
13965
-
13966
- ${chalk.dim("Run `mint` to start coding.")}`,
13967
- { padding: 1, borderColor: "green", borderStyle: "round" }
13968
- ));
13969
- } catch (err) {
13970
- console.log(chalk.red(`
13971
- Error: ${err.message}
13972
- `));
13973
- }
13974
- }
13975
- function waitForOAuthCallback() {
13976
- return new Promise((resolve12) => {
13977
- const timeout = setTimeout(() => {
13978
- server.close();
13979
- resolve12(null);
13980
- }, 12e4);
13981
- const server = createServer(async (req, res) => {
13982
- const url = new URL(req.url ?? "/", `http://localhost:${CALLBACK_PORT}`);
13983
- if (url.pathname === "/callback") {
13984
- let token = null;
13985
- if (req.method === "POST") {
13986
- const body = await new Promise((r) => {
13987
- let data = "";
13988
- req.on("data", (chunk) => {
13989
- data += chunk.toString();
13990
- });
13991
- req.on("end", () => r(data));
13992
- });
13993
- try {
13994
- const parsed = JSON.parse(body);
13995
- token = parsed.access_token ?? parsed.token ?? null;
13996
- } catch {
13997
- token = null;
13998
- }
13999
- } else {
14000
- token = url.searchParams.get("access_token") ?? url.searchParams.get("token");
14001
- }
14002
- res.setHeader("Access-Control-Allow-Origin", "*");
14003
- res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
14004
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
14005
- res.writeHead(200, { "Content-Type": "text/html" });
14006
- res.end(`
14007
- <html>
14008
- <body style="background:#07090d;color:#c8dae8;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
14009
- <div style="text-align:center">
14010
- <h1 style="color:#00d4ff">Connected!</h1>
14011
- <p>You can close this tab and return to the terminal.</p>
14012
- </div>
14013
- </body>
14014
- </html>
14015
- `);
14016
- clearTimeout(timeout);
14017
- server.close();
14018
- resolve12(token);
14019
- return;
14020
- }
14021
- if (req.method === "OPTIONS") {
14022
- res.setHeader("Access-Control-Allow-Origin", "*");
14023
- res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
14024
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
14025
- res.writeHead(204);
14026
- res.end();
14027
- return;
14028
- }
14029
- res.writeHead(404);
14030
- res.end("Not found");
14031
- });
14032
- server.listen(CALLBACK_PORT, () => {
14033
- const callbackUrl = `http://localhost:${CALLBACK_PORT}/callback`;
14034
- const authUrl = `${AUTH_PAGE_URL}?callback=${encodeURIComponent(callbackUrl)}`;
14035
- import("child_process").then(({ execFile }) => {
14036
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
14037
- execFile(cmd, [authUrl]);
14038
- });
14039
- });
14040
- server.on("error", () => {
14041
- clearTimeout(timeout);
14042
- resolve12(null);
14043
- });
14044
- });
14045
- }
14046
- async function signup() {
14047
- await login();
14048
- }
14049
- async function logout() {
14050
- if (!config2.isAuthenticated()) {
14051
- console.log(chalk.yellow("\n Not logged in.\n"));
14052
- return;
14053
- }
14054
- const email = config2.get("email");
14055
- config2.clear();
14056
- console.log(chalk.green(`
14057
- Logged out from ${email}
14058
- `));
14059
- }
14060
- async function whoami() {
14061
- if (!config2.isAuthenticated()) {
14062
- console.log(chalk.yellow("\n Not logged in."));
14063
- console.log(chalk.dim(" Run `mint login` to sign in.\n"));
14064
- return;
14065
- }
14066
- const email = config2.get("email");
14067
- console.log(boxen(
14068
- `${chalk.bold("Signed in")}
14069
-
14070
- Email: ${chalk.cyan(email)}`,
14071
- { padding: 1, borderColor: "cyan", borderStyle: "round" }
14072
- ));
14073
- }
14074
-
14075
15012
  // src/cli/commands/config.ts
14076
15013
  init_config();
14077
15014
  import chalk2 from "chalk";