Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .cursor/commands/onboard-new-developer.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
Comprehensive onboarding process to get new developer up and running quickly. Follow architecture, strategies, and decisions in `@apps/docu/`. After implementation, update docs, readme, and cursor rules if required to keep them aligned.

1. **Environment setup**: Install required tools, set up development environment, configure IDE/extensions, set up git/SSH keys
2. **Project familiarization**: Review project structure, understand architecture, read key documentation, set up local database, verify all tests passing, can run application locally, submit first PR
2. **Clone and setup**: Run `pnpm setup` at repo root
3. **Decide context**: Local only, or remote VPC? (Remote VPC = dev machine in cloud; trade-offs: networking, access, cost, latency. See @apps/docu/content/docs/development/dev-environments.)
4. **Run web/API**: `pnpm dev` (API + Next.js)
5. **Run mobile (optional)**: `pnpm --filter @repo/mobile start` (or `start:localhost` / `start:tunnel` for remote). See @apps/docu/content/docs/development/dev-environments
6. **Verify**: Simulator/device can load the app and reach the API
7. **Project familiarization**: Review project structure, understand architecture, read key documentation, set up local database, verify all tests passing, submit first PR
2 changes: 1 addition & 1 deletion .cursor/rules/frontend/design.mdc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
glob: "**/*.{tsx,css}"
glob: "apps/web/**/*.{tsx,css}"
alwaysApply: true
---

Expand Down
31 changes: 31 additions & 0 deletions .cursor/rules/frontend/expo.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
description: Expo/React Native standards
glob: "apps/mobile/**/*"
---

## Expo Router
File-based routing: `src/app/`, `_layout.tsx`, route groups `(group)`. `_layout` cannot be DOM component—see @.cursor/skills/expo-use-dom

## NativeTabs (SDK 55+)
Use `NativeTabs.Trigger.Icon`, `NativeTabs.Trigger.Label`, `NativeTabs.Trigger.Badge` (not separate imports). See @.cursor/skills/expo-upgrading/references/native-tabs.md

## React Native
Primitives only: View, Text, Pressable, ScrollView—no HTML elements

## Styling
Tailwind via NativeWind v5 + react-native-css. Use `className` on View, Text, Pressable, etc. StyleSheet for complex cases. See @.cursor/skills/expo-tailwind-setup-v55

## Platform extensions
Use `.web.tsx`, `.native.tsx` for platform-specific code

## Path aliases
`@/*` → `src/`, `@/assets/*` → `assets/`

## Hooks
Kebab-case filenames, collocated in `hooks/`. See @.cursor/rules/frontend/react-hooks.mdc

## React Compiler
Enabled in app.json—no manual `useMemo`/`useCallback`. Aligns with @.cursor/rules/frontend/react-hooks.mdc

## EAS / CI
EAS builds: mobile-build (manual), mobile-preview (main), mobile-pr-preview (EAS Update). EXPO_TOKEN required. See @apps/docu/content/docs/deployment/mobile-cicd.mdx
4 changes: 2 additions & 2 deletions .cursor/rules/frontend/mobile-first.mdc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Mobile-first responsive design patterns and best practices
glob: "**/*.{tsx,css}"
description: Mobile-first responsive design patterns and best practices (Tailwind breakpoints)
glob: "apps/web/**/*.{tsx,css}"
alwaysApply: true
---

Expand Down
2 changes: 1 addition & 1 deletion .cursor/rules/frontend/nextjs.mdc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Next.js standards
glob: "**/*.{tsx,css}"
glob: "apps/web/**/*.{tsx,css}"
alwaysApply: true
---

Expand Down
2 changes: 1 addition & 1 deletion .cursor/rules/frontend/shadcnui.mdc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: ShadcnUI component guidelines and patterns
glob: "**/*.{tsx,css}"
glob: "apps/web/**/*.{tsx,css},packages/ui/**/*.{tsx,css}"
alwaysApply: true
---

Expand Down
92 changes: 92 additions & 0 deletions .cursor/skills/expo-cicd-workflows-v55/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
name: expo-cicd-workflows-v55
description: Helps understand and write EAS workflow YAML files for Expo projects. Use this skill when the user asks about CI/CD or workflows in an Expo or EAS context, mentions .eas/workflows/, or wants help with EAS build pipelines or deployment automation.
allowed-tools: "Read,Write,Bash(node:*)"
version: 1.0.0
license: MIT License
---

