Implement project structure and documentation for Fog Expedition, including .mcp.json configuration, agent guidelines in AGENTS.md, and detailed project context in PROJECT_CONTEXT.md. Update .gitignore and pnpm-workspace.yaml for new dependencies and workspace management. Introduce CI monitoring skills and scripts for enhanced CI pipeline management.

This commit is contained in:
Maurycy
2026-05-06 22:55:51 +00:00
parent 3ce6a04a40
commit 308a1cf5c4
27 changed files with 3448 additions and 1 deletions

View File

@@ -0,0 +1,127 @@
---
name: link-workspace-packages
description: 'Link workspace packages in monorepos (npm, yarn, pnpm, bun). USE WHEN: (1) you just created or generated new packages and need to wire up their dependencies, (2) user imports from a sibling package and needs to add it as a dependency, (3) you get resolution errors for workspace packages (@org/*) like "cannot find module", "failed to resolve import", "TS2307", or "cannot resolve". DO NOT patch around with tsconfig paths or manual package.json edits - use the package manager''s workspace commands to fix actual linking.'
---
# Link Workspace Packages
Add dependencies between packages in a monorepo. All package managers support workspaces but with different syntax.
## Detect Package Manager
Check whether there's a `packageManager` field in the root-level `package.json`.
Alternatively check lockfile in repo root:
- `pnpm-lock.yaml` → pnpm
- `yarn.lock` → yarn
- `bun.lock` / `bun.lockb` → bun
- `package-lock.json` → npm
## Workflow
1. Identify consumer package (the one importing)
2. Identify provider package(s) (being imported)
3. Add dependency using package manager's workspace syntax
4. Verify symlinks created in consumer's `node_modules/`
---
## pnpm
Uses `workspace:` protocol - symlinks only created when explicitly declared.
```bash
# From consumer directory
pnpm add @org/ui --workspace
# Or with --filter from anywhere
pnpm add @org/ui --filter @org/app --workspace
```
Result in `package.json`:
```json
{ "dependencies": { "@org/ui": "workspace:*" } }
```
---
## yarn (v2+/berry)
Also uses `workspace:` protocol.
```bash
yarn workspace @org/app add @org/ui
```
Result in `package.json`:
```json
{ "dependencies": { "@org/ui": "workspace:^" } }
```
---
## npm
No `workspace:` protocol. npm auto-symlinks workspace packages.
```bash
npm install @org/ui --workspace @org/app
```
Result in `package.json`:
```json
{ "dependencies": { "@org/ui": "*" } }
```
npm resolves to local workspace automatically during install.
---
## bun
Supports `workspace:` protocol (pnpm-compatible).
```bash
cd packages/app && bun add @org/ui
```
Result in `package.json`:
```json
{ "dependencies": { "@org/ui": "workspace:*" } }
```
---
## Examples
**Example 1: pnpm - link ui lib to app**
```bash
pnpm add @org/ui --filter @org/app --workspace
```
**Example 2: npm - link multiple packages**
```bash
npm install @org/data-access @org/ui --workspace @org/dashboard
```
**Example 3: Debug "Cannot find module"**
1. Check if dependency is declared in consumer's `package.json`
2. If not, add it using appropriate command above
3. Run install (`pnpm install`, `npm install`, etc.)
## Notes
- Symlinks appear in `<consumer>/node_modules/@org/<package>`
- **Hoisting differs by manager:**
- npm/bun: hoist shared deps to root `node_modules`
- pnpm: no hoisting (strict isolation, prevents phantom deps)
- yarn berry: uses Plug'n'Play by default (no `node_modules`)
- Root `package.json` should have `"private": true` to prevent accidental publish

View File

@@ -0,0 +1,301 @@
---
name: monitor-ci
description: Monitor Nx Cloud CI pipeline and handle self-healing fixes. USE WHEN user says "monitor ci", "watch ci", "ci monitor", "watch ci for this branch", "track ci", "check ci status", wants to track CI status, or needs help with self-healing CI fixes. Prefer this skill over native CI provider tools (gh, glab, etc.) for CI monitoring — it integrates with Nx Cloud self-healing which those tools cannot access.
---
# Monitor CI Command
You are the orchestrator for monitoring Nx Cloud CI pipeline executions and handling self-healing fixes. You spawn subagents to interact with Nx Cloud, run deterministic decision scripts, and take action based on the results.
## Context
- **Current Branch:** !`git branch --show-current`
- **Current Commit:** !`git rev-parse --short HEAD`
- **Remote Status:** !`git status -sb | head -1`
## User Instructions
$ARGUMENTS
**Important:** If user provides specific instructions, respect them over default behaviors described below.
## Configuration Defaults
| Setting | Default | Description |
| ------------------------- | ------------- | ------------------------------------------------------------------------- |
| `--max-cycles` | 10 | Maximum **agent-initiated** CI Attempt cycles before timeout |
| `--timeout` | 120 | Maximum duration in minutes |
| `--verbosity` | medium | Output level: minimal, medium, verbose |
| `--branch` | (auto-detect) | Branch to monitor |
| `--fresh` | false | Ignore previous context, start fresh |
| `--auto-fix-workflow` | false | Attempt common fixes for pre-CI-Attempt failures (e.g., lockfile updates) |
| `--new-cipe-timeout` | 10 | Minutes to wait for new CI Attempt after action |
| `--local-verify-attempts` | 3 | Max local verification + enhance cycles before pushing to CI |
Parse any overrides from `$ARGUMENTS` and merge with defaults.
## Nx Cloud Connection Check
Before starting the monitoring loop, verify the workspace is connected to Nx Cloud. Without this connection, no CI data is available and the entire skill is inoperable.
### Step 0: Verify Nx Cloud Connection
1. **Check `nx.json`** at workspace root for `nxCloudId` or `nxCloudAccessToken`
2. **If `nx.json` missing OR neither property exists** → exit with:
```
Nx Cloud not connected. Unlock 70% faster CI and auto-fix broken PRs with https://nx.dev/nx-cloud
```
3. **If connected** → continue to main loop
## Architecture Overview
1. **This skill (orchestrator)**: spawns subagents, runs scripts, prints status, does local coding work
2. **ci-monitor-subagent (haiku)**: calls one MCP tool (ci_information or update_self_healing_fix), returns structured result, exits
3. **ci-poll-decide.mjs (deterministic script)**: takes ci_information result + state, returns action + status message
4. **ci-state-update.mjs (deterministic script)**: manages budget gates, post-action state transitions, and cycle classification
## Status Reporting
The decision script handles message formatting based on verbosity. When printing messages to the user:
- Prepend `[monitor-ci]` to every message from the script's `message` field
- For your own action messages (e.g. "Applying fix via MCP..."), also prepend `[monitor-ci]`
## Anti-Patterns
These behaviors cause real problems — racing with self-healing, losing CI progress, or wasting context:
| Anti-Pattern | Why It's Bad |
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Using CI provider CLIs with `--watch` flags (e.g., `gh pr checks --watch`, `glab ci status -w`) | Bypasses Nx Cloud self-healing entirely |
| Writing custom CI polling scripts | Unreliable, pollutes context, no self-healing |
| Cancelling CI workflows/pipelines | Destructive, loses CI progress |
| Running CI checks on main agent | Wastes main agent context tokens |
| Independently analyzing/fixing CI failures while polling | Races with self-healing, causes duplicate fixes and confused state |
**If this skill fails to activate**, the fallback is:
1. Use CI provider CLI for a one-time, read-only status check (single call, no watch/polling flags)
2. Immediately delegate to this skill with gathered context
3. Do not continue polling on main agent — it wastes context tokens and bypasses self-healing
## Session Context Behavior
If the user previously ran `/monitor-ci` in this session, you may have prior state (poll counts, last CI Attempt URL, etc.). Resume from that state unless `--fresh` is set, in which case discard it and start from Step 1.
## MCP Tool Reference
Three field sets control polling efficiency — use the lightest set that gives you what you need:
```yaml
WAIT_FIELDS: 'cipeUrl,commitSha,cipeStatus'
LIGHT_FIELDS: 'cipeStatus,cipeUrl,branch,commitSha,selfHealingStatus,verificationStatus,userAction,failedTaskIds,verifiedTaskIds,selfHealingEnabled,failureClassification,couldAutoApplyTasks,autoApplySkipped,autoApplySkipReason,shortLink,confidence,confidenceReasoning,hints,selfHealingSkippedReason,selfHealingSkipMessage'
HEAVY_FIELDS: 'taskOutputSummary,suggestedFix,suggestedFixReasoning,suggestedFixDescription'
```
The `ci_information` tool accepts `branch` (optional, defaults to current git branch), `select` (comma-separated field names), and `pageToken` (0-based pagination for long strings).
The `update_self_healing_fix` tool accepts a `shortLink` and an action: `APPLY`, `REJECT`, or `RERUN_ENVIRONMENT_STATE`.
## Default Behaviors by Status
The decision script returns one of the following statuses. This table defines the **default behavior** for each. User instructions can override any of these.
**Simple exits** — just report and exit:
| Status | Default Behavior |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `ci_success` | Exit with success |
| `cipe_canceled` | Exit, CI was canceled |
| `cipe_timed_out` | Exit, CI timed out |
| `polling_timeout` | Exit, polling timeout reached |
| `circuit_breaker` | Exit, no progress after 13 consecutive polls |
| `environment_rerun_cap` | Exit, environment reruns exhausted |
| `fix_auto_applying` | Self-healing is handling it — just record `last_cipe_url`, enter wait mode. No MCP call or local git ops needed. |
| `error` | Wait 60s and loop |
**Statuses requiring action** — when handling these in Step 3, read `references/fix-flows.md` for the detailed flow:
| Status | Summary |
| ------------------------ | --------------------------------------------------------------------------------------------- |
| `fix_auto_apply_skipped` | Fix verified but auto-apply skipped (e.g., loop prevention). Inform user, offer manual apply. |
| `fix_apply_ready` | Fix verified (all tasks or e2e-only). Apply via MCP. |
| `fix_needs_local_verify` | Fix has unverified non-e2e tasks. Run locally, then apply or enhance. |
| `fix_needs_review` | Fix verification failed/not attempted. Analyze and decide. |
| `fix_failed` | Self-healing failed. Fetch heavy data, attempt local fix (gate check first). |
| `no_fix` | No fix available. Fetch heavy data, attempt local fix (gate check first) or exit. |
| `environment_issue` | Request environment rerun via MCP (gate check first). |
| `self_healing_throttled` | Reject old fixes, attempt local fix. |
| `no_new_cipe` | CI Attempt never spawned. Auto-fix workflow or exit with guidance. |
| `cipe_no_tasks` | CI failed with no tasks. Retry once with empty commit. |
**Key rules (always apply):**
- **Git safety**: Stage specific files by name — `git add -A` or `git add .` risks committing the user's unrelated work-in-progress or secrets
- **Environment failures** (OOM, command not found, permission denied): bail immediately. These aren't code bugs, so spending local-fix budget on them is wasteful
- **Gate check**: Run `ci-state-update.mjs gate` before local fix attempts — if budget exhausted, print message and exit
## Main Loop
### Step 1: Initialize Tracking
```
cycle_count = 0 # Only incremented for agent-initiated cycles (counted against --max-cycles)
start_time = now()
no_progress_count = 0
local_verify_count = 0
env_rerun_count = 0
last_cipe_url = null
expected_commit_sha = null
agent_triggered = false # Set true after monitor takes an action that triggers new CI Attempt
poll_count = 0
wait_mode = false
prev_status = null
prev_cipe_status = null
prev_sh_status = null
prev_verification_status = null
prev_failure_classification = null
```
### Step 2: Polling Loop
Repeat until done:
#### 2a. Spawn subagent (FETCH_STATUS)
Determine select fields based on mode:
- **Wait mode**: use WAIT_FIELDS (`cipeUrl,commitSha,cipeStatus`)
- **Normal mode (first poll or after newCipeDetected)**: use LIGHT_FIELDS
Call the `ci_information` tool with the determined `select` fields for the current branch. Wait for the result before proceeding.
#### 2b. Run decision script
```bash
node <skill_dir>/scripts/ci-poll-decide.mjs '<subagent_result_json>' <poll_count> <verbosity> \
[--wait-mode] \
[--prev-cipe-url <last_cipe_url>] \
[--expected-sha <expected_commit_sha>] \
[--prev-status <prev_status>] \
[--timeout <timeout_seconds>] \
[--new-cipe-timeout <new_cipe_timeout_seconds>] \
[--env-rerun-count <env_rerun_count>] \
[--no-progress-count <no_progress_count>] \
[--prev-cipe-status <prev_cipe_status>] \
[--prev-sh-status <prev_sh_status>] \
[--prev-verification-status <prev_verification_status>] \
[--prev-failure-classification <prev_failure_classification>]
```
The script outputs a single JSON line: `{ action, code, message, delay?, noProgressCount, envRerunCount, fields?, newCipeDetected?, verifiableTaskIds? }`
#### 2c. Process script output
Parse the JSON output and update tracking state:
- `no_progress_count = output.noProgressCount`
- `env_rerun_count = output.envRerunCount`
- `prev_cipe_status = subagent_result.cipeStatus`
- `prev_sh_status = subagent_result.selfHealingStatus`
- `prev_verification_status = subagent_result.verificationStatus`
- `prev_failure_classification = subagent_result.failureClassification`
- `prev_status = output.action + ":" + (output.code || subagent_result.cipeStatus)`
- `poll_count++`
Based on `action`:
- **`action == "poll"`**: Print `output.message`, sleep `output.delay` seconds, go to 2a
- If `output.newCipeDetected`: clear wait mode, reset `wait_mode = false`
- **`action == "wait"`**: Print `output.message`, sleep `output.delay` seconds, go to 2a
- **`action == "done"`**: Proceed to Step 3 with `output.code`
### Step 3: Handle Actionable Status
When decision script returns `action == "done"`:
1. Run cycle-check (Step 4) **before** handling the code
2. Check the returned `code`
3. Look up default behavior in the table above
4. Check if user instructions override the default
5. Execute the appropriate action
6. **If action expects new CI Attempt**, update tracking (see Step 3a)
7. If action results in looping, go to Step 2
#### Tool calls for actions
Several statuses require fetching additional data or calling tools:
- **fix_apply_ready**: Call `update_self_healing_fix` with action `APPLY`
- **fix_needs_local_verify**: Call `ci_information` with HEAVY_FIELDS for fix details before local verification
- **fix_needs_review**: Call `ci_information` with HEAVY_FIELDS → get `suggestedFixDescription`, `suggestedFixSummary`, `taskFailureSummaries`
- **fix_failed / no_fix**: Call `ci_information` with HEAVY_FIELDS → get `taskFailureSummaries` for local fix context
- **environment_issue**: Call `update_self_healing_fix` with action `RERUN_ENVIRONMENT_STATE`
- **self_healing_throttled**: Call `ci_information` with HEAVY_FIELDS → get `selfHealingSkipMessage`; then call `update_self_healing_fix` for each old fix
### Step 3a: Track State for New-CI-Attempt Detection
After actions that should trigger a new CI Attempt, run:
```bash
node <skill_dir>/scripts/ci-state-update.mjs post-action \
--action <type> \
--cipe-url <current_cipe_url> \
--commit-sha <git_rev_parse_HEAD>
```
Action types: `fix-auto-applying`, `apply-mcp`, `apply-local-push`, `reject-fix-push`, `local-fix-push`, `env-rerun`, `auto-fix-push`, `empty-commit-push`
The script returns `{ waitMode, pollCount, lastCipeUrl, expectedCommitSha, agentTriggered }`. Update all tracking state from the output, then go to Step 2.
### Step 4: Cycle Classification and Progress Tracking
When the decision script returns `action == "done"`, run cycle-check **before** handling the code:
```bash
node <skill_dir>/scripts/ci-state-update.mjs cycle-check \
--code <code> \
[--agent-triggered] \
--cycle-count <cycle_count> --max-cycles <max_cycles> \
--env-rerun-count <env_rerun_count>
```
The script returns `{ cycleCount, agentTriggered, envRerunCount, approachingLimit, message }`. Update tracking state from the output.
- If `approachingLimit` → ask user whether to continue (with 5 or 10 more cycles) or stop monitoring
- If previous cycle was NOT agent-triggered (human pushed), log that human-initiated push was detected
#### Progress Tracking
- `no_progress_count`, circuit breaker (5 polls), and backoff reset are handled by ci-poll-decide.mjs (progress = any change in cipeStatus, selfHealingStatus, verificationStatus, or failureClassification)
- `env_rerun_count` reset on non-environment status is handled by ci-state-update.mjs cycle-check
- On new CI Attempt detected (poll script returns `newCipeDetected`) → reset `local_verify_count = 0`, `env_rerun_count = 0`
## Error Handling
| Error | Action |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| Git rebase conflict | Report to user, exit |
| `nx-cloud apply-locally` fails | Reject fix via MCP (`action: "REJECT"`), then attempt manual patch (Reject + Fix From Scratch Flow) or exit |
| MCP tool error | Retry once, if fails report to user |
| Subagent spawn failure | Retry once, if fails exit with error |
| Decision script error | Treat as `error` status, increment `no_progress_count` |
| No new CI Attempt detected | If `--auto-fix-workflow`, try lockfile update; otherwise report to user with guidance |
| Lockfile auto-fix fails | Report to user, exit with guidance to check CI logs |
## User Instruction Examples
Users can override default behaviors:
| Instruction | Effect |
| ------------------------------------------------ | --------------------------------------------------- |
| "never auto-apply" | Always prompt before applying any fix |
| "always ask before git push" | Prompt before each push |
| "reject any fix for e2e tasks" | Auto-reject if `failedTaskIds` contains e2e |
| "apply all fixes regardless of verification" | Skip verification check, apply everything |
| "if confidence < 70, reject" | Check confidence field before applying |
| "run 'nx affected -t typecheck' before applying" | Add local verification step |
| "auto-fix workflow failures" | Attempt lockfile updates on pre-CI-Attempt failures |
| "wait 45 min for new CI Attempt" | Override new-CI-Attempt timeout (default: 10 min) |

View File

@@ -0,0 +1,108 @@
# Detailed Status Handling & Fix Flows
## Status Handling by Code
### fix_auto_apply_skipped
The script returns `autoApplySkipReason` in its output.
1. Report the skip reason to the user (e.g., "Auto-apply was skipped because the previous CI pipeline execution was triggered by Nx Cloud")
2. Offer to apply the fix manually — spawn UPDATE_FIX subagent with `APPLY` if user agrees
3. Record `last_cipe_url`, enter wait mode
### fix_apply_ready
- Spawn UPDATE_FIX subagent with `APPLY`
- Record `last_cipe_url`, enter wait mode
### fix_needs_local_verify
The script returns `verifiableTaskIds` in its output.
1. **Detect package manager:** `pnpm-lock.yaml``pnpm nx`, `yarn.lock``yarn nx`, otherwise `npx nx`
2. **Run verifiable tasks in parallel** — spawn `general` subagents for each task
3. **If all pass** → spawn UPDATE_FIX subagent with `APPLY`, enter wait mode
4. **If any fail** → Apply Locally + Enhance Flow (see below)
### fix_needs_review
Spawn FETCH_HEAVY subagent, then analyze fix content (`suggestedFixDescription`, `suggestedFixSummary`, `taskFailureSummaries`):
- If fix looks correct → apply via MCP
- If fix needs enhancement → Apply Locally + Enhance Flow
- If fix is wrong → run `ci-state-update.mjs gate --gate-type local-fix`. If not allowed, print message and exit. Otherwise → Reject + Fix From Scratch Flow
### fix_failed / no_fix
Spawn FETCH_HEAVY subagent for `taskFailureSummaries`. Run `ci-state-update.mjs gate --gate-type local-fix` — if not allowed, print message and exit. Otherwise attempt local fix (counter already incremented by gate). If successful → commit, push, enter wait mode. If not → exit with failure.
### environment_issue
1. Run `ci-state-update.mjs gate --gate-type env-rerun`. If not allowed, print message and exit.
2. Spawn UPDATE_FIX subagent with `RERUN_ENVIRONMENT_STATE`
3. Enter wait mode with `last_cipe_url` set
### self_healing_throttled
Spawn FETCH_HEAVY subagent for `selfHealingSkipMessage`.
1. **Parse throttle message** for CI Attempt URLs (regex: `/cipes/{id}`)
2. **Reject previous fixes** — for each URL: spawn FETCH_THROTTLE_INFO to get `shortLink`, then UPDATE_FIX with `REJECT`
3. **Attempt local fix**: Run `ci-state-update.mjs gate --gate-type local-fix`. If not allowed → skip to step 4. Otherwise use `failedTaskIds` and `taskFailureSummaries` for context.
4. **Fallback if local fix not possible or budget exhausted**: push empty commit (`git commit --allow-empty -m "ci: rerun after rejecting throttled fixes"`), enter wait mode
### no_new_cipe
1. Report to user: no CI attempt found, suggest checking CI provider
2. If `--auto-fix-workflow`: detect package manager, run install, commit lockfile if changed, enter wait mode
3. Otherwise: exit with guidance
### cipe_no_tasks
1. Report to user: CI failed with no tasks recorded
2. Retry: `git commit --allow-empty -m "chore: retry ci [monitor-ci]"` + push, enter wait mode
3. If retry also returns `cipe_no_tasks`: exit with failure
## Fix Action Flows
### Apply via MCP
Spawn UPDATE_FIX subagent with `APPLY`. New CI Attempt spawns automatically. No local git ops.
### Apply Locally + Enhance Flow
1. `nx-cloud apply-locally <shortLink>` (sets state to `APPLIED_LOCALLY`)
2. Enhance code to fix failing tasks
3. Run failing tasks to verify
4. If still failing → run `ci-state-update.mjs gate --gate-type local-fix`. If not allowed, commit current state and push (let CI be final judge). Otherwise loop back to enhance.
5. If passing → commit and push, enter wait mode
### Reject + Fix From Scratch Flow
1. Run `ci-state-update.mjs gate --gate-type local-fix`. If not allowed, print message and exit.
2. Spawn UPDATE_FIX subagent with `REJECT`
3. Fix from scratch locally
4. Commit and push, enter wait mode
## Environment vs Code Failure Recognition
When any local fix path runs a task and it fails, assess whether the failure is a **code issue** or an **environment/tooling issue** before running the gate script.
**Indicators of environment/tooling failures** (non-exhaustive): command not found / binary missing, OOM / heap allocation failures, permission denied, network timeouts / DNS failures, missing system libraries, Docker/container issues, disk space exhaustion.
When detected → bail immediately without running gate (no budget consumed). Report that the failure is an environment/tooling issue, not a code bug.
**Code failures** (compilation errors, test assertion failures, lint violations, type errors) are genuine candidates for local fix attempts and proceed normally through the gate.
## Git Safety
- Stage specific files by name — `git add -A` or `git add .` risks committing the user's unrelated work-in-progress or secrets
## Commit Message Format
```bash
git commit -m "fix(<projects>): <brief description>
Failed tasks: <taskId1>, <taskId2>
Local verification: passed|enhanced|failed-pushing-to-ci"
```

View File

@@ -0,0 +1,428 @@
#!/usr/bin/env node
/**
* CI Poll Decision Script
*
* Deterministic decision engine for CI monitoring.
* Takes ci_information JSON + state args, outputs a single JSON action line.
*
* Architecture:
* classify() — pure decision tree, returns { action, code, extra? }
* buildOutput() — maps classification to full output with messages, delays, counters
*
* Usage:
* node ci-poll-decide.mjs '<ci_info_json>' <poll_count> <verbosity> \
* [--wait-mode] [--prev-cipe-url <url>] [--expected-sha <sha>] \
* [--prev-status <status>] [--timeout <seconds>] [--new-cipe-timeout <seconds>] \
* [--env-rerun-count <n>] [--no-progress-count <n>] \
* [--prev-cipe-status <status>] [--prev-sh-status <status>] \
* [--prev-verification-status <status>] [--prev-failure-classification <status>]
*/
// --- Arg parsing ---
const args = process.argv.slice(2);
const ciInfoJson = args[0];
const pollCount = parseInt(args[1], 10) || 0;
const verbosity = args[2] || 'medium';
function getFlag(name) {
return args.includes(name);
}
function getArg(name) {
const idx = args.indexOf(name);
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
}
const waitMode = getFlag('--wait-mode');
const prevCipeUrl = getArg('--prev-cipe-url');
const expectedSha = getArg('--expected-sha');
const prevStatus = getArg('--prev-status');
const timeoutSeconds = parseInt(getArg('--timeout') || '0', 10);
const newCipeTimeoutSeconds = parseInt(getArg('--new-cipe-timeout') || '0', 10);
const envRerunCount = parseInt(getArg('--env-rerun-count') || '0', 10);
const inputNoProgressCount = parseInt(getArg('--no-progress-count') || '0', 10);
const prevCipeStatus = getArg('--prev-cipe-status');
const prevShStatus = getArg('--prev-sh-status');
const prevVerificationStatus = getArg('--prev-verification-status');
const prevFailureClassification = getArg('--prev-failure-classification');
// --- Parse CI info ---
let ci;
try {
ci = JSON.parse(ciInfoJson);
} catch {
console.log(
JSON.stringify({
action: 'done',
code: 'error',
message: 'Failed to parse ci_information JSON',
noProgressCount: inputNoProgressCount + 1,
envRerunCount,
}),
);
process.exit(0);
}
const {
cipeStatus,
selfHealingStatus,
verificationStatus,
selfHealingEnabled,
selfHealingSkippedReason,
failureClassification: rawFailureClassification,
failedTaskIds = [],
verifiedTaskIds = [],
couldAutoApplyTasks,
autoApplySkipped,
autoApplySkipReason,
userAction,
cipeUrl,
commitSha,
} = ci;
const failureClassification = rawFailureClassification?.toLowerCase() ?? null;
// --- Helpers ---
function categorizeTasks() {
const verifiedSet = new Set(verifiedTaskIds);
const unverified = failedTaskIds.filter((t) => !verifiedSet.has(t));
if (unverified.length === 0) return { category: 'all_verified' };
const e2e = unverified.filter((t) => {
const parts = t.split(':');
return parts.length >= 2 && parts[1].includes('e2e');
});
if (e2e.length === unverified.length) return { category: 'e2e_only' };
const verifiable = unverified.filter((t) => {
const parts = t.split(':');
return !(parts.length >= 2 && parts[1].includes('e2e'));
});
return { category: 'needs_local_verify', verifiableTaskIds: verifiable };
}
function backoff(count) {
const delays = [60, 90, 120, 180];
return delays[Math.min(count, delays.length - 1)];
}
function hasStateChanged() {
if (prevCipeStatus && cipeStatus !== prevCipeStatus) return true;
if (prevShStatus && selfHealingStatus !== prevShStatus) return true;
if (prevVerificationStatus && verificationStatus !== prevVerificationStatus)
return true;
if (
prevFailureClassification &&
failureClassification !== prevFailureClassification
)
return true;
return false;
}
function isTimedOut() {
if (timeoutSeconds <= 0) return false;
const avgDelay = pollCount === 0 ? 0 : backoff(Math.floor(pollCount / 2));
return pollCount * avgDelay >= timeoutSeconds;
}
function isWaitTimedOut() {
if (newCipeTimeoutSeconds <= 0) return false;
return pollCount * 30 >= newCipeTimeoutSeconds;
}
function isNewCipe() {
return (
(prevCipeUrl && cipeUrl && cipeUrl !== prevCipeUrl) ||
(expectedSha && commitSha && commitSha === expectedSha)
);
}
// ============================================================
// classify() — pure decision tree
//
// Returns: { action: 'poll'|'wait'|'done', code: string, extra? }
//
// Decision priority (top wins):
// WAIT MODE:
// 1. new CI Attempt detected → poll (new_cipe_detected)
// 2. wait timed out → done (no_new_cipe)
// 3. still waiting → wait (waiting_for_cipe)
// NORMAL MODE:
// 4. polling timeout → done (polling_timeout)
// 5. circuit breaker (13 polls) → done (circuit_breaker)
// 6. CI succeeded → done (ci_success)
// 7. CI canceled → done (cipe_canceled)
// 8. CI timed out → done (cipe_timed_out)
// 9. CI failed, no tasks recorded → done (cipe_no_tasks)
// 10. environment failure → done (environment_rerun_cap | environment_issue)
// 11. self-healing throttled → done (self_healing_throttled)
// 12. CI in progress / not started → poll (ci_running)
// 13. self-healing in progress → poll (sh_running)
// 14. flaky task auto-rerun → poll (flaky_rerun)
// 15. fix auto-applied → poll (fix_auto_applied)
// 16. auto-apply: skipped → done (fix_auto_apply_skipped)
// 17. auto-apply: verification pending→ poll (verification_pending)
// 18. auto-apply: verified → done (fix_auto_applying)
// 19. fix: verification failed/none → done (fix_needs_review)
// 20. fix: all/e2e verified → done (fix_apply_ready)
// 21. fix: needs local verify → done (fix_needs_local_verify)
// 22. self-healing failed → done (fix_failed)
// 23. no fix available → done (no_fix)
// 24. fallback → poll (fallback)
// ============================================================
function classify() {
// --- Wait mode ---
if (waitMode) {
if (isNewCipe()) return { action: 'poll', code: 'new_cipe_detected' };
if (isWaitTimedOut()) return { action: 'done', code: 'no_new_cipe' };
return { action: 'wait', code: 'waiting_for_cipe' };
}
// --- Guards ---
if (isTimedOut()) return { action: 'done', code: 'polling_timeout' };
if (noProgressCount >= 13) return { action: 'done', code: 'circuit_breaker' };
// --- Terminal CI states ---
if (cipeStatus === 'SUCCEEDED') return { action: 'done', code: 'ci_success' };
if (cipeStatus === 'CANCELED')
return { action: 'done', code: 'cipe_canceled' };
if (cipeStatus === 'TIMED_OUT')
return { action: 'done', code: 'cipe_timed_out' };
// --- CI failed, no tasks ---
if (
cipeStatus === 'FAILED' &&
failedTaskIds.length === 0 &&
selfHealingStatus == null
)
return { action: 'done', code: 'cipe_no_tasks' };
// --- Environment failure ---
if (failureClassification === 'environment_state') {
if (envRerunCount >= 2)
return { action: 'done', code: 'environment_rerun_cap' };
return { action: 'done', code: 'environment_issue' };
}
// --- Throttled ---
if (selfHealingSkippedReason === 'THROTTLED')
return { action: 'done', code: 'self_healing_throttled' };
// --- Still running: CI ---
if (cipeStatus === 'IN_PROGRESS' || cipeStatus === 'NOT_STARTED')
return { action: 'poll', code: 'ci_running' };
// --- Still running: self-healing ---
if (
(selfHealingStatus === 'IN_PROGRESS' ||
selfHealingStatus === 'NOT_STARTED') &&
!selfHealingSkippedReason
)
return { action: 'poll', code: 'sh_running' };
// --- Still running: flaky rerun ---
if (failureClassification === 'flaky_task')
return { action: 'poll', code: 'flaky_rerun' };
// --- Fix auto-applied, waiting for new CI Attempt ---
if (userAction === 'APPLIED_AUTOMATICALLY')
return { action: 'poll', code: 'fix_auto_applied' };
// --- Auto-apply path (couldAutoApplyTasks) ---
if (couldAutoApplyTasks === true) {
if (autoApplySkipped === true)
return {
action: 'done',
code: 'fix_auto_apply_skipped',
extra: { autoApplySkipReason },
};
if (
verificationStatus === 'NOT_STARTED' ||
verificationStatus === 'IN_PROGRESS'
)
return { action: 'poll', code: 'verification_pending' };
if (verificationStatus === 'COMPLETED')
return { action: 'done', code: 'fix_auto_applying' };
// verification FAILED or NOT_EXECUTABLE → falls through to fix_needs_review
}
// --- Fix available ---
if (selfHealingStatus === 'COMPLETED') {
if (
verificationStatus === 'FAILED' ||
verificationStatus === 'NOT_EXECUTABLE' ||
(couldAutoApplyTasks !== true && !verificationStatus)
)
return { action: 'done', code: 'fix_needs_review' };
const tasks = categorizeTasks();
if (tasks.category === 'all_verified' || tasks.category === 'e2e_only')
return { action: 'done', code: 'fix_apply_ready' };
return {
action: 'done',
code: 'fix_needs_local_verify',
extra: { verifiableTaskIds: tasks.verifiableTaskIds },
};
}
// --- Fix failed ---
if (selfHealingStatus === 'FAILED')
return { action: 'done', code: 'fix_failed' };
// --- No fix available ---
if (
cipeStatus === 'FAILED' &&
(selfHealingEnabled === false || selfHealingStatus === 'NOT_EXECUTABLE')
)
return { action: 'done', code: 'no_fix' };
// --- Fallback ---
return { action: 'poll', code: 'fallback' };
}
// ============================================================
// buildOutput() — maps classification to full JSON output
// ============================================================
// Message templates keyed by status or key
const messages = {
// wait mode
new_cipe_detected: () =>
`New CI Attempt detected! CI: ${cipeStatus || 'N/A'}`,
no_new_cipe: () =>
'New CI Attempt timeout exceeded. No new CI Attempt detected.',
waiting_for_cipe: () => 'Waiting for new CI Attempt...',
// guards
polling_timeout: () => 'Polling timeout exceeded.',
circuit_breaker: () => 'No progress after 13 consecutive polls. Stopping.',
// terminal
ci_success: () => 'CI passed successfully!',
cipe_canceled: () => 'CI Attempt was canceled.',
cipe_timed_out: () => 'CI Attempt timed out.',
cipe_no_tasks: () => 'CI failed but no Nx tasks were recorded.',
// environment
environment_rerun_cap: () => 'Environment rerun cap (2) exceeded. Bailing.',
environment_issue: () => 'CI: FAILED | Classification: ENVIRONMENT_STATE',
// throttled
self_healing_throttled: () =>
'Self-healing throttled \u2014 too many unapplied fixes.',
// polling
ci_running: () => `CI: ${cipeStatus}`,
sh_running: () => `CI: ${cipeStatus} | Self-healing: ${selfHealingStatus}`,
flaky_rerun: () =>
'CI: FAILED | Classification: FLAKY_TASK (auto-rerun in progress)',
fix_auto_applied: () =>
'CI: FAILED | Fix auto-applied, new CI Attempt spawning',
verification_pending: () =>
`CI: FAILED | Self-healing: COMPLETED | Verification: ${verificationStatus}`,
// actionable
fix_auto_applying: () => 'Fix verified! Auto-applying...',
fix_auto_apply_skipped: (extra) =>
`Fix verified but auto-apply was skipped. ${
extra?.autoApplySkipReason
? `Reason: ${extra.autoApplySkipReason}`
: 'Offer to apply manually.'
}`,
fix_needs_review: () =>
`Fix available but needs review. Verification: ${
verificationStatus || 'N/A'
}`,
fix_apply_ready: () => 'Fix available and verified. Ready to apply.',
fix_needs_local_verify: (extra) =>
`Fix available. ${extra.verifiableTaskIds.length} task(s) need local verification.`,
fix_failed: () => 'Self-healing failed to generate a fix.',
no_fix: () => 'CI failed, no fix available.',
// fallback
fallback: () =>
`CI: ${cipeStatus || 'N/A'} | Self-healing: ${
selfHealingStatus || 'N/A'
} | Verification: ${verificationStatus || 'N/A'}`,
};
// Codes where noProgressCount resets to 0 (genuine progress occurred)
const resetProgressCodes = new Set([
'ci_success',
'fix_auto_applying',
'fix_auto_apply_skipped',
'fix_needs_review',
'fix_apply_ready',
'fix_needs_local_verify',
]);
function formatMessage(msg) {
if (verbosity === 'minimal') {
const currentStatus = `${cipeStatus}|${selfHealingStatus}|${verificationStatus}`;
if (currentStatus === (prevStatus || '')) return null;
return msg;
}
if (verbosity === 'verbose') {
return [
`Poll #${pollCount + 1} | CI: ${cipeStatus || 'N/A'} | Self-healing: ${
selfHealingStatus || 'N/A'
} | Verification: ${verificationStatus || 'N/A'}`,
msg,
].join('\n');
}
return `Poll #${pollCount + 1} | ${msg}`;
}
function buildOutput(decision) {
const { action, code, extra } = decision;
// noProgressCount is already computed before classify() was called.
// Here we only handle the reset for "genuine progress" done-codes.
const msgFn = messages[code];
const rawMsg = msgFn ? msgFn(extra) : `Unknown: ${code}`;
const message = formatMessage(rawMsg);
const result = {
action,
code,
message,
noProgressCount: resetProgressCodes.has(code) ? 0 : noProgressCount,
envRerunCount,
};
// Add delay
if (action === 'wait') {
result.delay = 30;
} else if (action === 'poll') {
result.delay = code === 'new_cipe_detected' ? 60 : backoff(noProgressCount);
result.fields = 'light';
}
// Add extras
if (code === 'new_cipe_detected') result.newCipeDetected = true;
if (extra?.verifiableTaskIds)
result.verifiableTaskIds = extra.verifiableTaskIds;
if (extra?.autoApplySkipReason)
result.autoApplySkipReason = extra.autoApplySkipReason;
console.log(JSON.stringify(result));
}
// --- Run ---
// Compute noProgressCount from input. Single assignment, no mutation.
// Wait mode: reset on new cipe, otherwise unchanged (wait doesn't count as no-progress).
// Normal mode: reset on any state change, otherwise increment.
const noProgressCount = (() => {
if (waitMode) return isNewCipe() ? 0 : inputNoProgressCount;
if (isNewCipe() || hasStateChanged()) return 0;
return inputNoProgressCount + 1;
})();
buildOutput(classify());