# EAS Workflows Skill

Help developers write and edit EAS CI/CD workflow YAML files.

## Reference Documentation

Fetch these resources before generating or validating workflow files. Use the fetch script (implemented using Node.js) in this skill's `scripts/` directory; it caches responses using ETags for efficiency:

```bash
# Fetch resources
node {baseDir}/scripts/fetch.js <url>
```

1. **JSON Schema** — https://api.expo.dev/v2/workflows/schema
- It is NECESSARY to fetch this schema
- Source of truth for validation
- All job types and their required/optional parameters
- Trigger types and configurations
- Runner types, VM images, and all enums

2. **Syntax Documentation** — https://raw.githubusercontent.com/expo/expo/refs/heads/main/docs/pages/eas/workflows/syntax.mdx
- Overview of workflow YAML syntax
- Examples and English explanations
- Expression syntax and contexts

3. **Pre-packaged Jobs** — https://raw.githubusercontent.com/expo/expo/refs/heads/main/docs/pages/eas/workflows/pre-packaged-jobs.mdx
- Documentation for supported pre-packaged job types
- Job-specific parameters and outputs

Do not rely on memorized values; these resources evolve as new features are added.

## Workflow File Location

Workflows live in `.eas/workflows/*.yml` (or `.yaml`).

## Top-Level Structure

A workflow file has these top-level keys:

- `name` — Display name for the workflow
- `on` — Triggers that start the workflow (at least one required)
- `jobs` — Job definitions (required)
- `defaults` — Shared defaults for all jobs
- `concurrency` — Control parallel workflow runs

Consult the schema for the full specification of each section.

## Expressions

Use `${{ }}` syntax for dynamic values. The schema defines available contexts:

- `github.*` — GitHub repository and event information
- `inputs.*` — Values from `workflow_dispatch` inputs
- `needs.*` — Outputs and status from dependent jobs
- `jobs.*` — Job outputs (alternative syntax)
- `steps.*` — Step outputs within custom jobs
- `workflow.*` — Workflow metadata

## Generating Workflows

When generating or editing workflows:

1. Fetch the schema to get current job types, parameters, and allowed values
2. Validate that required fields are present for each job type
3. Verify job references in `needs` and `after` exist in the workflow
4. Check that expressions reference valid contexts and outputs
5. Ensure `if` conditions respect the schema's length constraints

## Validation

After generating or editing a workflow file, validate it against the schema:

```sh
# Install dependencies if missing
[ -d "{baseDir}/scripts/node_modules" ] || npm install --prefix {baseDir}/scripts

node {baseDir}/scripts/validate.js <workflow.yml> [workflow2.yml ...]
```

The validator fetches the latest schema and checks the YAML structure. Fix any reported errors before considering the workflow complete.

## Answering Questions

When users ask about available options (job types, triggers, runner types, etc.), fetch the schema and derive the answer from it rather than relying on potentially outdated information.
110 changes: 110 additions & 0 deletions .cursor/skills/expo-cicd-workflows-v55/scripts/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env node

import { createHash } from 'node:crypto'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import process from 'node:process'

const CACHE_DIRECTORY = resolve(import.meta.dirname, '.cache')
const DEFAULT_TTL_SECONDS = 15 * 60 // 15 minutes

export async function fetchCached(url) {
await mkdir(CACHE_DIRECTORY, { recursive: true })

const cacheFile = resolve(CACHE_DIRECTORY, `${hashUrl(url)}.json`)
const cached = await loadCacheEntry(cacheFile)
if (cached && cached.expires > Math.floor(Date.now() / 1000)) {
return cached.data
}

// Make request, with conditional If-None-Match if we have an ETag.
// Cache-Control: max-age=0 overrides Node's default 'no-cache' to allow 304 responses.
const response = await fetch(url, {
signal: AbortSignal.timeout(30_000),
headers: {
'Cache-Control': 'max-age=0',
...(cached?.etag && { 'If-None-Match': cached.etag }),
},
})

if (response.status === 304 && cached) {
// Refresh expiration and return cached data
const entry = { ...cached, expires: getExpires(response.headers) }
await saveCacheEntry(cacheFile, entry)
return cached.data
}

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}

const etag = response.headers.get('etag')
const data = await response.text()
const expires = getExpires(response.headers)