View File

@@ -0,0 +1,160 @@
#!/usr/bin/env node
/**
* CI State Update Script
*
* Deterministic state management for CI monitor actions.
* Three commands: gate, post-action, cycle-check.
*
* Usage:
* node ci-state-update.mjs gate --gate-type <local-fix|env-rerun> [counter args]
* node ci-state-update.mjs post-action --action <type> [--cipe-url <url>] [--commit-sha <sha>]
* node ci-state-update.mjs cycle-check --code <code> [--agent-triggered] [counter args]
*/
// --- Arg parsing ---
const args = process.argv.slice(2);
const command = args[0];
function getFlag(name) {
return args.includes(name);
}
function getArg(name) {
const idx = args.indexOf(name);
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
}
function output(result) {
console.log(JSON.stringify(result));
}
// --- gate ---
// Check if an action is allowed and return incremented counter.
// Called before any local fix attempt or environment rerun.
function gate() {
const gateType = getArg('--gate-type');
if (gateType === 'local-fix') {
const count = parseInt(getArg('--local-verify-count') || '0', 10);
const max = parseInt(getArg('--local-verify-attempts') || '3', 10);
if (count >= max) {
return output({
allowed: false,
localVerifyCount: count,
message: `Local fix budget exhausted (${count}/${max} attempts)`,
});
}
return output({
allowed: true,
localVerifyCount: count + 1,
message: null,
});
}
if (gateType === 'env-rerun') {
const count = parseInt(getArg('--env-rerun-count') || '0', 10);
if (count >= 2) {
return output({
allowed: false,
envRerunCount: count,
message: `Environment issue persists after ${count} reruns. Manual investigation needed.`,
});
}
return output({
allowed: true,
envRerunCount: count + 1,
message: null,
});
}
output({ allowed: false, message: `Unknown gate type: ${gateType}` });
}
// --- post-action ---
// Compute next state after an action is taken.
// Returns wait mode params and whether the action was agent-triggered.
function postAction() {
const action = getArg('--action');
const cipeUrl = getArg('--cipe-url');
const commitSha = getArg('--commit-sha');
// MCP-triggered or auto-applied: track by cipeUrl
const cipeUrlActions = ['fix-auto-applying', 'apply-mcp', 'env-rerun'];
// Local push: track by commitSha
const commitShaActions = [
'apply-local-push',
'reject-fix-push',
'local-fix-push',
'auto-fix-push',
'empty-commit-push',
];
const trackByCipeUrl = cipeUrlActions.includes(action);
const trackByCommitSha = commitShaActions.includes(action);
if (!trackByCipeUrl && !trackByCommitSha) {
return output({ error: `Unknown action: ${action}` });
}
// fix-auto-applying: self-healing did it, NOT the monitor
const agentTriggered = action !== 'fix-auto-applying';
output({
waitMode: true,
pollCount: 0,
lastCipeUrl: trackByCipeUrl ? cipeUrl : null,
expectedCommitSha: trackByCommitSha ? commitSha : null,
agentTriggered,
});
}
// --- cycle-check ---
// Cycle classification + counter resets when a new "done" code is received.
// Called at the start of handling each actionable code.
function cycleCheck() {
const status = getArg('--code');
const wasAgentTriggered = getFlag('--agent-triggered');
let cycleCount = parseInt(getArg('--cycle-count') || '0', 10);
const maxCycles = parseInt(getArg('--max-cycles') || '10', 10);
let envRerunCount = parseInt(getArg('--env-rerun-count') || '0', 10);
// Cycle classification: if previous cycle was agent-triggered, count it
if (wasAgentTriggered) cycleCount++;
// Reset env_rerun_count on non-environment status
if (status !== 'environment_issue') envRerunCount = 0;
// Approaching limit gate
const approachingLimit = cycleCount >= maxCycles - 2;
output({
cycleCount,
agentTriggered: false,
envRerunCount,
approachingLimit,
message: approachingLimit
? `Approaching cycle limit (${cycleCount}/${maxCycles})`
: null,
});
}
// --- Dispatch ---
switch (command) {
case 'gate':
gate();
break;
case 'post-action':
postAction();
break;
case 'cycle-check':
cycleCheck();
break;
default:
output({ error: `Unknown command: ${command}` });
}

View File

@@ -0,0 +1,166 @@
---
name: nx-generate
description: Generate code using nx generators. INVOKE IMMEDIATELY when user mentions scaffolding, setup, structure, creating apps/libs, or setting up project structure. Trigger words - scaffold, setup, create a ... app, create a ... lib, project structure, generate, add a new project. ALWAYS use this BEFORE calling nx_docs or exploring - this skill handles discovery internally.
---
# Run Nx Generator
Nx generators are powerful tools that scaffold projects, make automated code migrations or automate repetitive tasks in a monorepo. They ensure consistency across the codebase and reduce boilerplate work.
This skill applies when the user wants to:
- Create new projects like libraries or applications
- Scaffold features or boilerplate code
- Run workspace-specific or custom generators
- Do anything else that an nx generator exists for
## Key Principles
1. **Always use `--no-interactive`** - Prevents prompts that would hang execution
2. **Read the generator source code** - The schema alone is not enough; understand what the generator actually does
3. **Match existing repo patterns** - Study similar artifacts in the repo and follow their conventions
4. **Verify with lint/test/build/typecheck etc.** - Generated code must pass verification. The listed targets are just an example, use what's appropriate for this workspace.
## Steps
### 1. Discover Available Generators
Use the Nx CLI to discover available generators:
- List all generators for a plugin: `npx nx list @nx/react`
- View available plugins: `npx nx list`
This includes plugin generators (e.g., `@nx/react:library`) and local workspace generators.
### 2. Match Generator to User Request
Identify which generator(s) could fulfill the user's needs. Consider what artifact type they want, which framework is relevant, and any specific generator names mentioned.
**IMPORTANT**: When both a local workspace generator and an external plugin generator could satisfy the request, **always prefer the local workspace generator**. Local generators are customized for the specific repo's patterns.
If no suitable generator exists, you can stop using this skill. However, the burden of proof is high—carefully consider all available generators before deciding none apply.
### 3. Get Generator Options
Use the `--help` flag to understand available options:
```bash
npx nx g @nx/react:library --help
```
Pay attention to required options, defaults that might need overriding, and options relevant to the user's request.
### Library Buildability
**Default to non-buildable libraries** unless there's a specific reason for buildable.
| Type | When to use | Generator flags |
| --------------------------- | ----------------------------------------------------------------- | ----------------------------------- |
| **Non-buildable** (default) | Internal monorepo libs consumed by apps | No `--bundler` flag |
| **Buildable** | Publishing to npm, cross-repo sharing, stable libs for cache hits | `--bundler=vite` or `--bundler=swc` |
Non-buildable libs:
- Export `.ts`/`.tsx` source directly
- Consumer's bundler compiles them
- Faster dev experience, less config
Buildable libs:
- Have their own build target
- Useful for stable libs that rarely change (cache hits)
- Required for npm publishing
**If unclear, ask the user:** "Should this library be buildable (own build step, better caching) or non-buildable (source consumed directly, simpler setup)?"
### 4. Read Generator Source Code
**This step is critical.** The schema alone does not tell you everything. Reading the source code helps you:
- Know exactly what files will be created/modified and where
- Understand side effects (updating configs, installing deps, etc.)
- Identify behaviors and options not obvious from the schema
- Understand how options interact with each other
To find generator source code:
- For plugin generators: Use `node -e "console.log(require.resolve('@nx/<plugin>/generators.json'));"` to find the generators.json, then locate the source from there
- If that fails, read directly from `node_modules/<plugin>/generators.json`
- For local generators: Typically in `tools/generators/` or a local plugin directory. Search the repo for the generator name.
After reading the source, reconsider: Is this the right generator? If not, go back to step 2.
> **⚠️ `--directory` flag behavior can be misleading.**
> It should specify the full path of the generated library or component, not the parent path that it will be generated in.
>
> ```bash
> # ✅ Correct - directory is the full path for the library
> nx g @nx/react:library --directory=libs/my-lib
> # generates libs/my-lib/package.json and more
>
> # ❌ Wrong - this will create files at libs and libs/src/...
> nx g @nx/react:library --name=my-lib --directory=libs
> # generates libs/package.json and more
> ```
### 5. Examine Existing Patterns
Before generating, examine the target area of the codebase:
- Look at similar existing artifacts (other libraries, applications, etc.)
- Identify naming conventions, file structures, and configuration patterns
- Note which test runners, build tools, and linters are used
- Configure the generator to match these patterns
### 6. Dry-Run to Verify File Placement
**Always run with `--dry-run` first** to verify files will be created in the correct location:
```bash
npx nx g @nx/react:library --name=my-lib --dry-run --no-interactive
```
Review the output carefully. If files would be created in the wrong location, adjust your options based on what you learned from the generator source code.
Note: Some generators don't support dry-run (e.g., if they install npm packages). If dry-run fails for this reason, proceed to running the generator for real.
### 7. Run the Generator
Execute the generator:
```bash
nx generate <generator-name> <options> --no-interactive
```
> **Tip:** New packages often need workspace dependencies wired up (e.g., importing shared types, being consumed by apps). The `link-workspace-packages` skill can help add these correctly.
### 8. Modify Generated Code (If Needed)
Generators provide a starting point. Modify the output as needed to:
- Add or modify functionality as requested
- Adjust imports, exports, or configurations
- Integrate with existing code patterns
**Important:** If you replace or delete generated test files (e.g., `*.spec.ts`), either write meaningful replacement tests or remove the `test` target from the project configuration. Empty test suites will cause `nx test` to fail.
### 9. Format and Verify
Format all generated/modified files:
```bash
nx format --fix
```
This example is for built-in nx formatting with prettier. There might be other formatting tools for this workspace, use these when appropriate.
Then verify the generated code works. Keep in mind that the changes you make with a generator or subsequent modifications might impact various projects so it's usually not enough to only run targets for the artifact you just created.
```bash
# these targets are just an example!
nx run-many -t build,lint,test,typecheck
```
These targets are common examples used across many workspaces. You should do research into other targets available for this workspace and its projects. CI configuration is usually a good guide for what the critical targets are that have to pass.
If verification fails with manageable issues (a few lint errors, minor type issues), fix them. If issues are extensive, attempt obvious fixes first, then escalate to the user with details about what was generated, what's failing, and what you've attempted.

View File

@@ -0,0 +1,238 @@
---
name: nx-import
description: Import, merge, or combine repositories into an Nx workspace using nx import. USE WHEN the user asks to adopt Nx across repos, move projects into a monorepo, or bring code/history from another repository.
---
## Quick Start
- `nx import` brings code from a source repository or folder into the current workspace, preserving commit history.
- After nx `22.6.0`, `nx import` responds with .ndjson outputs and follow-up questions. For earlier versions, always run with `--no-interactive` and specify all flags directly.
- Run `nx import --help` for available options.
- Make sure the destination directory is empty before importing.
EXAMPLE: target has `libs/utils` and `libs/models`; source has `libs/ui` and `libs/data-access` — you cannot import `libs/` into `libs/` directly. Import each source library individually.
Primary docs:
- https://nx.dev/docs/guides/adopting-nx/import-project
- https://nx.dev/docs/guides/adopting-nx/preserving-git-histories
Read the nx docs if you have the tools for it.
## Import Strategy
**Subdirectory-at-a-time** (`nx import <source> apps --source=apps`):
- **Recommended for monorepo sources** — files land at top level, no redundant config
- Caveats: multiple import commands (separate merge commits each); dest must not have conflicting directories; root configs (deps, plugins, targetDefaults) not imported
- **Directory conflicts**: Import into alternate-named dir (e.g. `imported-apps/`), then rename
**Whole repo** (`nx import <source> imported --source=.`):
- **Only for non-monorepo sources** (single-project repos)
- For monorepos, creates messy nested config (`imported/nx.json`, `imported/tsconfig.base.json`, etc.)
- If you must: keep imported `tsconfig.base.json` (projects extend it), prefix workspace globs and executor paths
### Directory Conventions
- **Always prefer the destination's existing conventions.** Source uses `libs/`but dest uses `packages/`? Import into `packages/` (`nx import <source> packages/foo --source=libs/foo`).
- If dest has no convention (empty workspace), ask the user.
### Application vs Library Detection
Before importing, identify whether the source is an **application** or a **library**:
- **Applications**: Deployable end products. Common indicators:
- _Frontend_: `next.config.*`, `vite.config.*` with a build entry point, framework-specific app scaffolding (CRA, Angular CLI app, etc.)
- _Backend (Node.js)_: Express/Fastify/NestJS server entrypoint, no `"exports"` field in `package.json`
- _JVM_: Maven `pom.xml` with `<packaging>jar</packaging>` or `<packaging>war</packaging>` and a `main` class; Gradle `application` plugin or `mainClass` setting
- _.NET_: `.csproj`/`.fsproj` with `<OutputType>Exe</OutputType>` or `<OutputType>WinExe</OutputType>`
- _General_: Dockerfile, a runnable entrypoint, no public API surface intended for import by other projects
- **Libraries**: Reusable packages consumed by other projects. Common indicators: `"main"`/`"exports"` in `package.json`, Maven/Gradle packaging as a library jar, .NET `<OutputType>Library</OutputType>`, named exports intended for import by other packages.
**Destination directory rules**:
- Applications → `apps/<name>`. Check workspace globs (e.g. `pnpm-workspace.yaml`, `workspaces` in root `package.json`) for an existing `apps/*` entry.
- If `apps/*` is **not** present, add it before importing: update the workspace glob config and commit (or stage) the change.
- Example: `nx import <source> apps/my-app --source=packages/my-app`
- Libraries → follow the dest's existing convention (`packages/`, `libs/`, etc.).
## Common Issues
### pnpm Workspace Globs (Critical)
`nx import` adds the imported directory itself (e.g. `apps`) to `pnpm-workspace.yaml`, **NOT** glob patterns for packages within it. Cross-package imports will fail with `Cannot find module`.
**Fix**: Replace with proper globs from the source config (e.g. `apps/*`, `libs/shared/*`), then `pnpm install`.
### Root Dependencies and Config Not Imported (Critical)
`nx import` does **NOT** merge from the source's root:
- `dependencies`/`devDependencies` from `package.json`
- `targetDefaults` from `nx.json` (e.g. `"@nx/esbuild:esbuild": { "dependsOn": ["^build"] }` — critical for build ordering)
- `namedInputs` from `nx.json` (e.g. `production` exclusion patterns for test files)
- Plugin configurations from `nx.json`
**Fix**: Diff source and dest `package.json` + `nx.json`. Add missing deps, merge relevant `targetDefaults` and `namedInputs`.
### TypeScript Project References
After import, run `nx sync --yes`. If it reports nothing but typecheck still fails, `nx reset` first, then `nx sync --yes` again.
### Explicit Executor Path Fixups
Inferred targets (via Nx plugins) resolve config relative to project root — no changes needed. Explicit executor targets (e.g. `@nx/esbuild:esbuild`) have workspace-root-relative paths (`main`, `outputPath`, `tsConfig`, `assets`, `sourceRoot`) that must be prefixed with the import destination directory.
### Plugin Detection
- **Whole-repo import**: `nx import` detects and offers to install plugins. Accept them.
- **Subdirectory import**: Plugins NOT auto-detected. Manually add with `npx nx add @nx/PLUGIN`. Check `include`/`exclude` patterns — defaults won't match alternate directories (e.g. `apps-beta/`).
- Run `npx nx reset` after any plugin config changes.
### Redundant Root Files (Whole-Repo Only)
Whole-repo import brings ALL source root files into the dest subdirectory. Clean up:
- `pnpm-lock.yaml` — stale; dest has its own lockfile
- `pnpm-workspace.yaml` — source workspace config; conflicts with dest
- `node_modules/` — stale symlinks pointing to source filesystem
- `.gitignore` — redundant with dest root `.gitignore`
- `nx.json` — source Nx config; dest has its own
- `README.md` — optional; keep or remove
**Don't blindly delete** `tsconfig.base.json` — imported projects may extend it via relative paths.
### Root ESLint Config Missing (Subdirectory Import)
Subdirectory import doesn't bring the source's root `eslint.config.mjs`, but project configs reference `../../eslint.config.mjs`.
**Fix order**:
1. Install ESLint deps first: `pnpm add -wD eslint@^9 @nx/eslint-plugin typescript-eslint` (plus framework-specific plugins)
2. Create root `eslint.config.mjs` (copy from source or create with `@nx/eslint-plugin` base rules)
3. Then `npx nx add @nx/eslint` to register the plugin in `nx.json`
Install `typescript-eslint` explicitly — pnpm's strict hoisting won't auto-resolve this transitive dep of `@nx/eslint-plugin`.
### ESLint Version Pinning (Critical)
**Pin ESLint to v9** (`eslint@^9.0.0`). ESLint 10 breaks `@nx/eslint` and many plugins with cryptic errors like `Cannot read properties of undefined (reading 'version')`.
`@nx/eslint` may peer-depend on ESLint 8, causing the wrong version to resolve. If lint fails with `Cannot read properties of undefined (reading 'allow')`, add `pnpm.overrides`:
```json
{ "pnpm": { "overrides": { "eslint": "^9.0.0" } } }
```
### Dependency Version Conflicts
After import, compare key deps (`typescript`, `eslint`, framework-specific). If dest uses newer versions, upgrade imported packages to match (usually safe). If source is newer, may need to upgrade dest first. Use `pnpm.overrides` to enforce single-version policy if desired.
### Module Boundaries
Imported projects may lack `tags`. Add tags or update `@nx/enforce-module-boundaries` rules.
### Project Name Collisions (Multi-Import)
Same `name` in `package.json` across source and dest causes `MultipleProjectsWithSameNameError`. **Fix**: Rename conflicting names (e.g. `@org/api``@org/teama-api`), update all dep references and import statements, `pnpm install`. The root `package.json` of each imported repo also becomes a project — rename those too.
### Workspace Dep Import Ordering
`pnpm install` fails during `nx import` if a `"workspace:*"` dependency hasn't been imported yet. File operations still succeed. **Fix**: Import all projects first, then `pnpm install --no-frozen-lockfile`.
### `.gitkeep` Blocking Subdirectory Import
The TS preset creates `packages/.gitkeep`. Remove it and commit before importing.
### Frontend tsconfig Base Settings (Critical)
The TS preset defaults (`module: "nodenext"`, `moduleResolution: "nodenext"`, `lib: ["es2022"]`) are incompatible with frontend frameworks (React, Next.js, Vue, Vite). After importing frontend projects, verify the dest root `tsconfig.base.json`:
- **`moduleResolution`**: Must be `"bundler"` (not `"nodenext"`)
- **`module`**: Must be `"esnext"` (not `"nodenext"`)
- **`lib`**: Must include `"dom"` and `"dom.iterable"` (frontend projects need these)
- **`jsx`**: `"react-jsx"` for React-only workspaces, per-project for mixed frameworks
For **subdirectory imports**, the dest root tsconfig is authoritative — update it. For **whole-repo imports**, imported projects may extend their own nested `tsconfig.base.json`, making this less critical.
If the dest also has backend projects needing `nodenext`, use per-project overrides instead of changing the root.
**Gotcha**: TypeScript does NOT merge `lib` arrays — a project-level override **replaces** the base array entirely. Always include all needed entries (e.g. `es2022`, `dom`, `dom.iterable`) in any project-level `lib`.
### `@nx/react` Typings for Libraries
React libraries generated with `@nx/react:library` reference `@nx/react/typings/cssmodule.d.ts` and `@nx/react/typings/image.d.ts` in their tsconfig `types`. These fail with `Cannot find type definition file` unless `@nx/react` is installed in the dest workspace.
**Fix**: `pnpm add -wD @nx/react`
### Jest Preset Missing (Subdirectory Import)
Nx presets create `jest.preset.js` at the workspace root, and project jest configs reference it (e.g. `../../jest.preset.js`). Subdirectory import does NOT bring this file.
**Fix**:
1. Run `npx nx add @nx/jest` — registers `@nx/jest/plugin` in `nx.json` and updates `namedInputs`
2. Create `jest.preset.js` at workspace root (see `references/JEST.md` for content) — `nx add` only creates this when a generator runs, not on bare `nx add`
3. Install test runner deps: `pnpm add -wD jest jest-environment-jsdom ts-jest @types/jest`
4. Install framework-specific test deps as needed (see `references/JEST.md`)
For deeper Jest issues (tsconfig.spec.json, Babel transforms, CI atomization, Jest vs Vitest coexistence), see `references/JEST.md`.
### Target Name Prefixing (Whole-Repo Import)
When importing a project with existing npm scripts (`build`, `dev`, `start`, `lint`), Nx plugins auto-prefix inferred target names to avoid conflicts: e.g. `next:build`, `vite:build`, `eslint:lint`.
**Fix**: Remove the Nx-rewritten npm scripts from the imported `package.json`, then either:
- Accept the prefixed names (e.g. `nx run app:next:build`)
- Rename plugin target names in `nx.json` to use unprefixed names
## Non-Nx Source Issues
When the source is a plain pnpm/npm workspace without `nx.json`.
### npm Script Rewriting (Critical)
Nx rewrites `package.json` scripts during init, creating broken commands (e.g. `vitest run``nx test run`). **Fix**: Remove all rewritten scripts — Nx plugins infer targets from config files.
### `noEmit` → `composite` + `emitDeclarationOnly` (Critical)
Plain TS projects use `"noEmit": true`, incompatible with Nx project references.
**Symptoms**: "typecheck target is disabled because one or more project references set 'noEmit: true'" or TS6310.
**Fix** in **all** imported tsconfigs:
1. Remove `"noEmit": true`. If inherited via extends chain, set `"noEmit": false` explicitly.
2. Add `"composite": true`, `"emitDeclarationOnly": true`, `"declarationMap": true`
3. Add `"outDir": "dist"` and `"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"`
4. Add `"extends": "../../tsconfig.base.json"` if missing. Remove settings now inherited from base.
### Stale node_modules and Lockfiles
`nx import` may bring `node_modules/` (pnpm symlinks pointing to the source filesystem) and `pnpm-lock.yaml` from the source. Both are stale.
**Fix**: `rm -rf imported/node_modules imported/pnpm-lock.yaml imported/pnpm-workspace.yaml imported/.gitignore`, then `pnpm install`.
### ESLint Config Handling
- **Legacy `.eslintrc.json` (ESLint 8)**: Delete all `.eslintrc.*`, remove v8 deps, create flat `eslint.config.mjs`.
- **Flat config (`eslint.config.js`)**: Self-contained configs can often be left as-is.
- **No ESLint**: Create both root and project-level configs from scratch.
### TypeScript `paths` Aliases
Nx uses `package.json` `"exports"` + pnpm workspace linking instead of tsconfig `"paths"`. If packages have proper `"exports"`, paths are redundant. Otherwise, update paths for the new directory structure.
## Technology-specific Guidance
Identify technologies in the source repo, then read and apply the matching reference file(s).
Available references:
- `references/ESLINT.md` — ESLint projects: duplicate `lint`/`eslint:lint` targets, legacy `.eslintrc.*` linting generated files, flat config `.cjs` self-linting, `typescript-eslint` v7/v9 peer dep conflict, mixed ESLint v8+v9 in one workspace.
- `references/GRADLE.md`
- `references/JEST.md` — Jest testing: `@nx/jest/plugin` setup, jest.preset.js, testing deps by framework, tsconfig.spec.json, Jest vs Vitest coexistence, Babel transforms, CI atomization.
- `references/NEXT.md` — Next.js projects: `@nx/next/plugin` targets, `withNx`, Next.js TS config (`noEmit`, `jsx: "preserve"`), auto-installing deps via wrong PM, non-Nx `create-next-app` imports, mixed Next.js+Vite coexistence.
- `references/TURBOREPO.md`
- `references/VITE.md` — Vite projects (React, Vue, or both): `@nx/vite/plugin` typecheck target, `resolve.alias`/`__dirname` fixes, framework deps, Vue-specific setup, mixed React+Vue coexistence.

View File

@@ -0,0 +1,109 @@
## ESLint
ESLint-specific guidance for `nx import`. For generic import issues (root deps, pnpm globs, project references), see `SKILL.md`.
---
### How `@nx/eslint/plugin` Works
`@nx/eslint/plugin` scans for ESLint config files and creates a lint target for each project. It detects **both** flat config files (`eslint.config.{js,mjs,cjs,ts,mts,cts}`) and legacy config files (`.eslintrc.{json,js,cjs,mjs,yml,yaml}`).
**Plugin options (set during `nx add @nx/eslint`):**
```json
{
"plugin": "@nx/eslint/plugin",
"options": {
"targetName": "eslint:lint"
}
}
```
**Auto-installation**: `nx import` auto-detects ESLint config files and offers to install `@nx/eslint`. Accept the offer — it registers the plugin and updates `namedInputs.production` to exclude ESLint config files.
---
### Duplicate `lint` and `eslint:lint` Targets
After import, projects will have **two** lint-related targets if the source `package.json` has a `"lint"` npm script:
- `eslint:lint` — inferred by `@nx/eslint/plugin`; has proper caching and input/output tracking
- `lint` — created by Nx from the npm script via `nx:run-script`; no caching intelligence, just wraps `npm run lint`
**Fix**: Remove the `"lint"` script from each project's `package.json`. Keep `"lint:fix"` if present — there is no plugin-inferred equivalent for auto-fixing.
---
### Legacy `.eslintrc.*` Configs Linting Generated Files
When `@nx/eslint/plugin` runs `eslint .` on a project with a legacy `.eslintrc.*` config that uses `parserOptions.project`, it tries to lint **all** files in the project directory including:
- Generated `dist/**/*.d.ts` files (not in tsconfig `include`)
- The `.eslintrc.js` config file itself (not in tsconfig `include`)
This causes `Parsing error: ESLint was configured to run on X using parserOptions.project, however that TSConfig does not include this file`.
**Fix**: Add `ignorePatterns` to the `.eslintrc.*` config:
```json
// .eslintrc.json
{
"ignorePatterns": ["dist/**"]
}
```
```js
// .eslintrc.js — also ignore the config file itself since module.exports isn't in tsconfig
module.exports = {
ignorePatterns: ['dist/**', '.eslintrc.js'],
// ...
};
```
---
### Flat Config `.cjs` Files Self-Linting
When a project uses `eslint.config.cjs` (CJS flat config), `eslint .` lints the config file itself. The `require()` call on line 1 triggers `@typescript-eslint/no-require-imports`.
**Fix**: Add the config filename to the top-level `ignores` array:
```js
module.exports = tseslint.config(
{
ignores: ['dist/**', 'node_modules/**', 'eslint.config.cjs'],
},
// ...
);
```
The same applies to `eslint.config.js` in a CJS project (no `"type": "module"`) if it uses `require()`.
---
### `typescript-eslint` Version Conflict With ESLint 9
`typescript-eslint@7.x` declares `peerDependencies: { "eslint": "^8.56.0" }`, but it is commonly used alongside `"eslint": "^9.0.0"`. npm treats this as a hard peer dep conflict and refuses to install.
**Root cause**: `@nx/eslint` init adds `eslint@~8.57.0` at the workspace root (for its own peer deps). Workspace packages that request `eslint@^9.0.0` + `typescript-eslint@^7.0.0` trigger the conflict when npm resolves their deps.
**Fix**: Upgrade `typescript-eslint` from `^7.0.0` to `^8.0.0` directly in the affected workspace package's `package.json`. The `tseslint.config()` API and `tseslint.configs.recommended` are identical between v7 and v8 — no config changes needed.
```json
// packages/my-package/package.json
{
"devDependencies": {
"typescript-eslint": "^8.0.0"
}
}
```
**Note**: npm's root-level `"overrides"` field does not force versions for workspace packages' direct dependencies — update each package.json individually.
---
### Mixed ESLint v8 and v9 in One Workspace
Legacy v8 and flat-config v9 packages can coexist in the same workspace. Each package resolves its own `eslint` version. The root `eslint@~8.57.0` (added by `@nx/eslint` init) is used by legacy v8 packages; v9 packages get their own hoisted `eslint@9`.
`@nx/eslint/plugin` infers `eslint:lint` targets for **both** config formats. Legacy packages run ESLint v8 with `.eslintrc.*`; flat-config packages run ESLint v9 with `eslint.config.*`. No special nx.json configuration is needed to support both simultaneously.

View File

@@ -0,0 +1,12 @@
## Gradle
- If you import an entire Gradle repository into a subfolder, files like `gradlew`, `gradlew.bat`, and `gradle/wrapper` will end up inside that imported subfolder.
- The `@nx/gradle` plugin expects those files at the workspace root to infer Gradle projects/tasks automatically.
- If the target workspace has no Gradle setup yet, consider moving those files to the root (especially when using `@nx/gradle`).
- If the target workspace already has Gradle configured, avoid duplicate wrappers: remove imported duplicates from the subfolder or merge carefully.
- Because the import lands in a subfolder, Gradle project references can break; review settings and project path references, then fix any errors.
- If `@nx/gradle` is installed, run `nx show projects` to verify that Gradle projects are being inferred.
Helpful docs:
- https://nx.dev/docs/technologies/java/gradle/introduction

View File

@@ -0,0 +1,223 @@
## Jest
Jest-specific guidance for `nx import`. For the basic "Jest Preset Missing" fix (create `jest.preset.js`, install deps), see `SKILL.md`. This file covers deeper Jest integration issues.
---
### How `@nx/jest` Works
`@nx/jest/plugin` scans for `jest.config.{ts,js,cjs,mjs,cts,mts}` and creates a `test` target for each project.
**Plugin options:**
```json
{
"plugin": "@nx/jest/plugin",
"options": {
"targetName": "test"
}
}
```
`npx nx add @nx/jest` does two things:
1. **Registers `@nx/jest/plugin` in `nx.json`** — without this, no `test` targets are inferred
2. Updates `namedInputs.production` to exclude test files
**Gotcha**: `nx add @nx/jest` does NOT create `jest.preset.js` — that file is only generated when you run a generator (e.g. `@nx/jest:configuration`). For imports, you must create it manually (see "Jest Preset" section below).
**Other gotcha**: If you create `jest.preset.js` manually but skip `npx nx add @nx/jest`, the plugin won't be registered and `nx run PROJECT:test` will fail with "Cannot find target 'test'". You need both.
---
### Jest Preset
The preset provides shared Jest configuration (test patterns, ts-jest transform, resolver, jsdom environment).
**Root `jest.preset.js`:**
```js
const nxPreset = require('@nx/jest/preset').default;
module.exports = { ...nxPreset };
```
**Project `jest.config.ts`:**
```ts
export default {
displayName: 'my-lib',
preset: '../../jest.preset.js',
// project-specific overrides
};
```
The `preset` path is relative from the project root to the workspace root. Subdirectory imports preserve the original relative path (e.g. `../../jest.preset.js`), which resolves correctly if the import destination matches the source directory depth.
---
### Testing Dependencies
#### Core (always needed)
```
pnpm add -wD jest ts-jest @types/jest @nx/jest
```
#### Environment-specific
- **DOM testing** (React, Vue, browser libs): `jest-environment-jsdom`
- **Node testing** (APIs, CLIs): no extra deps (Jest defaults to `node` env, but Nx preset defaults to `jsdom`)
#### React testing
```
pnpm add -wD @testing-library/react @testing-library/jest-dom
```
#### React with Babel (non-ts-jest transform)
Some React projects use Babel instead of ts-jest for JSX transformation:
```
pnpm add -wD babel-jest @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
```
**When**: Project `jest.config` has `transform` using `babel-jest` instead of `ts-jest`. Common in older Nx workspaces and CRA migrations.
#### Vue testing
```
pnpm add -wD @vue/test-utils
```
Vue projects typically use Vitest (not Jest) — see VITE.md.
---
### `tsconfig.spec.json`
Jest projects need a `tsconfig.spec.json` that includes test files:
```json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}
```
**Common issues after import:**
- Missing `"types": ["jest", "node"]` — causes `describe`/`it`/`expect` to be unrecognized
- Missing `"module": "commonjs"` — Jest doesn't support ESM by default (ts-jest transpiles to CJS)
- `include` array missing test patterns — TypeScript won't check test files
---
### Jest vs Vitest Coexistence
Workspaces can have both:
- **Jest**: Next.js apps, older React libs, Node libraries
- **Vitest**: Vite-based React/Vue apps and libs
Both `@nx/jest/plugin` and `@nx/vite/plugin` (which infers Vitest targets) coexist without conflicts — they detect different config files (`jest.config.*` vs `vite.config.*`).
**Target naming**: Both default to `test`. If a project somehow has both config files, rename one:
```json
{
"plugin": "@nx/jest/plugin",
"options": { "targetName": "jest-test" }
}
```
---
### `@testing-library/jest-dom` — Jest vs Vitest
Projects migrating from Jest to Vitest (or workspaces with both) need different imports:
**Jest** (in `test-setup.ts`):
```ts
import '@testing-library/jest-dom';
```
**Vitest** (in `test-setup.ts`):
```ts
import '@testing-library/jest-dom/vitest';
```
If the source used Jest but the dest workspace uses Vitest for that project type, update the import path. Also add `@testing-library/jest-dom` to tsconfig `types` array.
---
### Non-Nx Source: Test Script Rewriting
Nx rewrites `package.json` scripts during init. Test scripts get broken:
- `"test": "jest"``"test": "nx test"` (circular if no executor configured)
- `"test": "vitest run"``"test": "nx test run"` (broken — `run` becomes an argument)
**Fix**: Remove all rewritten test scripts. `@nx/jest/plugin` and `@nx/vite/plugin` infer test targets from config files.
---
### CI Atomization
`@nx/jest/plugin` supports splitting tests per-file for CI parallelism:
```json
{
"plugin": "@nx/jest/plugin",
"options": {
"targetName": "test",
"ciTargetName": "test-ci"
}
}
```
This creates `test-ci--src/lib/foo.spec.ts` targets for each test file, enabling Nx Cloud distribution. Not relevant during import, but useful for post-import CI setup.
---
### Common Post-Import Issues
1. **"Cannot find target 'test'"**: `@nx/jest/plugin` not registered in `nx.json`. Run `npx nx add @nx/jest` or manually add the plugin entry.
2. **"Cannot find module 'jest-preset'"**: `jest.preset.js` missing at workspace root. Create it (see SKILL.md).
3. **"Cannot find type definition file for 'jest'"**: Missing `@types/jest` or `tsconfig.spec.json` doesn't have `"types": ["jest", "node"]`.
4. **Tests fail with "Cannot use import statement outside a module"**: `ts-jest` not installed or not configured as transform. Check `jest.config.ts` transform section.
5. **Snapshot path mismatches**: After import, `__snapshots__` directories may have paths baked in. Run tests once with `--updateSnapshot` to regenerate.
---
## Fix Order
### Subdirectory Import (Nx Source)
1. `npx nx add @nx/jest` — registers plugin in `nx.json` (does NOT create `jest.preset.js`)
2. Create `jest.preset.js` manually (see "Jest Preset" section above)
3. Install deps: `pnpm add -wD jest jest-environment-jsdom ts-jest @types/jest`
4. Install framework test deps: `@testing-library/react @testing-library/jest-dom` (React), `@vue/test-utils` (Vue)
5. Verify `tsconfig.spec.json` has `"types": ["jest", "node"]`
6. `nx run-many -t test`
### Whole-Repo Import (Non-Nx Source)
1. Remove rewritten test scripts from `package.json`
2. `npx nx add @nx/jest` — registers plugin (does NOT create preset)
3. Create `jest.preset.js` manually
4. Install deps (same as above)
5. Verify/fix `jest.config.*` — ensure `preset` path points to root `jest.preset.js`
6. Verify/fix `tsconfig.spec.json` — add `types`, `module`, `include` if missing
7. `nx run-many -t test`