await saveCacheEntry(cacheFile, { url, etag, expires, data })

return data
}

function hashUrl(url) {
return createHash('sha256').update(url).digest('hex').slice(0, 16)
}

async function loadCacheEntry(cacheFile) {
try {
return JSON.parse(await readFile(cacheFile, 'utf-8'))
} catch {
return null
}
}

async function saveCacheEntry(cacheFile, entry) {
await writeFile(cacheFile, JSON.stringify(entry, null, 2))
}

function getExpires(headers) {
const now = Math.floor(Date.now() / 1000)

// Prefer Cache-Control: max-age
const maxAgeSeconds = parseMaxAge(headers.get('cache-control'))
if (maxAgeSeconds != null) {
return now + maxAgeSeconds
}

// Fall back to Expires header
const expires = headers.get('expires')
if (expires) {
const expiresTime = Date.parse(expires)
if (!Number.isNaN(expiresTime)) {
return Math.floor(expiresTime / 1000)
}
}

// Default TTL
return now + DEFAULT_TTL_SECONDS
}

function parseMaxAge(cacheControl) {
if (!cacheControl) {
return null
}
const match = cacheControl.match(/max-age=(\d+)/i)
return match ? parseInt(match[1], 10) : null
}

if (import.meta.main) {
const url = process.argv[2]

if (!url || url === '--help' || url === '-h') {
console.log(`Usage: fetch <url>

Fetches a URL with HTTP caching (ETags + Cache-Control/Expires).
Default TTL: ${DEFAULT_TTL_SECONDS / 60} minutes.
Cache is stored in: ${CACHE_DIRECTORY}/`)
process.exit(url ? 0 : 1)
}

const data = await fetchCached(url)
console.log(data)
}
11 changes: 11 additions & 0 deletions .cursor/skills/expo-cicd-workflows-v55/scripts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@expo/cicd-workflows-skill",
"version": "0.0.0",
"private": true,
"type": "module",
"dependencies": {
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"js-yaml": "^4.1.0"
}
}
94 changes: 94 additions & 0 deletions .cursor/skills/expo-cicd-workflows-v55/scripts/validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env node

import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import process from 'node:process'

import Ajv2020 from 'ajv/dist/2020.js'
import addFormats from 'ajv-formats'
import yaml from 'js-yaml'

import { fetchCached } from './fetch.js'

const SCHEMA_URL = 'https://api.expo.dev/v2/workflows/schema'

async function fetchSchema() {
const data = await fetchCached(SCHEMA_URL)
const body = JSON.parse(data)
return body.data
}

function createValidator(schema) {
const ajv = new Ajv2020({ allErrors: true, strict: true })
addFormats(ajv)
return ajv.compile(schema)
}

async function validateFile(validator, filePath) {
let content
try {
content = await readFile(filePath, 'utf-8')
} catch (e) {
return { valid: false, error: `Unable to read file: ${e.message}` }
}

let doc
try {
doc = yaml.load(content)
} catch (e) {
return { valid: false, error: `YAML parse error: ${e.message}` }
}

const valid = validator(doc)
if (!valid) {
return { valid: false, error: formatErrors(validator.errors) }
}

return { valid: true }
}

function formatErrors(errors) {
return errors
.map(error => {
const path = error.instancePath || '(root)'
const allowed = error.params?.allowedValues?.join(', ')
return ` ${path}: ${error.message}${allowed ? ` (allowed: ${allowed})` : ''}`
})
.join('\n')
}

if (import.meta.main) {
const args = process.argv.slice(2)
const files = args.filter(a => !a.startsWith('-'))

if (files.length === 0 || args.includes('--help') || args.includes('-h')) {
console.log(`Usage: validate <workflow.yml> [workflow2.yml ...]

Validates EAS workflow YAML files against the official schema.`)
process.exit(files.length === 0 ? 1 : 0)
}

let validator
try {
validator = createValidator(await fetchSchema())
} catch (e) {
console.error(`✗ Failed to load workflow schema\n${e.message}`)
process.exit(1)
}

let hasErrors = false

for (const file of files) {
const filePath = resolve(process.cwd(), file)
const result = await validateFile(validator, filePath)

if (result.valid) {
console.log(`✓ ${file}`)
} else {
console.error(`✗ ${file}\n${result.error}`)
hasErrors = true
}
}

process.exit(hasErrors ? 1 : 0)
}
Loading
Loading