View File

@@ -0,0 +1,214 @@
## Next.js
Next.js-specific guidance for `nx import`. For generic import issues (pnpm globs, root deps, project references, name collisions, ESLint, frontend tsconfig base settings, `@nx/react` typings, Jest preset, target name prefixing, non-Nx source handling), see `SKILL.md`.
---
### `@nx/next/plugin` Inferred Targets
`@nx/next/plugin` detects `next.config.{ts,js,cjs,mjs}` and creates these targets:
- `build``next build` (with `dependsOn: ['^build']`)
- `dev``next dev`
- `start``next start` (depends on `build`)
- `serve-static` → same as `start`
- `build-deps` / `watch-deps` — for TS solution setup
**No separate typecheck target** — Next.js runs TypeScript checking as part of `next build`. The `@nx/js/typescript` plugin provides a standalone `typecheck` target for non-Next libraries in the workspace.
**Build target conflict**: Both `@nx/next/plugin` and `@nx/js/typescript` define a `build` target. `@nx/next/plugin` wins for Next.js projects (it detects `next.config.*`), while `@nx/js/typescript` handles libraries with `tsconfig.lib.json`. No rename needed — they coexist.
### `withNx` in `next.config.js`
Nx-generated Next.js projects use `composePlugins(withNx)` from `@nx/next`. This wrapper is optional for `next build` via the inferred plugin (which just runs `next build`), but it provides Nx-specific configuration. Keep it if present.
### Root Dependencies for Next.js
Beyond the generic root deps issue (see SKILL.md), Next.js projects typically need:
**Core**: `react`, `react-dom`, `@types/react`, `@types/react-dom`, `@types/node`, `@nx/react` (see SKILL.md for `@nx/react` typings)
**Nx plugins**: `@nx/next` (auto-installed by import), `@nx/eslint`, `@nx/jest`
**Testing**: see SKILL.md "Jest Preset Missing" section
**ESLint**: `@next/eslint-plugin-next` (in addition to generic ESLint deps from SKILL.md)
### Next.js Auto-Installing Dependencies via Wrong Package Manager
Next.js detects missing `@types/react` during `next build` and tries to install it using `yarn add` regardless of the actual package manager. In a pnpm workspace, this fails with a "nearest package directory isn't part of the project" error.
**Root cause**: `@types/react` is missing from root devDependencies.
**Fix**: Install deps at the root before building: `pnpm add -wD @types/react @types/react-dom`
### Next.js TypeScript Config Specifics
Next.js app tsconfigs have unique patterns compared to Vite:
- **`noEmit: true`** with `emitDeclarationOnly: false` — Next.js handles emit, TS just checks types. This conflicts with `composite: true` from the TS solution setup.
- **`"types": ["jest", "node"]`** — includes test types in the main tsconfig (no separate `tsconfig.app.json`)
- **`"plugins": [{ "name": "next" }]`** — for IDE integration
- **`include`** references `.next/types/**/*.ts` for Next.js auto-generated types
- **`"jsx": "preserve"`** — Next.js uses its own JSX transform, not React's
**Gotcha**: The Next.js tsconfig sets `"noEmit": true` which disables `composite` mode. This is fine because Next.js projects use `next build` for building, not `tsc`. The `@nx/js/typescript` plugin's `typecheck` target is not needed for Next.js apps.
### `next.config.js` Lint Warning
Imported Next.js configs may have `// eslint-disable-next-line @typescript-eslint/no-var-requires` but the project ESLint config enables different rule sets. This produces `Unused eslint-disable directive` warnings. Harmless — remove the comment or ignore.
### `@nx/next:init` Rewrites All npm Scripts (Whole-Repo Import)
When `@nx/next:init` runs during a whole-repo import, it rewrites the project's `package.json` scripts to prefixed `nx` calls:
```json
{
"dev": "nx next:dev",
"build": "nx next:build",
"start": "nx next:start"
}
```
This is the standard "npm Script Rewriting" issue from SKILL.md, but triggered by `@nx/next:init` rather than Nx init. **Fix**: Remove all rewritten scripts from `package.json``@nx/next/plugin` infers all targets from `next.config.*`.
---
## Non-Nx Source (create-next-app)
### Whole-Repo Import Recommended
For single-project `create-next-app` repos, use whole-repo import into a subdirectory:
```bash
nx import /path/to/source apps/web --ref=main --source=. --no-interactive
```
### `next-env.d.ts`
`next build` auto-generates `next-env.d.ts` at the project root. Add `next-env.d.ts` to the dest root `.gitignore` — it is framework-generated and should not be committed.
### ESLint: Self-Contained `eslint-config-next`
`create-next-app` generates a flat ESLint config using `eslint-config-next` (which bundles its own plugins). This is **self-contained** — no root `eslint.config.mjs` needed, no `@nx/eslint-plugin` dependency. The `@nx/eslint/plugin` detects it and creates a lint target.
### TypeScript: No Changes Needed
Non-Nx Next.js projects have self-contained tsconfigs with `noEmit: true`, their own `lib`, `module`, `moduleResolution`, and `jsx` settings. Since `next build` handles type checking internally, no tsconfig modifications are needed. The project does NOT need to extend `tsconfig.base.json`.
**Gotcha**: The `@nx/js/typescript` plugin won't create a `typecheck` target because there's no `tsconfig.lib.json`. This is fine — use `next:build` for type checking.
### `noEmit: true` and TS Solution Setup
Non-Nx Next.js projects use `noEmit: true`, which conflicts with Nx's TS solution setup (`composite: true`). If the dest workspace uses project references and you want the Next.js app to participate:
1. Remove `noEmit: true`, add `composite: true`, `emitDeclarationOnly: true`
2. Add `extends: "../../tsconfig.base.json"`
3. Add `outDir` and `tsBuildInfoFile`
**However**, this is optional for standalone Next.js apps that don't export types consumed by other workspace projects.
### Tailwind / PostCSS
`create-next-app` with Tailwind generates `postcss.config.mjs`. This works as-is after import — no path changes needed since PostCSS resolves relative to the project root.
---
## Mixed Next.js + Vite Coexistence
When both Next.js and Vite projects exist in the same workspace.
### Plugin Coexistence
Both `@nx/next/plugin` and `@nx/vite/plugin` can coexist in `nx.json`. They detect different config files (`next.config.*` vs `vite.config.*`) so there are no conflicts. The `@nx/js/typescript` plugin handles libraries.
### Vite Standalone Project tsconfig Fixes
Vite standalone projects (imported as whole-repo) have self-contained tsconfigs without `composite: true`. The `@nx/js/typescript` plugin's typecheck target runs `tsc --build --emitDeclarationOnly` which requires `composite`.
**Fix**:
1. Add `extends: "../../tsconfig.base.json"` to the root project tsconfig
2. Add `composite: true`, `declaration: true`, `declarationMap: true`, `tsBuildInfoFile` to `tsconfig.app.json` and `tsconfig.spec.json`
3. Set `moduleResolution: "bundler"` (replace `"node"`)
4. Add source files to `tsconfig.spec.json` `include` — specs import app code, and `composite` mode requires all files to be listed
### Typecheck Target Names
- `@nx/vite/plugin` defaults `typecheckTargetName` to `"vite:typecheck"`
- `@nx/js/typescript` uses `"typecheck"`
- Next.js projects have NO standalone typecheck target — Next.js runs type checking during `next build`
No naming conflicts between frameworks.
---
## Fix Order — Nx Source (Subdirectory Import)
1. Import Next.js apps into `apps/<name>` (see SKILL.md: "Application vs Library Detection")
2. Generic fixes from SKILL.md (pnpm globs, root deps, `.gitkeep` removal, frontend tsconfig base settings, `@nx/react` typings)
3. Install Next.js-specific deps: `pnpm add -wD @next/eslint-plugin-next`
4. ESLint setup (see SKILL.md: "Root ESLint Config Missing")
5. Jest setup (see SKILL.md: "Jest Preset Missing")
6. `nx reset && nx sync --yes && nx run-many -t typecheck,build,test,lint`
## Fix Order — Non-Nx Source (create-next-app)
1. Import into `apps/<name>` (see SKILL.md: "Application vs Library Detection")
2. Generic fixes from SKILL.md (pnpm globs, stale files cleanup, script rewriting, target name prefixing)
3. (Optional) If app needs to export types for other workspace projects: fix `noEmit``composite` (see SKILL.md)
4. `nx reset && nx run-many -t next:build,eslint:lint` (or unprefixed names if renamed)
---
## Iteration Log
### Scenario 1: Basic Nx Next.js App Router + Shared Lib → TS preset (PASS)
- Source: CNW next preset (Next.js 16, App Router) + `@nx/react:library` shared-ui
- Dest: CNW ts preset (Nx 23)
- Import: subdirectory-at-a-time (apps, libs separately)
- Errors found & fixed:
1. pnpm-workspace.yaml: `apps`/`libs``apps/*`/`libs/*`
2. Root tsconfig: `nodenext``bundler`, add `dom`/`dom.iterable` to `lib`, add `jsx: react-jsx`
3. Missing `@nx/react` (for CSS module/image type defs in lib)
4. Missing `@types/react`, `@types/react-dom`, `@types/node`
5. Next.js trying `yarn add @types/react` — fixed by installing at root
6. Missing `@nx/eslint`, root `eslint.config.mjs`, ESLint plugins
7. Missing `@nx/jest`, `jest.preset.js`, `jest-environment-jsdom`, `ts-jest`
- All targets green: typecheck, build, test, lint
### Scenario 3: Non-Nx create-next-app (App Router + Tailwind) → TS preset (PASS)
- Source: `create-next-app@latest` (Next.js 16.1.6, App Router, Tailwind v4, flat ESLint config)
- Dest: CNW ts preset (Nx 23)
- Import: whole-repo into `apps/web`
- Errors found & fixed:
1. pnpm-workspace.yaml: `apps/web``apps/*`
2. Stale files: `node_modules/`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `.gitignore` — deleted
3. Nx-rewritten npm scripts (`"build": "nx next:build"`, etc.) — removed
- No tsconfig changes needed — self-contained config with `noEmit: true`
- ESLint self-contained via `eslint-config-next` — no root config needed
- No test setup (create-next-app doesn't include tests)
- All targets green: next:build, eslint:lint
### Scenario 4: Non-Nx create-next-app (alongside Vite, React Router 7, TanStack, CRA) → TS preset (PASS)
- See VITE.md Scenario 6 for the full multi-import scenario
- Next.js-specific findings:
1. `@nx/next:init` rewrote all scripts to `nx next:*` format — removed all rewritten scripts
2. Stale files: `node_modules/`, `package-lock.json`, `.gitignore` — deleted (npm workspace, no pnpm files)
3. ESLint self-contained via `eslint-config-next` — no root config needed
4. No tsconfig changes needed — `noEmit: true` stays; `next build` handles type checking
- Targets: `next:build`, `next:dev`, `next:start`, `eslint:lint`
### Scenario 5: Mixed Next.js (Nx) + Vite React (standalone) → TS preset (PASS)
- Source A: CNW next preset (Next.js 16, App Router) — subdirectory import of `apps/`
- Source B: CNW react-standalone preset (Vite 7, React 19) — whole-repo import into `apps/vite-app`
- Dest: CNW ts preset (Nx 23)
- Errors found & fixed:
1. All Scenario 1 fixes for the Next.js app
2. Stale files from Vite source: `node_modules/`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `.gitignore`, `nx.json`
3. Removed rewritten scripts from Vite app's `package.json`
4. ESLint 8 vs 9 conflict — `@nx/eslint` peer on ESLint 8 resolved wrong version. Fixed with `pnpm.overrides`
5. Vite tsconfigs missing `composite: true`, `declaration: true` — needed for `tsc --build --emitDeclarationOnly`
6. Vite `tsconfig.spec.json` `include` missing source files — specs import app code
7. Vite tsconfig `moduleResolution: "node"``"bundler"`, added `extends: "../../tsconfig.base.json"`
- All targets green: typecheck, build, test, lint for both projects

View File

@@ -0,0 +1,62 @@
## Turborepo
- Nx replaces Turborepo task orchestration, but a clean migration requires handling Turborepo's config packages.
- Migration guide: https://nx.dev/docs/guides/adopting-nx/from-turborepo#easy-automated-migration-example
- Since Nx replaces Turborepo, all turbo config files and config packages become dead code and should be removed.
## The Config-as-Package Pattern
Turborepo monorepos ship with internal workspace packages that share configuration:
- **`@repo/typescript-config`** (or similar) — tsconfig files (`base.json`, `nextjs.json`, `react-library.json`, etc.)
- **`@repo/eslint-config`** (or similar) — ESLint config files and all ESLint plugin dependencies
These are not code libraries. They distribute config via Node module resolution (e.g., `"extends": "@repo/typescript-config/nextjs.json"`). This is the **default** Turborepo pattern — expect it in virtually every Turborepo import. Package names vary — check `package.json` files to identify the actual names.
## Check for Root Config Files First
**Before doing any config merging, check whether the destination workspace uses shared root configuration.** This decides how to handle the config packages.
- If the workspace has a root `tsconfig.base.json` and/or root `eslint.config.mjs` that projects extend, merge the config packages into these root configs (see steps below).
- If the workspace does NOT have root config files — each project manages its own configuration independently (similar to Turborepo). In this case, **do not create root config files or merge into them**. Just remove turbo-specific parts (`turbo.json`, `eslint-plugin-turbo`) and leave the config packages in place, or ask the user how they want to handle them.
If unclear, check for the presence of `tsconfig.base.json` at the root or ask the user.
## Merging TypeScript Config (Only When Root tsconfig.base.json Exists)
The config package contains a hierarchy of tsconfig files. Each project extends one via package name.
1. **Read the config package** — trace the full inheritance chain (e.g., `nextjs.json` extends `base.json`).
2. **Update root `tsconfig.base.json`** — absorb `compilerOptions` from the base config. Add Nx `paths` for cross-project imports (Turborepo doesn't use path aliases, Nx relies on them).
3. **Update each project's `tsconfig.json`**:
- Change `"extends"` from `"@repo/typescript-config/<variant>.json"` to the relative path to root `tsconfig.base.json`.
- Inline variant-specific overrides from the intermediate config (e.g., Next.js: `"module": "ESNext"`, `"moduleResolution": "Bundler"`, `"jsx": "preserve"`, `"noEmit": true`; React library: `"jsx": "react-jsx"`).
- Preserve project-specific settings (`outDir`, `include`, `exclude`, etc.).
4. **Delete the config package** and remove it from all `devDependencies`.
## Merging ESLint Config (Only When Root eslint.config Exists)
The config package centralizes ESLint plugin dependencies and exports composable flat configs.
1. **Read the config package** — identify exported configs, plugin dependencies, and inheritance.
2. **Update root `eslint.config.mjs`** — absorb base rules (JS recommended, TypeScript-ESLint, Prettier, etc.). Drop `eslint-plugin-turbo`.
3. **Update each project's `eslint.config.mjs`** — switch from importing `@repo/eslint-config/<variant>` to extending the root config, adding framework-specific plugins inline.
4. **Move ESLint plugin dependencies** from the config package to root `devDependencies`.
5. If `@nx/eslint` plugin is configured with inferred targets, remove `"lint"` scripts from project `package.json` files.
6. **Delete the config package** and remove it from all `devDependencies`.
## General Cleanup
- Remove turbo-specific dependencies: `turbo`, `eslint-plugin-turbo`.
- Delete all `turbo.json` files (root and per-package).
- Run workspace validation (`nx run-many -t build lint test typecheck`) to confirm nothing broke.
## Key Pitfalls
- **Trace the full inheritance chain** before inlining — check what each variant inherits from the base.
- **Module resolution changes** — from Node package resolution (`@repo/...`) to relative paths (`../../tsconfig.base.json`).
- **ESLint configs are JavaScript, not JSON** — handle JS imports, array spreading, and plugin objects when merging.
Helpful docs:
- https://nx.dev/docs/guides/adopting-nx/from-turborepo

View File

@@ -0,0 +1,393 @@
## Vite
Vite-specific guidance for `nx import`. For generic import issues (pnpm globs, root deps, project references, name collisions, ESLint, frontend tsconfig base settings, `@nx/react` typings, Jest preset, non-Nx source handling), see `SKILL.md`.
---
### `@nx/vite/plugin` Typecheck Target
`@nx/vite/plugin` defaults `typecheckTargetName` to `"vite:typecheck"`. If the workspace expects `"typecheck"`, set it explicitly in `nx.json`. If `@nx/js/typescript` is also registered, rename one target to avoid conflicts (e.g. `"tsc-typecheck"` for the JS plugin).
Keep both plugins only if the workspace has non-Vite pure TS libraries — `@nx/js/typescript` handles those while `@nx/vite/plugin` handles Vite projects.
### @nx/vite Plugin Install Failure
Plugin init loads `vite.config.ts` before deps are available. **Fix**: `pnpm add -wD vite @vitejs/plugin-react` (or `@vitejs/plugin-vue`) first, then `pnpm exec nx add @nx/vite`.
### Vite `resolve.alias` and `__dirname` (Non-Nx Sources)
**`__dirname` undefined** (CJS-only): Replace with `fileURLToPath(new URL('./src', import.meta.url))` from `'node:url'`.
**`@/` path alias**: Vite's `resolve.alias` works at runtime but TS needs matching `"paths"`. Set `"baseUrl": "."` in project tsconfig.
**PostCSS/Tailwind**: Verify `content` globs resolve correctly after import.
### Missing TypeScript `types` (Non-Nx Sources)
Non-Nx tsconfigs may not declare all needed types. Ensure Vite projects include `"types": ["node", "vite/client"]` in their tsconfig.
### `noEmit` Fix: Vite-Specific Notes
See SKILL.md for the generic noEmit→composite fix. Vite-specific additions:
- Non-Nx Vite projects often have **both** `tsconfig.app.json` and `tsconfig.node.json` with `noEmit` — fix both
- Solution-style tsconfigs (`"files": [], "references": [...]`) may lack `extends`. Add `extends` pointing to the dest root `tsconfig.base.json` so base settings (`moduleResolution`, `lib`) apply.
- This is safe — Vite/Vitest ignore TypeScript emit settings.
### Dependency Version Conflicts
**Shared Vite deps (both frameworks):** `vite`, `vitest`, `jsdom`, `@types/node`, `typescript` (dev)
**Vite 6→7**: Typecheck fails (`Plugin<any>` type mismatch); build/serve still works. Fix: align versions.
**Vitest 3→4**: Usually works; type conflicts may surface in shared test utils.
---
## React Router 7 (Vite-Based)
React Router 7 (`@react-router/dev`) uses Vite under the hood with a `vite.config.ts` and a `react-router.config.ts`. The `@nx/vite/plugin` detects `vite.config.ts` and creates inferred targets.
### Targets
`@nx/vite/plugin` creates `build`, `dev`, `serve` targets. The `build` target invokes the script defined in `package.json` (usually `react-router build`), not `vite build` directly.
**No separate typecheck target from `@nx/vite/plugin`** — React Router 7 typegen is run as part of `typecheck` (e.g. `react-router typegen && tsc`). The `typecheck` target is inferred from the tsconfig. Keep the `typecheck` script in `package.json` if present; it is not rewritten.
### tsconfig Notes
React Router 7 uses a single `tsconfig.json` (no `tsconfig.app.json`/`tsconfig.node.json` split). It includes:
- `"rootDirs": [".", "./.react-router/types"]` — for generated type files; keep as-is
- `"paths": { "~/*": ["./app/*"] }` — self-referential alias; keep as-is
- `"noEmit": true` — replace with composite settings per SKILL.md
### Build Output
React Router 7 outputs to `build/` (not `dist/`). Add `build` to the dest root `.gitignore`.
### Generated Types Directory
React Router 7 generates `.react-router/` at the project root for route type generation. Add `.react-router` to the dest root `.gitignore`.
---
## TanStack Start (Vite-Based)
TanStack Start uses Vinxi under the hood, which wraps Vite. Projects have a standard `vite.config.ts` that `@nx/vite/plugin` detects normally.
### Targets
`@nx/vite/plugin` creates `build`, `dev`, `preview`, `serve-static`, `typecheck` targets. The `build` target runs `vite build` which invokes the TanStack Start Vinxi pipeline (produces both client and SSR bundles).
### tsconfig Notes
TanStack Start uses a single `tsconfig.json` with `"allowImportingTsExtensions": true` and `"noEmit": true`. Apply the standard noEmit → composite fix. `allowImportingTsExtensions` is compatible with `emitDeclarationOnly: true` — no change needed.
### `paths` Aliases
TanStack Start commonly uses `"#/*": ["./src/*"]` and `"@/*": ["./src/*"]`. These are self-referential — keep as-is for a single-project app.
### Uncommitted Source Repo
`create-tan-stack` initializes a git repo but does NOT make an initial commit. Before importing, commit first:
```bash
git -C /path/to/source add . && git -C /path/to/source commit -m "Initial commit"
```
### Generated and Build Directories
TanStack Start / Vinxi / Nitro generate several directories that must be added to the dest root `.gitignore`:
- `.vinxi` — Vinxi build cache
- `.tanstack` — TanStack generated files
- `.nitro` — Nitro build artifacts
- `.output` — server-side build output (SSR/edge)
These are not covered by `dist` or `build`.
---
## React-Specific
### React Dependencies
**Production:** `react`, `react-dom`
**Dev:** `@types/react`, `@types/react-dom`, `@vitejs/plugin-react`, `@testing-library/react`, `@testing-library/jest-dom`, `jsdom`
**ESLint (Nx sources):** `eslint-plugin-import`, `eslint-plugin-jsx-a11y`, `eslint-plugin-react`, `eslint-plugin-react-hooks`
**ESLint (`create-vite`):** `eslint-plugin-react-refresh`, `eslint-plugin-react-hooks` — self-contained flat configs can be left as-is
**Nx plugins:** `@nx/react` (generators), `@nx/vite`, `@nx/vitest`, `@nx/eslint`
### React TypeScript Configuration
Add `"jsx": "react-jsx"` — in `tsconfig.base.json` for single-framework workspaces, per-project for mixed (see Mixed section).
### React ESLint Config
```js
import nx from '@nx/eslint-plugin';
import baseConfig from '../../eslint.config.mjs';
export default [...baseConfig, ...nx.configs['flat/react'], { files: ['**/*.ts', '**/*.tsx'], rules: {} }];
```
### React Version Conflicts
React 18 (source) + React 19 (dest): pnpm may hoist mismatched `react-dom`, causing `TypeError: Cannot read properties of undefined (reading 'S')`. **Fix**: Align versions with `pnpm.overrides`.
### `@testing-library/jest-dom` with Vitest
If source used Jest: change import to `@testing-library/jest-dom/vitest` in test-setup.ts, add to tsconfig `types`.
---
## Vue-Specific
### Vue Dependencies
**Production:** `vue` (plus `vue-router`, `pinia` if used)
**Dev:** `@vitejs/plugin-vue`, `vue-tsc`, `@vue/test-utils`, `jsdom`
**ESLint:** `eslint-plugin-vue`, `vue-eslint-parser`, `@vue/eslint-config-typescript`, `@vue/eslint-config-prettier`
**Nx plugins:** `@nx/vue` (generators), `@nx/vite`, `@nx/vitest`, `@nx/eslint` (install AFTER deps — see below)
### Vue TypeScript Configuration
Add to `tsconfig.base.json` (single-framework) or per-project (mixed):
```json
{ "jsx": "preserve", "jsxImportSource": "vue", "resolveJsonModule": true }
```
### `vue-shims.d.ts`
Vue SFC files need a type declaration. Usually exists in each project's `src/` and imports cleanly. If missing:
```ts
declare module '*.vue' {
import { defineComponent } from 'vue';
const component: ReturnType<typeof defineComponent>;
export default component;
}
```
### `vue-tsc` Auto-Detection
Both `@nx/js/typescript` and `@nx/vite/plugin` auto-detect `vue-tsc` when installed — no manual config needed. Remove source scripts like `"typecheck": "vue-tsc --noEmit"`.
### ESLint Plugin Installation Order (Critical)
`@nx/eslint` init **crashes** if Vue ESLint deps aren't installed first (it loads all config files).
**Correct order:**
1. `pnpm add -wD eslint@^9 eslint-plugin-vue vue-eslint-parser @vue/eslint-config-typescript @typescript-eslint/parser @nx/eslint-plugin typescript-eslint`
2. Create root `eslint.config.mjs`
3. Then `npx nx add @nx/eslint`
### Vue ESLint Config Pattern
```js
import vue from 'eslint-plugin-vue';
import vueParser from 'vue-eslint-parser';
import tsParser from '@typescript-eslint/parser';
import baseConfig from '../../eslint.config.mjs';
export default [
...baseConfig,
...vue.configs['flat/recommended'],
{
files: ['**/*.vue'],
languageOptions: { parser: vueParser, parserOptions: { parser: tsParser } },
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
rules: { 'vue/multi-word-component-names': 'off' },
},
];
```
**Important**: `vue-eslint-parser` override must come **AFTER** base config — `flat/typescript` sets the TS parser globally without a `files` filter, breaking `.vue` parsing.
`vue-eslint-parser` must be an explicit pnpm dependency (strict resolution prevents transitive import).
**Known issue**: Some generated Vue ESLint configs omit `vue-eslint-parser`. Use the pattern above instead.
---
## Mixed React + Vue
When both frameworks coexist, several settings become per-project.
### tsconfig `jsx` — Per-Project Only
- React: `"jsx": "react-jsx"` in project tsconfig
- Vue: `"jsx": "preserve"`, `"jsxImportSource": "vue"` in project tsconfig
- Root: **NO** `jsx` setting
### Typecheck — Auto-Detects Framework
`@nx/vite/plugin` uses `vue-tsc` for Vue projects and `tsc` for React automatically.
```json
{
"plugins": [
{ "plugin": "@nx/eslint/plugin", "options": { "targetName": "lint" } },
{
"plugin": "@nx/vite/plugin",
"options": {
"buildTargetName": "build",
"typecheckTargetName": "typecheck",
"testTargetName": "test"
}
}
]
}
```
Remove `@nx/js/typescript` if all projects use Vite. Keep it (renamed to `"tsc-typecheck"`) only for non-Vite pure TS libs.
### ESLint — Three-Tier Config
1. **Root**: Base rules only, no framework-specific rules
2. **React projects**: Extend root + `nx.configs['flat/react']`
3. **Vue projects**: Extend root + `vue.configs['flat/recommended']` + `vue-eslint-parser`
**Required packages**: Shared (`eslint@^9`, `@nx/eslint-plugin`, `typescript-eslint`, `@typescript-eslint/parser`), React (`eslint-plugin-import`, `eslint-plugin-jsx-a11y`, `eslint-plugin-react`, `eslint-plugin-react-hooks`), Vue (`eslint-plugin-vue`, `vue-eslint-parser`)
`@nx/react`/`@nx/vue` are for generators only — no target conflicts.
---
## Redundant npm Scripts After Import
`nx import` copies `package.json` verbatim, so npm scripts come along. For Vite-based projects `@nx/vite/plugin` already infers the same targets from `vite.config.ts` — the npm scripts just shadow the plugin with weaker `nx:run-script` wrappers (no first-class caching inputs/outputs). Remove them after import.
### Standalone Vite App (`create-vite`)
Remove the following scripts — every one is redundant:
| Script | Plugin replacement |
| ----------------------------- | ---------------------------------------------------------------------------- |
| `dev: vite` | `@nx/vite/plugin``dev` |
| `build: tsc -b && vite build` | `@nx/vite/plugin``build`; `typecheck` via `@nx/js/typescript` handles tsc |
| `preview: vite preview` | `@nx/vite/plugin``preview` |
| `lint: eslint .` | `@nx/eslint/plugin``eslint:lint` |
### TanStack Start
Remove `build`, `dev`, `preview`, and `test` scripts, but move any hardcoded `--port` flag to `vite.config.ts` first:
```ts
// vite.config.ts
export default defineConfig({
server: { port: 3000 }, // replaces `vite dev --port 3000`
...
})
```
### React Router 7 — Keep ALL scripts
Do **not** remove React Router 7 scripts. They use the framework CLI (`react-router build`, `react-router dev`, `react-router-serve`) which is not interchangeable with plain `vite`:
- `typecheck` runs `react-router typegen && tsc` — typegen must precede `tsc` or it fails on missing route types
- `start` serves the SSR bundle — no plugin equivalent
---
## Fix Orders
### Nx Source
1. Generic fixes from SKILL.md (pnpm globs, root deps, executor paths, frontend tsconfig base settings, `@nx/react` typings)
2. Configure `@nx/vite/plugin` typecheck target
3. **React**: `jsx: "react-jsx"` (root or per-project)
4. **Vue**: `jsx: "preserve"` + `jsxImportSource: "vue"`; verify `vue-shims.d.ts`; install ESLint deps before `@nx/eslint`
5. **Mixed**: `jsx` per-project; remove/rename `@nx/js/typescript`
6. `nx sync --yes && nx reset && nx run-many -t typecheck,build,test,lint`
### Non-Nx Source (additional steps)
0. Import into `apps/<name>` (see SKILL.md: "Application vs Library Detection")
1. Generic fixes from SKILL.md (stale files cleanup, pnpm globs, rewritten scripts, target name prefixing, noEmit→composite, ESLint handling)
2. Fix `noEmit` in **all** tsconfigs (app, node, etc. — non-Nx projects often have multiple)
3. Add `extends` to solution-style tsconfigs so root settings apply
4. Fix `resolve.alias` / `__dirname` / `baseUrl`
5. Ensure `types` include `vite/client` and `node`
6. Install `@nx/vite` manually if it failed during import
7. Remove redundant npm scripts so `@nx/vite/plugin` infers them natively (see "Redundant npm Scripts" section)
8. **Vue**: Add `outDir` + `**/*.vue.d.ts` to ESLint ignores
9. Full verification
### Multiple-Source Imports
See SKILL.md for generic multi-import (name collisions, dep refs). Vite-specific: fix tsconfig `references` paths for alternate directories (`../../libs/``../../libs-beta/`).
### Non-Nx Source: React Router 7
1. Ensure source has at least one commit (see SKILL.md: "Source Repo Has No Commits")
2. `nx import` whole-repo into `apps/<name>` (see SKILL.md: "Application vs Library Detection") → auto-installs `@nx/vite`, `@nx/react`
3. Stale file cleanup: `node_modules/`, `package-lock.json`, `.gitignore`
4. Fix `tsconfig.json`: `noEmit``composite + emitDeclarationOnly + outDir + tsBuildInfoFile`
5. Add `build` and `.react-router` to dest root `.gitignore`
6. **Keep all npm scripts** — React Router 7 uses framework CLI (`react-router build/dev`), not plain vite (see "Redundant npm Scripts" above)
7. `npm install && nx reset && nx sync --yes`
### Non-Nx Source: TanStack Start
1. Ensure source has at least one commit — `create-tan-stack` does NOT auto-commit (see SKILL.md)
2. `nx import` whole-repo into `apps/<name>` (see SKILL.md: "Application vs Library Detection") → auto-installs `@nx/vite`, `@nx/vitest`
3. Stale file cleanup: `node_modules/`, `package-lock.json`, `.gitignore`
4. Fix `tsconfig.json`: `noEmit``composite + emitDeclarationOnly + outDir + tsBuildInfoFile`
5. Keep `allowImportingTsExtensions` — compatible with `emitDeclarationOnly: true`
6. Add `.vinxi`, `.tanstack`, `.nitro`, `.output` to dest root `.gitignore`
7. Move hardcoded `--port` from `dev` script into `vite.config.ts` (`server: { port: N }`)
8. Remove redundant npm scripts — `@nx/vite/plugin` infers `build`, `dev`, `preview`, `test` (see "Redundant npm Scripts" above)
9. `npm install && nx reset && nx sync --yes`
### Quick Reference: React vs Vue
| Aspect | React | Vue |
| ------------- | ------------------------ | ----------------------------------------- |
| Vite plugin | `@vitejs/plugin-react` | `@vitejs/plugin-vue` |
| Type checker | `tsc` | `vue-tsc` (auto-detected) |
| SFC support | N/A | `vue-shims.d.ts` needed |
| tsconfig jsx | `"react-jsx"` | `"preserve"` + `"jsxImportSource": "vue"` |
| ESLint parser | Standard TS | `vue-eslint-parser` + TS sub-parser |
| ESLint setup | Straightforward | Must install deps before `@nx/eslint` |
| Test utils | `@testing-library/react` | `@vue/test-utils` |
### Quick Reference: Vite-Based React Frameworks
| Aspect | Vite (standalone) | React Router 7 | TanStack Start |
| ------------------ | ----------------- | ----------------------- | ------------------------ |
| Build config | `vite.config.ts` | `vite.config.ts` | `vite.config.ts` |
| Build output | `dist/` | `build/` | `dist/` |
| SSR bundle | No | Yes (`build/server/`) | Yes (`dist/server/`) |
| tsconfig layout | app + node split | Single tsconfig | Single tsconfig |
| Auto-committed | Depends on tool | Usually yes | **No — commit first** |
| `nx import` plugin | `@nx/vite` | `@nx/vite`, `@nx/react` | `@nx/vite`, `@nx/vitest` |
---
## Iteration Log
### Scenario 6: Multiple non-Nx React apps (CRA, Next.js, React Router 7, TanStack Start, Vite) → TS preset (PASS)
- Sources: 5 standalone non-Nx repos with different build tools
- Dest: CNW ts preset (Nx 22.5.1), npm workspaces, `packages/*`
- Import: whole-repo for each, sequential into `packages/<name>`
- Pre-import fixes:
1. Removed `packages/.gitkeep` and committed
2. `git init && git add . && git commit` in Vite app (no git at all)
3. `git add . && git commit` in TanStack app (git init'd but no commits)
- Import: `npm exec nx -- import <source> packages/<name> --source=. --ref=main --no-interactive`
- Next.js import auto-installed `@nx/eslint`, `@nx/next`
- React Router 7 import auto-installed `@nx/vite`, `@nx/react`, `@nx/docker` (Dockerfile present)
- TanStack import auto-installed `@nx/vitest`
- Post-import fixes:
1. Removed stale `node_modules/`, `package-lock.json`, `.gitignore` from each package
2. Removed Nx-rewritten scripts from `board-games-nextjs/package.json` (had `"build": "nx next:build"`, etc.)
3. Updated root `tsconfig.base.json`: `nodenext``bundler`, added `dom`/`dom.iterable` to lib, added `jsx: react-jsx`
4. Added `build` to dest root `.gitignore` (CRA and React Router 7 output there)
5. Fixed `noEmit``composite + emitDeclarationOnly` in: `board-games-vite/tsconfig.app.json`, `board-games-vite/tsconfig.node.json`, `board-games-react-router/tsconfig.json`, `board-games-tanstack/tsconfig.json`
6. Fixed `tsBuildInfoFile` paths from `./node_modules/.tmp/...` to `./dist/...`
7. Installed root `@types/react`, `@types/react-dom`, `@types/node`
- All targets green: `build` for all 5 projects; `typecheck` for Vite/React Router/TanStack; `next:build` for Next.js

View File

@@ -0,0 +1,9 @@
---
name: nx-plugins
description: Find and add Nx plugins. USE WHEN user wants to discover available plugins, install a new plugin, or add support for a specific framework or technology to the workspace.
---
## Finding and Installing new plugins
- List plugins: `pnpm nx list`
- Install plugins `pnpm nx add <plugin>`. Example: `pnpm nx add @nx/react`.

View File

@@ -0,0 +1,58 @@
---
name: nx-run-tasks
description: Helps with running tasks in an Nx workspace. USE WHEN the user wants to execute build, test, lint, serve, or run any other tasks defined in the workspace.
---
You can run tasks with Nx in the following way.
Keep in mind that you might have to prefix things with npx/pnpx/yarn if the user doesn't have nx installed globally. Look at the package.json or lockfile to determine which package manager is in use.
For more details on any command, run it with `--help` (e.g. `nx run-many --help`, `nx affected --help`).
## Understand which tasks can be run
You can check those via `nx show project <projectname> --json`, for example `nx show project myapp --json`. It contains a `targets` section which has information about targets that can be run. You can also just look at the `package.json` scripts or `project.json` targets, but you might miss out on inferred tasks by Nx plugins.
## Run a single task
```
nx run <project>:<task>
```
where `project` is the project name defined in `package.json` or `project.json` (if present).
## Run multiple tasks
```
nx run-many -t build test lint typecheck
```
You can pass a `-p` flag to filter to specific projects, otherwise it runs on all projects. You can also use `--exclude` to exclude projects, and `--parallel` to control the number of parallel processes (default is 3).
Examples:
- `nx run-many -t test -p proj1 proj2` — test specific projects
- `nx run-many -t test --projects=*-app --exclude=excluded-app` — test projects matching a pattern
- `nx run-many -t test --projects=tag:api-*` — test projects by tag
## Run tasks for affected projects
Use `nx affected` to only run tasks on projects that have been changed and projects that depend on changed projects. This is especially useful in CI and for large workspaces.
```
nx affected -t build test lint
```
By default it compares against the base branch. You can customize this:
- `nx affected -t test --base=main --head=HEAD` — compare against a specific base and head
- `nx affected -t test --files=libs/mylib/src/index.ts` — specify changed files directly
## Useful flags
These flags work with `run`, `run-many`, and `affected`:
- `--skipNxCache` — rerun tasks even when results are cached
- `--verbose` — print additional information such as stack traces
- `--nxBail` — stop execution after the first failed task
- `--configuration=<name>` — use a specific configuration (e.g. `production`)

View File

@@ -0,0 +1,284 @@
---
name: nx-workspace
description: "Explore and understand Nx workspaces. USE WHEN answering questions about the workspace, projects, or tasks. ALSO USE WHEN an nx command fails or you need to check available targets/configuration before running a task. EXAMPLES: 'What projects are in this workspace?', 'How is project X configured?', 'What depends on library Y?', 'What targets can I run?', 'Cannot find configuration for task', 'debug nx task failure'."
---
# Nx Workspace Exploration
This skill provides read-only exploration of Nx workspaces. Use it to understand workspace structure, project configuration, available targets, and dependencies.
Keep in mind that you might have to prefix commands with `npx`/`pnpx`/`yarn` if nx isn't installed globally. Check the lockfile to determine the package manager in use.
## Listing Projects
Use `nx show projects` to list projects in the workspace.
The project filtering syntax (`-p`/`--projects`) works across many Nx commands including `nx run-many`, `nx release`, `nx show projects`, and more. Filters support explicit names, glob patterns, tag references (e.g. `tag:name`), directories, and negation (e.g. `!project-name`).
```bash
# List all projects
nx show projects
# Filter by pattern (glob)
nx show projects --projects "apps/*"
nx show projects --projects "shared-*"
# Filter by tag
nx show projects --projects "tag:publishable"
nx show projects -p 'tag:publishable,!tag:internal'
# Filter by target (projects that have a specific target)
nx show projects --withTarget build
# Combine filters
nx show projects --type lib --withTarget test
nx show projects --affected --exclude="*-e2e"
nx show projects -p "tag:scope:client,packages/*"
# Negate patterns
nx show projects -p '!tag:private'
nx show projects -p '!*-e2e'
# Output as JSON
nx show projects --json
```
## Project Configuration
Use `nx show project <name> --json` to get the full resolved configuration for a project.
**Important**: Do NOT read `project.json` directly - it only contains partial configuration. The `nx show project --json` command returns the full resolved config including inferred targets from plugins.
You can read the full project schema at `node_modules/nx/schemas/project-schema.json` to understand nx project configuration options.
```bash
# Get full project configuration
nx show project my-app --json
# Extract specific parts from the JSON
nx show project my-app --json | jq '.targets'
nx show project my-app --json | jq '.targets.build'
nx show project my-app --json | jq '.targets | keys'
# Check project metadata
nx show project my-app --json | jq '{name, root, sourceRoot, projectType, tags}'
```
## Target Information
Targets define what tasks can be run on a project.
```bash
# List all targets for a project
nx show project my-app --json | jq '.targets | keys'
# Get full target configuration
nx show project my-app --json | jq '.targets.build'
# Check target executor/command
nx show project my-app --json | jq '.targets.build.executor'
nx show project my-app --json | jq '.targets.build.command'
# View target options
nx show project my-app --json | jq '.targets.build.options'
# Check target inputs/outputs (for caching)
nx show project my-app --json | jq '.targets.build.inputs'
nx show project my-app --json | jq '.targets.build.outputs'
# Find projects with a specific target
nx show projects --withTarget serve
nx show projects --withTarget e2e
```
## Workspace Configuration
Read `nx.json` directly for workspace-level configuration.
You can read the full project schema at `node_modules/nx/schemas/nx-schema.json` to understand nx project configuration options.
```bash
# Read the full nx.json
cat nx.json
# Or use jq for specific sections
cat nx.json | jq '.targetDefaults'
cat nx.json | jq '.namedInputs'
cat nx.json | jq '.plugins'
cat nx.json | jq '.generators'
```
Key nx.json sections:
- `targetDefaults` - Default configuration applied to all targets of a given name
- `namedInputs` - Reusable input definitions for caching
- `plugins` - Nx plugins and their configuration
- ...and much more, read the schema or nx.json for details
## Affected Projects
If the user is asking about affected projects, read the [affected projects reference](references/AFFECTED.md) for detailed commands and examples.
## Common Exploration Patterns
### "What's in this workspace?"
```bash
nx show projects
nx show projects --type app
nx show projects --type lib
```
### "How do I build/test/lint project X?"
```bash
nx show project X --json | jq '.targets | keys'
nx show project X --json | jq '.targets.build'
```
### "What depends on library Y?"
```bash
# Use the project graph to find dependents
nx graph --print | jq '.graph.dependencies | to_entries[] | select(.value[].target == "Y") | .key'
```
## Programmatic Answers
When processing nx CLI results, use command-line tools to compute the answer programmatically rather than counting or parsing output manually. Always use `--json` flags to get structured output that can be processed with `jq`, `grep`, or other tools you have installed locally.
### Listing Projects
```bash
nx show projects --json
```
Example output:
```json
["my-app", "my-app-e2e", "shared-ui", "shared-utils", "api"]
```
Common operations:
```bash
# Count projects
nx show projects --json | jq 'length'
# Filter by pattern
nx show projects --json | jq '.[] | select(startswith("shared-"))'
# Get affected projects as array
nx show projects --affected --json | jq '.'
```
### Project Details
```bash
nx show project my-app --json
```
Example output:
```json
{
"root": "apps/my-app",
"name": "my-app",
"sourceRoot": "apps/my-app/src",
"projectType": "application",
"tags": ["type:app", "scope:client"],
"targets": {
"build": {
"executor": "@nx/vite:build",
"options": { "outputPath": "dist/apps/my-app" }
},
"serve": {
"executor": "@nx/vite:dev-server",
"options": { "buildTarget": "my-app:build" }
},
"test": {
"executor": "@nx/vite:test",
"options": {}
}
},
"implicitDependencies": []
}
```
Common operations:
```bash
# Get target names
nx show project my-app --json | jq '.targets | keys'
# Get specific target config
nx show project my-app --json | jq '.targets.build'
# Get tags
nx show project my-app --json | jq '.tags'
# Get project root
nx show project my-app --json | jq -r '.root'
```
### Project Graph
```bash
nx graph --print
```
Example output:
```json
{
"graph": {
"nodes": {
"my-app": {
"name": "my-app",
"type": "app",
"data": { "root": "apps/my-app", "tags": ["type:app"] }
},
"shared-ui": {
"name": "shared-ui",
"type": "lib",
"data": { "root": "libs/shared-ui", "tags": ["type:ui"] }
}
},
"dependencies": {
"my-app": [{ "source": "my-app", "target": "shared-ui", "type": "static" }],
"shared-ui": []
}
}
}
```
Common operations:
```bash
# Get all project names from graph
nx graph --print | jq '.graph.nodes | keys'
# Find dependencies of a project
nx graph --print | jq '.graph.dependencies["my-app"]'
# Find projects that depend on a library
nx graph --print | jq '.graph.dependencies | to_entries[] | select(.value[].target == "shared-ui") | .key'
```
## Troubleshooting
### "Cannot find configuration for task X:target"
```bash
# Check what targets exist on the project
nx show project X --json | jq '.targets | keys'
# Check if any projects have that target
nx show projects --withTarget target
```
### "The workspace is out of sync"
```bash
nx sync
nx reset # if sync doesn't fix stale cache
```

View File

@@ -0,0 +1,27 @@
## Affected Projects
Find projects affected by changes in the current branch.
```bash
# Affected since base branch (auto-detected)
nx show projects --affected
# Affected with explicit base
nx show projects --affected --base=main
nx show projects --affected --base=origin/main
# Affected between two commits
nx show projects --affected --base=abc123 --head=def456
# Affected apps only
nx show projects --affected --type app
# Affected excluding e2e projects
nx show projects --affected --exclude="*-e2e"
# Affected by uncommitted changes
nx show projects --affected --uncommitted
# Affected by untracked files
nx show projects --affected --untracked
```

13
.claude/settings.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extraKnownMarketplaces": {
"nx-claude-plugins": {
"source": {
"source": "github",
"repo": "nrwl/nx-ai-agents-config"
}
}
},
"enabledPlugins": {
"nx@nx-claude-plugins": true
}
}

12
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm
# Install pnpm globally as root, then make available to the node user
RUN corepack enable && corepack prepare pnpm@latest --activate
# Switch to non-root user (provided by base image)
USER node
# Pre-create pnpm store directory for cache mounting later if desired
RUN mkdir -p /home/node/.local/share/pnpm
ENV PNPM_HOME=/home/node/.local/share/pnpm
ENV PATH=$PNPM_HOME:$PATH

View File

@@ -0,0 +1,44 @@
{
"name": "fog-expedition",
"build": {
"dockerfile": "Dockerfile"
},
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
"ghcr.io/devcontainers/features/git:1": {}
},
"forwardPorts": [3000, 4200, 5432, 6379, 443],
"portsAttributes": {
"3000": { "label": "API (Nest)" },
"4200": { "label": "Overlay (Angular)" },
"5432": { "label": "Postgres" },
"6379": { "label": "Redis" },
"443": { "label": "Caddy HTTPS" }
},
"customizations": {
"vscode": {
"extensions": [
"anthropic.claude-code",
"nrwl.angular-console",
"angular.ng-template",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"vitest.explorer",
"firsttris.vscode-jest-runner",
"ms-azuretools.vscode-docker"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
}
},
"postCreateCommand": "pnpm install && pnpm approve-builds || true",
"remoteUser": "node",
"mounts": [
"source=${localEnv:HOME}/.gitconfig,target=/home/node/.gitconfig,type=bind,consistency=cached"
]
}

4
.gitignore vendored
View File

@@ -47,4 +47,6 @@ Thumbs.db
__screenshots__/
vitest.config.*.timestamp*
vitest.config.*.timestamp*
.nx/polygraph
.pnpm-store/

28
.mcp.json Normal file
View File

@@ -0,0 +1,28 @@
{
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-postgres",
"postgresql://fog:fog_dev@host.docker.internal:5432/fog_expedition"
]
},
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@playwright/mcp"]
},
"context7": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"]
},
"nx-mcp": {
"type": "stdio",
"command": "pnpm",
"args": ["exec", "nx", "mcp"]
}
}
}

27
AGENTS.md Normal file
View File

@@ -0,0 +1,27 @@
<!-- nx configuration start-->
<!-- Leave the start & end comments to automatically receive updates. -->
# General Guidelines for working with Nx
- For navigating/exploring the workspace, invoke the `nx-workspace` skill first - it has patterns for querying projects, targets, and dependencies
- When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through `nx` (i.e. `nx run`, `nx run-many`, `nx affected`) instead of using the underlying tooling directly
- Prefix nx commands with the workspace's package manager (e.g., `pnpm nx build`, `npm exec nx test`) - avoids using globally installed CLI
- You have access to the Nx MCP server and its tools, use them to help the user
- For Nx plugin best practices, check `node_modules/@nx/<plugin>/PLUGIN.md`. Not all plugins have this file - proceed without it if unavailable.
- NEVER guess CLI flags - always check nx_docs or `--help` first when unsure
## Scaffolding & Generators
- For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the `nx-generate` skill FIRST before exploring or calling MCP tools
## When to use nx_docs
- USE for: advanced config options, unfamiliar flags, migration guides, plugin configuration, edge cases
- DON'T USE for: basic generator syntax (`nx g @nx/react:app`), standard commands, things you already know
- The `nx-generate` skill handles generator discovery internally - don't call nx_docs just to look up generator syntax
<!-- nx configuration end-->
# Fog Expedition — project guidance
See PROJECT_CONTEXT.md and the project-specific section in CLAUDE.md. Treat both as authoritative.

62
CLAUDE.md Normal file
View File

@@ -0,0 +1,62 @@
<!-- nx configuration start-->
<!-- Leave the start & end comments to automatically receive updates. -->
# General Guidelines for working with Nx
- For navigating/exploring the workspace, invoke the `nx-workspace` skill first - it has patterns for querying projects, targets, and dependencies
- When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through `nx` (i.e. `nx run`, `nx run-many`, `nx affected`) instead of using the underlying tooling directly
- Prefix nx commands with the workspace's package manager (e.g., `pnpm nx build`, `npm exec nx test`) - avoids using globally installed CLI
- You have access to the Nx MCP server and its tools, use them to help the user
- For Nx plugin best practices, check `node_modules/@nx/<plugin>/PLUGIN.md`. Not all plugins have this file - proceed without it if unavailable.
- NEVER guess CLI flags - always check nx_docs or `--help` first when unsure
## Scaffolding & Generators
- For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the `nx-generate` skill FIRST before exploring or calling MCP tools
## When to use nx_docs
- USE for: advanced config options, unfamiliar flags, migration guides, plugin configuration, edge cases
- DON'T USE for: basic generator syntax (`nx g @nx/react:app`), standard commands, things you already know
- The `nx-generate` skill handles generator discovery internally - don't call nx_docs just to look up generator syntax
<!-- nx configuration end-->
# Fog Expedition — project guidance
This is a Twitch Video Overlay extension implementing an autonomous tick-based ZPG. See `PROJECT_CONTEXT.md` at the repo root for full architecture, design direction, and build stages — read it before any non-trivial work.
## Conventions
- Sentence case in UI text and log messages.
- Zod schemas (not just TS types) at the EBS boundary.
- Pure functions for game logic. Seeded PRNG via `seedrandom`, never `Math.random()`. Persist seeds in `mission_logs`.
- Migrations from day one. No "I'll add migrations later."
- Round every displayed number; JS float math leaks artifacts.
- Structured logging with correlation IDs (`missionId`, `tickIndex`, `channelId`, `opaqueUserId`).
- Bind Nest services to `0.0.0.0`, not `127.0.0.1`. The Twitch dev rig on Windows reaches them via the WSL2/devcontainer port forwarding chain.
## Hard rules
- Do not use `Math.random()` anywhere in game logic.
- Do not skip `nextTickAt` jitter — synchronised global ticks will thunder-herd Postgres.
- Do not assume Twitch PubSub message delivery; treat as a hint to refresh, reconcile from REST.
- Do not add inline scripts or inline event handlers to the overlay HTML — Twitch CSP forbids them.
- Do not reproduce Dead by Daylight assets, character names, or trademarked terms in encounter content. The aesthetic is *inspired by* the genre, not a clone.
## Twitch extension specifics
- JWTs use HS256 with the base64-decoded shared secret. Not RS256.
- `opaque_user_id` starts with `U` (logged-in viewer) or `A` (anonymous). Anonymous viewers cannot have persistent survivors — read-only view only.
- PubSub limit: 1 message/sec per channel, 5KB max payload, no delivery guarantees. Batch all per-channel updates per tick into one message.
## Quality bar
- Property-based tests (`fast-check`) for pure functions, especially the encounter resolver.
- Monte Carlo balance snapshot test in CI for the resolver.
- Type-check and lint must pass before declaring work complete.
- No commented-out code. No `TODO` without a tracked issue reference.
## When stuck
If a decision isn't covered by `PROJECT_CONTEXT.md` and isn't trivially derivable from convention, surface it as a question rather than guessing.

279
PROJECT_CONTEXT.md Normal file
View File

@@ -0,0 +1,279 @@
# Fog Expedition — Project Context
A Twitch extension that runs an autonomous, tick-based ZPG (zero-player game) inspired by Progress Quest, themed around a Dead by Daylightstyle survival fiction. Viewers watch (and lightly direct) a survivor venturing into "the fog" while a streamer plays.
This document is the canonical context for the project. Read it before starting any non-trivial work. AI assistants (Claude Code, Cursor, etc.) should treat this as authoritative — if something here conflicts with assumptions in your training data, this document wins.
---
## 1. Product summary
- **What it is:** A Twitch Video Overlay extension. A small "lantern" sits on the streamer's broadcast; viewers see a survivor's mission unfold as a live log of encounters resolved every 60 seconds.
- **Why it exists:** Ambient stream entertainment. The streamer plays their game; the extension layers a parallel narrative driven by viewer participation without competing for attention.
- **Core loop:** Viewer's survivor enters a mission → server resolves an encounter every 60s using survivor stats + perks + RNG → log line streams to all viewers' overlays → mission ends in success/injury/sacrifice → progression updates persist.
- **Multiplayer:** "SWF" (Survive With Friends) groups of 24 survivors run shared missions with team perks and synchronized outcomes.
---
## 2. Architecture overview
```
Twitch Video Overlay (panel) NestJS API (EBS)
├── Lantern + ticker (minimised) ├── /missions/* REST endpoints
├── Ambient event card ├── Twitch JWT auth guard
└── Expanded survivor + log card ├── Tick engine (60s heartbeat)
├── Encounter resolver (pure)
├── Group synergy service
└── PubSub publisher
│ │
└────────── HTTPS + JWT ────────────────┘
┌─────────────┴─────────────┐
│ │
Postgres Redis
(durable state: (active missions,
users, survivors, lobbies, tick locks,
missions, logs) rate limiting)
Twitch Extension PubSub
(server → viewer push)
```
### Stack
- **Monorepo:** Nx workspace, pnpm package manager.
- **Backend:** NestJS (Node 20+, TypeScript). Modules: Auth, Missions, TickEngine, PubSub.
- **Frontend (overlay):** **Reconsider the original Angular choice** — the overlay UI is a single component with three states. Vanilla TypeScript + Lit (or hand-rolled) is likely a better fit. Smaller bundle = easier Twitch review. Decide before starting Stage 2.
- **Datastores:** Postgres (durable), Redis (ephemeral mission state, locks, lobbies).
- **Infra:** Docker Compose for local dev. Devcontainer for editor environment.
- **Shared types:** `@fog-explorer/api-interfaces` library, consumed by both apps. Use Zod schemas, not just TS types — runtime validation matters at the EBS boundary.
---
## 3. Twitch-specific constraints (read carefully — these shape architecture)
### Extension type
This is a **Video Overlay extension**, not a Panel extension. Configure the manifest accordingly. Streamers configure position; default to bottom-left.
### JWT and auth
- Twitch extension JWTs use **HS256** with the base64-decoded shared secret. Not RS256.
- Roles in the JWT: `viewer`, `broadcaster`, `external` (server-to-server). Treat them differently. Reject `broadcaster` for write actions unless explicitly streamer-only.
- `opaque_user_id` starts with `U` for logged-in viewers, `A` for anonymous. Anonymous viewers cannot have persistent survivors — they get a read-only view of the active mission.
### PubSub
- **1 message/sec per channel** for broadcaster messages. **Max 5KB payload.** **No delivery guarantees.**
- Treat PubSub as a "hint to refresh" rather than authoritative state. Viewers who load mid-tick must be able to fetch state via REST.
- Batch all updates for a channel into a single PubSub message per tick.
- Frontend store must reconcile, not just append.
### HTTPS requirement
The EBS must serve HTTPS, even locally. Use Caddy or mkcert + Node TLS for dev.
### Review process
- Twitch reviews every version manually. **13 week cycles.** Plan for this; build for the review checklist from day one.
- CSP rules: no inline scripts, explicit allowlist for the EBS domain.
- No fingerprinting. GDPR-compliant handling of obfuscated user IDs.
- Working logged-out viewer experience is mandatory.
- Overlays must not obscure meaningful gameplay pixels. Streamer must have a kill switch.
---
## 4. Design direction
### Aesthetic
"Field journal in the fog." Muted desaturated palette, single warm accent (lantern amber `#E8A547`), cold red for danger (`#C03A3A`). Flat, printed, slightly weathered. No gradients, no drop shadows.
### Typography
- Survivor names: condensed serif or slab (e.g. Cormorant). Grim-tarot feel.
- Live log: monospace (e.g. JetBrains Mono, IBM Plex Mono). Reinforces "transmission from the fog."
- UI chrome: clean sans.
### Three overlay states
**1. Minimised (default, ~95% of viewing time) — ~290×56px**
- Circular lantern badge (~48px), state-coloured ring: amber=active, ochre=injured, red=sacrificed.
- Adjacent ticker showing the **single latest log line** in monospace, ellipsis truncation, no scrolling feed.
- Background: `rgba(15, 18, 22, 0.88)` — opacity ≥80% is non-negotiable for legibility over arbitrary stream content.
**2. Ambient event (~290×92px, ~4s auto-dismiss)**
- Triggers only on significant events: injury, sacrifice, mission complete, perk acquired. **Not every tick.**
- Slides up from the lantern with the event glyph and a one-line description.
- Use sparingly — overuse drives streamers to disable.
**3. Expanded (lantern click) — ~320×440px**
- Survivor card (portrait, name, state, perk slots).
- Mission strip (name, difficulty as 13 fog-glyphs, T+N tick counter, draining progress line).
- Live log with progressive opacity (older lines fade).
- Single primary action button at the bottom when relevant; hidden when no action available.
- Backdrop dim/blur behind it to read against any stream content.
### Motion budget
Almost zero. Slow tick-drain animation, fade-in per log line, brief pulse on state changes, summon/unfold for the expanded card. Nothing else. Twitch viewers mute or hide animated panels.
### Accessibility
- Never encode state in colour alone. Vertical bar on survivor card changes shape (solid/dashed/crosshatched) per state. Log line colours are backed by glyphs.
- All custom monochrome glyphs (no emoji). Three glyphs minimum: difficulty pip, injury wound, perk-triggered.
### Group/SWF mode
Expanded card collapses the survivor section to a horizontal row of 4 portraits with state-coloured borders. The currently-resolving survivor highlights when their log line appears. Tap a portrait to expand detail. **Not yet mocked — design before Stage 3 group logic.**
### Streamer config screen
Separate from the viewer overlay. Live preview of the lantern in all four corner positions, one-click positioning, kill switch. Streamers who feel in control keep extensions installed.
### Empty/onboarding states
- **First-time viewer:** moody intro card, fog-shrouded silhouette, "The fog stirs. Awaiting a survivor." + Begin button. Only chance to explain what the extension is before they bounce.
- **Anonymous viewer:** desaturated lantern, no ticker, click to learn more. Don't hide the panel.
---
## 5. Build stages
### Stage 1 — Workspace & infrastructure
- Initialise Nx workspace with TypeScript, `@nx/angular` (or `@nx/js` if dropping Angular), `@nx/nest`, `@nx/node`.
- Generate apps: overlay frontend, NestJS API.
- Generate `@fog-explorer/api-interfaces` lib with Zod schemas for `Survivor`, `SurvivorState`, `Mission`, `MissionState`, `EncounterResult`, `Perk`, `PerkModifier`, `TeamPerk`.
- Configure `@nx/enforce-module-boundaries` with project tags (`scope:shared`, `scope:api`, `scope:overlay`). AI assistants love to reach across libraries with relative imports — enforce early.
- Root `docker-compose.yml`: Postgres (named `fog_expedition`, healthcheck), Redis, optional API service.
- `.devcontainer/` directory with config (see Section 7).
### Stage 2 — Overlay frontend
- Decide framework first (Angular vs Lit vs vanilla). Smaller is better for review.
- `PanelShellComponent` mounting only after `window.Twitch.ext.onAuthorized` fires.
- `LiveLogComponent`, `SurvivorStatusComponent`, lantern + ticker components for the three states.
- `TwitchAuthService`: wraps `onAuthorized`, `onContext`, `onVisibilityChanged`. Stores JWT and parsed payload.
- `EbsApiService`: attaches `Authorization: Bearer <jwt>` to all calls. Uses `@fog-explorer/api-interfaces` types.
- `MissionStateStore`: signals or BehaviorSubject. Pulls initial state from REST, subscribes to PubSub for deltas.
- **Visibility lifecycle:** when overlay is hidden via `onVisibilityChanged`, stop PubSub processing and polling, but retain local state for instant re-expand.
- **Ambient event queue:** if events fire while collapsed, drop them and show current state on expand. Don't replay old drama.
### Stage 3 — Mission engine
- `TickEngineModule` with `TickService` using `@nestjs/schedule`. Global heartbeat per server, but **jitter `nextTickAt` per mission** to avoid thundering herd. Each mission has its own due time; worker picks up due ones.
- Distributed lock pattern: `SET key value NX PX <ttl>` with unique token, verify token before release. Or use Redlock. Naive `SETNX` without TTL = stuck tick on crash.
- **Encounter resolver — pure function** in `libs/mission-logic`. Signature `resolveEncounter(survivor, perks, difficulty, seed) → EncounterResult`. Use seeded PRNG (`seedrandom`), not `Math.random()`. Store seed per tick in `mission_logs` for replay/debug.
- `EncounterService` calls the resolver, updates state, emits log events.
- `GroupSynergyService` aggregates team perks for SWF missions, applies to all relevant rolls.
- `MissionStore` repository abstracting Redis. Keys: `active_mission:{id}`, `mission_lobby:{id}`.
### Stage 4 — EBS persistence
- `MissionsController`: `POST /missions/start`, `GET /missions/state`, `POST /missions/choose-perk`. DTOs from `@fog-explorer/api-interfaces`.
- `TwitchAuthGuard`: HS256 JWT verification, attach `opaqueUserId` and `channelId` to request. Reject expired or wrong-role tokens.
- Postgres schema (TypeORM or Prisma — pick one and stick to it):
- `users` (id, twitch_opaque_user_id, created_at)
- `survivors` (id, user_id FK, stats JSONB, perk_slots JSONB, state, created_at)
- `missions` (id, group_id nullable, difficulty, status, started_at, ended_at)
- `mission_logs` (id, mission_id FK, tick_index, encounter_key, rendered_text, seed, modifiers_applied JSONB)
- **Migrations from day one.** No "I'll add migrations later."
- `TwitchPubSubService`: signs messages with extension client ID + secret, publishes per-channel updates per tick.
- **Rate limit `/missions/state`** keyed off JWT `user_id`. A buggy panel could hammer it.
### Stage 5 — Content library & balance
- `@fog-explorer/encounter-library` with `encounters.json`. Records keyed by ID (`cleanse_hex`, `escape_hatch`, etc.) with: base success chance, difficulty tags, perk tags, **flavor text variants** (target 200+ per encounter type — this is the editorial product).
- TS wrapper: `getEncounterById`, `getRandomEncounterByTier`, `pickFlavor(encounter, context)`.
- **Version the encounter content.** In-flight missions use the version they started with; don't hot-swap.
- `PerkMath` helper: aggregates additive/multiplicative modifiers, separates survivor-level from team-level. Clamp `P{success}` to sane bounds.
- **Property-based tests** with `fast-check` on the resolver. Example tests pass while edge cases (modifier overflow, negative probabilities, empty perks) silently break.
- **Monte Carlo balance harness** in CI. Simulate 10k missions across perk loadouts. Snapshot test on success/injury/sacrifice distributions. Catches AI-generated balance numbers that *feel* reasonable but produce degenerate gameplay.
---
## 6. Cross-cutting concerns
### Observability
- Structured logging (pino) with correlation IDs: `missionId`, `tickIndex`, `channelId`, `opaqueUserId`. From day one.
- OpenTelemetry on the Nest side. Cheap to add upfront, painful later.
- Tag every log line emitted by the encounter resolver with the seed used. Debugging mission desyncs without this is misery.
### Data retention
- `mission_logs` will grow fast. Plan archival: partition by month, or TTL completed missions older than N days.
### Local dev without Twitch
- Add a `NODE_ENV=development` flag where the overlay mints a fake JWT and the API accepts it. 10× faster iteration than rigging the Twitch local test harness for every change. **Strictly dev-only — assert and fail loudly if seen in prod.**
### Monetisation seam
- Bits/transaction JWTs use a different flow. Even if not implementing now, stub the seam. Future Bits-driven mission events will be a small addition rather than a refactor.
---
## 7. Dev environment
### Host setup
- **Windows + WSL2 + devcontainer.** No dual-boot needed. Stack has no kernel/GPU dependencies; bare-metal Linux gives no measurable benefit and adds context-switch friction.
- **Code lives in WSL2 filesystem** (`~/code/fog-expedition`), not on `/mnt/c/`. The 9P boundary is 1020× slower for filesystem ops; Nx with thousands of `node_modules` files will feel sluggish if mounted from Windows.
- VS Code with Dev Containers extension. Open the repo via Remote-WSL, then "Reopen in Container."
### Devcontainer config
- `.devcontainer/devcontainer.json` references the root `docker-compose.yml` plus a `docker-compose.dev.yml` overlay that adds the workspace service.
- Features: `docker-outside-of-docker` (lets the container run Docker commands against the host daemon — better than docker-in-docker), `github-cli`.
- `forwardPorts`: 3000 (API), 4200 (overlay dev server), 5432 (Postgres), 6379 (Redis).
- `postCreateCommand: pnpm install`.
- VS Code extensions to preinstall: Nx Console, ESLint, Prettier, Jest Runner.
### Twitch dev rig integration
- The Twitch dev rig is an Electron app on the Windows side. It needs to reach the API.
- Bind Nest to `0.0.0.0`, not `127.0.0.1`. WSL2's localhost forwarding handles WSL2 → Windows; `forwardPorts` handles container → WSL2.
- HTTPS for EBS even locally. Add a `caddy` service to dev compose for TLS termination — three lines of config, avoids fiddling with Node TLS.
### File watching
- Nx file watchers use inotify. Inotify is unreliable on `/mnt/c/`. Inside WSL2 / devcontainer Linux filesystem, it works fine. This is another reason to keep code off the Windows drive.
---
## 8. AI-assisted build conventions
This project is being built with heavy AI assistance. A few conventions to keep things sane:
- **Lock the contracts before generating implementations.** Finish `@fog-explorer/api-interfaces` (Zod schemas, not just TS types) before generating controllers/services. AI is great at filling in implementations against tight contracts, terrible at maintaining consistency when types drift.
- **OpenAPI generation from Nest decorators.** Keeps the overlay client in sync automatically. Worth setting up Stage 1.
- **Property-based tests on pure functions.** Especially the encounter resolver. AI-generated example tests pass while edge cases break.
- **Monte Carlo balance harness as a snapshot test.** AI invents plausible-looking numbers that produce degenerate gameplay. Catch it in CI.
- **Module boundary enforcement.** `@nx/enforce-module-boundaries` with project tags. AI loves cross-library relative imports.
- **No hidden state in conversations.** Anything decided in chat that affects the codebase goes into a doc — this one, an ADR, or a code comment. Future-you and future-AI both need it.
---
## 9. Open decisions
Things deliberately not decided yet — flag them when you reach them:
- **Overlay framework:** Angular (per original plan) vs Lit vs vanilla TS. Decide before Stage 2 starts.
- **ORM:** TypeORM vs Prisma vs Drizzle. Pick one before any DB code is written.
- **Group/SWF expanded UI:** designed conceptually, not mocked.
- **Streamer config screen:** designed conceptually, not mocked.
- **Custom glyph set:** placeholder Tabler icons used in mocks; commission three monochrome glyphs (difficulty pip, injury wound, perk-triggered) before submitting for review.
- **Hosted EBS domain:** required for Twitch review. Decide hosting (Fly.io, Railway, AWS, etc.) before Stage 4.
---
## 10. Out of scope (for v1)
- Bits-driven mission events (stub the seam, don't build).
- Multiple survivors per user (one survivor per user, who replaces them on death).
- Cross-channel persistence (survivors are scoped to the channel they were created on, for now).
- Mobile app (the Twitch mobile app renders extensions, but design-validate at desktop first).
- Streamer-customisable encounter content (single canonical content library for v1).

51
TOOLCHAIN.md Normal file
View File

@@ -0,0 +1,51 @@
# Toolchain
Pinned versions and setup steps for this workspace. If you're on a new machine, install in this order.
## System
- WSL2 Ubuntu 24.04 (Noble) on Windows 11
- Docker Desktop with WSL2 integration enabled
## Runtime
- Node.js 24.x (via nvm)
- pnpm 10.x (`npm install -g pnpm@latest`)
## Workspace
- Nx 22.7.x
- Angular 21.2.x
- NestJS 11.1.x
- TypeScript 5.9.x
## Test runners
- API: Jest 30
- Overlay: Vitest 4 via `vitest-angular` (Angular's official Vitest integration)
- Libraries: Vitest 4
- E2E: Playwright 1.59 (chromium only)
## Local services
Run via `docker compose up -d postgres redis`:
- Postgres 16-alpine (port 5432, db `fog_expedition`, user `fog`, password `fog_dev`)
- Redis 7-alpine (port 6379)
- Caddy 2 (port 443, HTTPS reverse proxy to host:3000)
## Bootstrap on a new machine
```bash
nvm install 24
nvm use 24
npm install -g pnpm@latest
git clone http://pihole.home:3000/maurycy/fog.git ~/code/fog-expedition
cd ~/code/fog-expedition
pnpm install
pnpm approve-builds # select all
pnpm install
pnpm exec playwright install --with-deps chromium
docker compose up -d postgres redis
pnpm exec nx run-many --target=test --all --watch=false
```

View File

@@ -9,3 +9,13 @@ allowBuilds:
nx: true
unrs-resolver: true
autoInstallPeers: true
onlyBuiltDependencies:
- '@nestjs/core'
- '@parcel/watcher'
- '@swc/core'
- esbuild
- less
- lmdb
- msgpackr-extract
- nx
- unrs-resolver