diff --git a/.agents/README.md b/.agents/README.md new file mode 100644 index 000000000..f1b39845e --- /dev/null +++ b/.agents/README.md @@ -0,0 +1,57 @@ +# Workspace agents (`adt-cli`) + +Team-visible standards for AI-assisted work live here. **`.agents/` is the single source of truth** for rules, skills, workflows, and ADT command docs. Tool-specific folders ([`.cursor/`](../.cursor/README.md), legacy [`.windsurf/`](../.windsurf/README.md)) stay thin so Cursor/Windsurf only add discovery glue (commands, optional stubs). + +## Repository conventions + +Monorepo layout, MCP↔CLI coupling, rules index, and package-level guides: +[`repo-guide.md`](repo-guide.md). + +## Layout + +``` +.agents/ + README.md # You are here + repo-guide.md # Monorepo + architecture reference for agents + agents/ # Multi-agent manifest + per-runtime notes + rules/ # Conditional rules (“when X, do Y”) + skills/ # Skills (SKILL.md trees; shared across agents) + workflows/ # Multi-step procedures (OpenSpec, lint, …) + commands/adt/ # Long-form ADT workflows (schema, contract, …) +``` + +## Rules (`.agents/rules/`) + +Use imperative, scenario-based wording. Agents load these when the situation matches. + +## Skills (`.agents/skills/`) + +Documentation for the model: prerequisites, commands to run, troubleshooting. Install into a native agent cache with [`npx skills add`](https://github.com/vercel-labs/skills) when needed; the repo copy remains authoritative. + +## Workflows (`.agents/workflows/`) + +Reusable procedures (OpenSpec opsx-\*, lint). **Cursor slash commands** under `.cursor/commands/` delegate here—edit the workflow file, not the Cursor stub. + +ADT specifics: see [`.agents/workflows/adt/README.md`](workflows/adt/README.md) → [`.agents/commands/adt/`](commands/adt/). + +## OpenSpec & planning + +- `openspec/specs/` — specifications +- `openspec/changes/` — active changes +- `openspec/config.yaml` — project config +- `docs/planning/` — coordination docs + +## Memory: internal vs repo + +- **Internal** (vendor-specific persistence): preferences and session DB—not committed. +- **Workspace** (this tree + `openspec/`): versioned, reviewable, shared. + +Prefer repo-backed docs for anything the team must agree on. + +## Migrating from Windsurf + +Legacy `.windsurf/workflows/` and `.windsurf/skills/` now **redirect** to `.agents/`. Remove Windsurf-only duplicates from your mental model; extend `.agents/` instead. + +## Multi-agent setup + +See [`agents/README.md`](agents/README.md) for registered runtimes (Cursor IDE, Claude Code, Windsurf stubs), ownership, and delegation hints. diff --git a/.agents/agents/README.md b/.agents/agents/README.md new file mode 100644 index 000000000..1a69504d4 --- /dev/null +++ b/.agents/agents/README.md @@ -0,0 +1,33 @@ +# Multi-agent manifest (`adt-cli`) + +This repo is edited by **several AI runtimes**. They share one canonical tree—**`.agents/`**—and keep native config thin. + +## Principles + +1. **Single source of truth**: Rules, skills, and workflow bodies live under `.agents/`. +2. **Thin native layers**: `.cursor/` carries Cursor-only discovery (slash commands → workflows). Legacy `.windsurf/` remains as stubs only. +3. **No duplicated procedure text**: If you change an OpenSpec flow, edit `.agents/workflows/opsx-*.md`. + +## Registered agents + +| Agent | Native config | Consumes from repo | +| ----------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| **Cursor (IDE)** | `.cursor/commands/`, `.cursor/rules/`, `.cursor/skills/` (stubs) | `.agents/rules/`, `.agents/skills/`, `.agents/workflows/`, `.agents/commands/` | +| **Claude Code / Codex** | `.claude/` ([README](../../.claude/README.md)) — thin commands/skills + hooks/settings | Same `.agents/` paths; sync skills with `npx skills add` when needed | +| **Windsurf Cascade** | `.windsurf/` (**deprecated stubs**) | Redirects to `.agents/` | + +Details: [`manifest.yaml`](manifest.yaml). + +## Delegation + +- **Specs & planning**: OpenSpec workflows in `.agents/workflows/opsx-*.md`. +- **Nx / monorepo**: Skills `nx-workspace`, `nx-run-tasks`, `nx-generate`, … under `.agents/skills/`. +- **ADT / SAP**: Commands under `.agents/commands/adt/` plus skills `add-endpoint`, `adt-export`, etc. + +When spawning subagents or parallel tasks, pass **paths under `.agents/`** so every runtime reads the same instructions. + +## Adding a new runtime + +1. Document it in `manifest.yaml`. +2. Point its skill/rule discovery at `.agents/` (symlink, CLI sync, or copy-on-install). +3. Add a short profile file here (e.g. `my-runtime.md`) describing quirks and env vars—keep vendor secrets out of git. diff --git a/.agents/agents/claude-code.md b/.agents/agents/claude-code.md new file mode 100644 index 000000000..d50094158 --- /dev/null +++ b/.agents/agents/claude-code.md @@ -0,0 +1,15 @@ +# Claude Code profile (`adt-cli`) + +## Role + +CLI / IDE agent using `.claude/` for hooks, settings, and native slash commands. + +## Where to read first + +Same as other runtimes: `.agents/rules/`, `.agents/skills/`, `.agents/workflows/`, and `commands/opsx/*.md` stubs that delegate to `.agents/workflows/opsx-*.md`. + +## Claude-only files + +Keep secrets scanners, git hooks, and `settings*.json` here—they are not portable to Cursor/Windsurf. + +See also: `../../.claude/README.md`. diff --git a/.agents/agents/cursor-ide.md b/.agents/agents/cursor-ide.md new file mode 100644 index 000000000..701219383 --- /dev/null +++ b/.agents/agents/cursor-ide.md @@ -0,0 +1,21 @@ +# Cursor IDE profile (`adt-cli`) + +## Role + +Primary interactive editor agent with repo access, terminal, and MCP. + +## Where to read first + +1. Repo root `AGENTS.md` (Nx expectations). +2. `.agents/rules/` for conditional standards. +3. `.agents/workflows/` when the user runs `/opsx:*` or asks for a procedural flow. + +## Cursor-only files + +- `.cursor/commands/*.md` — **Stub only**: frontmatter for the `/` palette + pointer to `.agents/workflows/`. +- `.cursor/rules/*.mdc` — Always-on or scoped hints; keep them short. +- `.cursor/skills/openspec-*/SKILL.md` — **Stub only**: discovery wrapper; canonical skill body is `.agents/skills/openspec-*`. + +Do not grow duplicate procedure text in `.cursor/`—extend `.agents/` instead. + +There is **no** `/models` command in this repo. If you still see one that hits Anthropic’s API, delete it from Cursor (`/commands`) or from `~/.cursor/commands/models.md` / `.cursor/commands/models.md`. diff --git a/.agents/agents/manifest.yaml b/.agents/agents/manifest.yaml new file mode 100644 index 000000000..c46e47819 --- /dev/null +++ b/.agents/agents/manifest.yaml @@ -0,0 +1,29 @@ +# Multi-agent registry for adt-cli (documentation contract; not loaded by tooling yet). +version: 1 +canonical_roots: + rules: .agents/rules/ + skills: .agents/skills/ + workflows: .agents/workflows/ + adt_commands: .agents/commands/ + +agents: + - id: cursor-ide + description: Cursor IDE composer/agent + native_config_root: .cursor/ + thin_layer_readme: .cursor/README.md + entrypoints: + slash_commands: .cursor/commands/ + optional_rules: .cursor/rules/ + skill_stubs: .cursor/skills/ + + - id: claude-code + description: Claude Code / CLI agents using local .claude/ + native_config_root: .claude/ + thin_layer_readme: .claude/README.md + notes: Slash commands and openspec skill stubs delegate to .agents/; hooks/settings stay in .claude/. + + - id: windsurf-cascade + description: Windsurf Cascade (legacy) + native_config_root: .windsurf/ + deprecated: true + migration: Prefer .agents/; .windsurf retains redirect stubs only. diff --git a/.agents/commands/adt/adk.md b/.agents/commands/adt/adk.md new file mode 100644 index 000000000..95cf15f6f --- /dev/null +++ b/.agents/commands/adt/adk.md @@ -0,0 +1,434 @@ +# ADT-ADK Object Type Implementation Workflow + +Comprehensive workflow for implementing a new ADK object type with full stack support. + +## Usage + +```bash +/adt-adk +``` + +**Example:** `/adt-adk TABL` (implement table object type) + +## Prerequisites + +- Understanding of SAP ADT REST API for the object type +- Access to SAP system for endpoint discovery and testing +- Familiarity with abapify package structure + +## Workflow Steps + +### Step 1: Understand Object Type Semantics + +**Goal:** Fully understand the ABAP object type before implementation. + +**Actions:** + +1. Research the object type in SAP documentation +2. Identify ADT endpoint(s) for the object type: + - Discovery: `GET /sap/bc/adt/discovery` - find available endpoints + - Object URI pattern: `/sap/bc/adt/{area}/{object_name}` +3. Document object semantics: + - Object lifecycle (create, read, update, delete, activate) + - Object relationships (parent package, dependencies, includes) + - Object-specific actions (release, check, transport) +4. Identify if it's a repository object (transportable) or runtime object + +**Output:** Object type analysis document with: + +- ADT endpoint(s) +- Supported operations (CRUD + actions) +- Object relationships +- Transport behavior + +### Step 2: Create/Update ADT Schema + +**Invoke:** `/adt-schema ` + +**Goal:** Type-safe XML parsing for the object type. + +**Actions:** + +1. Capture sample XML responses from SAP: + - GET single object + - GET list/collection + - POST create request/response + - PUT update request/response +2. Check if XSD exists in SAP SDK or create manual schema +3. Generate schema using ts-xsd: + ```bash + npx nx run adt-schemas-xsd:generate + ``` +4. Export schema from `adt-schemas-xsd/src/schemas/index.ts` +5. **MANDATORY:** Create test scenario in `adt-schemas-xsd/tests/scenarios/` + +**Output:** + +- Schema in `adt-schemas-xsd/src/schemas/generated/` or `manual/` +- Test scenario with real SAP XML fixture +- Exported from index.ts + +### Step 3: Create/Update ADT Contract + +**Invoke:** `/adt-contract ` + +**Goal:** Type-safe API contract definition. + +**Actions:** + +1. Create contract in `adt-contracts/src/adt/{area}/`: + + ```typescript + import { contract } from '../../base'; + import { mySchema } from '@abapify/adt-schemas-xsd'; + + export const myObject = { + get: (name: string) => + contract({ + method: 'GET', + path: `/sap/bc/adt/{area}/${name}`, + headers: { Accept: 'application/xml' }, + responses: { 200: mySchema }, + }), + // ... other operations + }; + ``` + +2. Export from area index and main index +3. **MANDATORY:** Create test scenario in `adt-contracts/tests/contracts/` + +**Output:** + +- Contract in `adt-contracts/src/adt/{area}/` +- Test scenario with contract validation +- Exported from index.ts + +### Step 4: Create/Update Client Service (if needed) + +**Goal:** Business logic layer for complex operations. + +**When needed:** + +- Multi-step operations (create + activate + transport) +- Complex error handling +- Caching or optimization +- Cross-object coordination + +**Actions:** + +1. Create service in `adt-client-v2/src/services/{area}/`: + + ```typescript + export class MyObjectService { + constructor(private client: AdtClient) {} + + async get(name: string): Promise { + const contract = myObjectContract.get(name); + return this.client.execute(contract); + } + // ... other methods + } + ``` + +2. Register service in client factory +3. Add tests for service methods + +**Output:** + +- Service class in `adt-client-v2/src/services/` +- Service registration in client +- Unit tests + +### Step 5: Implement ADK Object Model + +**Goal:** High-level object abstraction with lazy loading and cross-references. + +**Location:** `adk-v2/src/objects/{category}/{object_type}/` + +**Files to create:** + +1. `{type}.model.ts` - Main ADK object class +2. `{type}.types.ts` - TypeScript interfaces +3. `index.ts` - Exports + +**⚠️ CRITICAL: Use Response Types from adt-client-v2** + +**NEVER** create manual interfaces for API response types. **ALWAYS** import the response type from `@abapify/adt-client-v2`: + +```typescript +// ❌ WRONG - Manual interface duplicates schema definition +export interface MyObjectXml { + name: string; + description?: string; + // ... manually typed fields that will drift from contract +} + +// ❌ WRONG - Importing directly from schema (bypasses contract) +import type { InferXsd } from 'ts-xsd'; +import { mySchema } from 'adt-schemas-xsd'; +export type MyObjectData = InferXsd; + +// ❌ WRONG - Using speci internals (implementation detail) +import type { InferSuccessResponse } from 'speci/rest'; +export type MyObjectData = InferSuccessResponse<...>; + +// ❌ WRONG - Importing directly from adt-contracts (adds extra dependency) +import type { MyObjectResponse } from 'adt-contracts'; +export type MyObjectData = MyObjectResponse; + +// ✅ CORRECT - Import from adt-client-v2 (re-exports contract types) +import type { MyObjectResponse } from '@abapify/adt-client-v2'; +export type MyObjectData = MyObjectResponse; +``` + +**Why adt-client-v2 is the import source:** + +- **Single dependency:** ADK only depends on `adt-client-v2`, not `adt-contracts` directly +- **Clean layering:** `adt-client-v2` re-exports contract types for consumers +- **Contract is still source of truth:** Types originate from contract, but flow through client +- **Simpler dependency graph:** ADK → client → contracts → schemas + +**Dependency Architecture:** + +``` +adt-schemas-xsd (schema definitions) + ↓ +adt-contracts (API contracts, exports response types) + ↓ +adt-client-v2 (HTTP client, RE-EXPORTS contract types) + ↓ +adk-v2 (imports types from client only) +``` + +**Pattern:** + +```typescript +// {type}.model.ts +import { AdkObject } from '../../../base/model'; +import { MyObjectKind } from '../../../base/kinds'; +import type { MyObjectResponse } from '@abapify/adt-client-v2'; + +// Type alias for clarity (optional but recommended) +export type MyObjectData = MyObjectResponse; + +export class AdkMyObject extends AdkObject { + readonly kind = MyObjectKind; + + get objectUri(): string { + return `/sap/bc/adt/{area}/${encodeURIComponent(this.name)}`; + } + + // Properties from schema + get description(): string { + return this.dataSync.description; + } + + // Lazy-loaded relationships + async getRelated(): Promise { + return this.lazy('related', async () => { + // Load via service + }); + } + + // CRUD operations + async load(): Promise { + const data = await this.ctx.services.myObject.get(this.name); + this.setData(data); + return this; + } + + // Actions + async activate(): Promise { + /* ... */ + } +} +``` + +**Cross-object references:** + +- Use `AdkContext` for accessing other object types +- Implement lazy loading for relationships +- Use `lazy()` helper for caching + +**Output:** + +- ADK object model with full type safety +- Lazy-loaded relationships +- CRUD + action methods +- Exported from objects index + +### Step 6: Implement CLI Commands + +**Invoke:** `/adt-command ` + +**Goal:** Full CRUD cycle + actions via command line. + +See `/adt-command` workflow for detailed implementation guide. + +**Quick summary:** + +- Location: `adt-cli/src/lib/commands/{area}/` +- Uses `getAdtClientV2()` for client +- Uses router + pages for display +- Full CRUD: get, list, create, update, delete + actions + +### Step 7: Implement CLI Display Pages + +**Invoke:** `/adt-page ` + +**Goal:** Rich terminal display for object information. + +See `/adt-page` workflow for detailed implementation guide. + +**Quick summary:** + +- Location: `adt-cli/src/lib/ui/pages/{type}.ts` +- Self-registering via `definePage()` +- Uses ADK for data fetching +- Component-based rendering + +### Step 8: Implement TUI Editor (for editable objects) + +**Goal:** Interactive terminal UI for object editing. + +**Location:** `adt-tui/src/pages/{area}/` + +**Pattern:** + +```typescript +// pages/{area}/{type}-editor.tsx +import { useState } from 'react'; +import { Box, Text, TextInput } from 'ink'; +import { useNavigation } from '../../lib/context'; + +export function MyObjectEditor({ obj }: { obj: AdkMyObject }) { + const [description, setDescription] = useState(obj.description); + const { navigate } = useNavigation(); + + const save = async () => { + await obj.update({ description }); + navigate('back'); + }; + + return ( + + Edit {obj.name} + + {/* More fields */} + + ); +} +``` + +**Integration:** + +- Register page in TUI routes +- Connect to ADK object for save operations +- Handle validation and errors + +**Output:** + +- TUI editor page +- Form components for object fields +- Save/cancel actions + +### Step 9: Add abapGit Plugin Support (for repository objects) + +**Goal:** Enable Git-based version control for the object type. + +**Location:** `plugins/abapgit/src/objects/{type}/` + +**Files to create:** + +1. `{type}-handler.ts` - Serialization/deserialization +2. `{type}-files.ts` - File mapping + +**Pattern:** + +```typescript +// objects/{type}/{type}-handler.ts +import { ObjectHandler } from '../../lib/handler'; +import { AdkMyObject } from '@abapify/adk-v2'; + +export class MyObjectHandler implements ObjectHandler { + readonly objectType = 'MYOB'; + readonly fileExtension = 'myob'; + + async serialize(obj: AdkMyObject): Promise { + return { + [`${obj.name.toLowerCase()}.${this.fileExtension}.xml`]: + this.buildMetadataXml(obj), + // Additional files (source code, etc.) + }; + } + + async deserialize(files: SerializedFiles): Promise { + // Parse files back to object data + } +} +``` + +**Register handler:** + +```typescript +// objects/index.ts +import { MyObjectHandler } from './{type}/{type}-handler'; +export const handlers = [ + // ... existing + new MyObjectHandler(), +]; +``` + +**Output:** + +- Object handler for serialization +- File mapping for Git storage +- Round-trip tests + +## Checklist + +Before marking complete, verify: + +- [ ] **Schema:** Created with test scenario, parses real SAP XML +- [ ] **Contract:** Created with test scenario, correct method/path/headers +- [ ] **Service:** Created if needed, handles business logic +- [ ] **ADK Model:** Full type safety, lazy loading, CRUD + actions +- [ ] **CLI Commands:** Full CRUD cycle, help text, examples +- [ ] **CLI Pages:** Display pages for view operations +- [ ] **TUI Editor:** Edit pages for modify operations (if applicable) +- [ ] **abapGit:** Handler for Git serialization (if repository object) +- [ ] **Tests:** All components have tests passing +- [ ] **Documentation:** README updated with new object type + +## Package Dependencies + +``` +adt-schemas-xsd (schema definitions) + ↓ +adt-contracts (API contracts, exports response types) + ↓ +adt-client-v2 (HTTP client, RE-EXPORTS contract types for consumers) + ↓ +adk-v2 (model - ONLY depends on adt-client-v2) + ↓ +adt-cli (commands + pages) + ↓ +adt-tui (editor) + ↓ +plugins/abapgit (git support) +``` + +**Key insight:** `adt-client-v2` re-exports types from `adt-contracts` so that consumers like `adk-v2` only need a single dependency. This keeps the dependency graph clean and avoids diamond dependency issues. + +## Related Workflows + +- `/adt-schema` - Schema creation workflow +- `/adt-contract` - Contract creation workflow +- `/adt-command` - CLI command creation workflow +- `/adt-page` - CLI page creation workflow +- `/implement` - General implementation workflow +- `/build` - Execute implementation plan diff --git a/.agents/commands/adt/command.md b/.agents/commands/adt/command.md new file mode 100644 index 000000000..f3a6b18d4 --- /dev/null +++ b/.agents/commands/adt/command.md @@ -0,0 +1,305 @@ +# ADT CLI Command Creation Workflow + +Create CLI commands for ADT object types. + +## Usage + +```bash +/adt-command +``` + +**Example:** `/adt-command transport` + +## Prerequisites + +- ADK model implemented via `/adt-adk` workflow +- Understanding of CLI patterns in `adt-cli` + +## Fundamental Concepts + +### Data Access Preference Hierarchy + +**ALWAYS prefer higher-level abstractions. Use the first applicable option:** + +``` +1. ADK (adk-v2) ← PREFERRED - High-level object facade + │ Full type safety, lazy loading, CRUD+actions + │ Example: AdkTransportRequest.get(ctx, number) + ▼ +2. Client Service ← If ADK doesn't exist yet + │ Business logic layer + │ Example: client.services.transports.get(number) + ▼ +3. Contract ← If service doesn't exist + │ Type-safe API definition + │ Example: client.execute(transportContract.get(number)) + ▼ +4. Raw Fetch ← LAST RESORT - Only if nothing else applies + Direct HTTP call + Example: client.fetch('/sap/bc/adt/cts/...') +``` + +**Why this order?** + +- **ADK** - Encapsulates business logic, relationships, lazy loading +- **Service** - Reusable business logic, error handling +- **Contract** - Type-safe but low-level +- **Fetch** - No type safety, manual parsing + +### Command Architecture + +``` +┌─────────────────────────────────────┐ +│ Commands (get, list, create, etc.) │ +│ - Handles CLI args, auth, progress │ +│ - Uses ADK for object operations │ +└─────────────┬───────────────────────┘ + │ Prefer: ADK > Service > Contract > Fetch + ▼ +┌─────────────────────────────────────┐ +│ ADK Objects (adk-v2) │ +│ - Type-safe object models │ +│ - Lazy loading, CRUD operations │ +└─────────────────────────────────────┘ +``` + +### Command Patterns + +**Full CRUD cycle:** + +- `get ` - Display single object +- `list` - List objects with filters +- `create` - Create new object +- `update ` - Modify object +- `delete ` - Delete object +- `{action} ` - Object-specific actions + +### Client Initialization + +**ALWAYS use shared helper:** + +```typescript +import { getAdtClientV2 } from '../utils/adt-client-v2'; +import { AdkTransportRequest } from '@abapify/adk-v2'; + +// Get client - this also initializes ADK global context automatically +const client = await getAdtClientV2(); + +// Use ADK directly - no context needed! +const transport = await AdkTransportRequest.get(number); +``` + +**Key points:** + +- `getAdtClientV2()` automatically initializes ADK global context +- ADK objects can be used without passing context explicitly +- Context is optional - pass it only for testing or multi-connection scenarios + +**NEVER import v1 directly in commands.** + +## Workflow Steps + +### Step 1: Create Command File + +**Location:** `adt-cli/src/lib/commands/{area}/{type}.ts` + +**Pattern:** + +```typescript +/** + * adt {area} {type} - {Type} commands + * + * Uses ADK ({AdkType}) for operations. + */ + +import { Command } from 'commander'; +import { getAdtClientV2 } from '../../utils/adt-client-v2'; +import { createProgressReporter } from '../../utils/progress-reporter'; +import { createCliLogger } from '../../utils/logger-config'; +import { router } from '../../ui/router'; +// Import to trigger page registration +import '../../ui/pages/{type}'; + +export const {type}Command = new Command('{type}') + .description('Manage {type} objects'); + +// GET subcommand +{type}Command + .command('get ') + .description('Get {type} details') + .option('--json', 'Output as JSON') + .action(async function(this: Command, id: string, options) { + const globalOpts = this.optsWithGlobals?.() ?? {}; + const logger = createCliLogger({ verbose: globalOpts.verbose }); + const progress = createProgressReporter({ compact: !globalOpts.verbose, logger }); + + try { + const client = await getAdtClientV2(); + progress.step(`🔍 Getting {type} ${id}...`); + + // Use router for page-based display + const page = await router.navTo(client, '{TYPE_CODE}', { name: id }); + progress.clear(); + + if (options.json) { + // Raw JSON output + const data = await client.services.{types}.get(id); + console.log(JSON.stringify(data, null, 2)); + } else { + page.print(); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + progress.done(`❌ Failed: ${message}`); + process.exit(1); + } + }); + +// LIST subcommand +{type}Command + .command('list') + .description('List {type} objects') + .option('-p, --package ', 'Filter by package') + .option('-u, --user ', 'Filter by user') + .option('--json', 'Output as JSON') + .action(async function(this: Command, options) { + // ... similar pattern + }); + +// CREATE subcommand (if applicable) +{type}Command + .command('create') + .description('Create new {type}') + .requiredOption('-d, --description ', 'Description') + .option('-p, --package ', 'Target package') + .action(async function(this: Command, options) { + // ... similar pattern + }); + +// Object-specific actions +{type}Command + .command('release ') + .description('Release {type}') + .action(async function(this: Command, id: string) { + // ... similar pattern + }); +``` + +### Step 2: Create Index Export + +**Location:** `adt-cli/src/lib/commands/{area}/index.ts` + +```typescript +import { Command } from 'commander'; +import { {type}Command } from './{type}'; + +export const {area}Command = new Command('{area}') + .description('{Area} commands') + .addCommand({type}Command); +``` + +### Step 3: Register in CLI + +**Location:** `adt-cli/src/lib/cli.ts` + +```typescript +import { {area}Command } from './commands/{area}'; + +program.addCommand({area}Command); +``` + +### Step 4: Add Tests + +**Location:** `adt-cli/src/lib/commands/{area}/{type}.test.ts` + +```typescript +import { describe, it, expect, vi } from 'vitest'; +import { {type}Command } from './{type}'; + +describe('{type} command', () => { + it('should have get subcommand', () => { + const getCmd = {type}Command.commands.find(c => c.name() === 'get'); + expect(getCmd).toBeDefined(); + expect(getCmd?.description()).toContain('details'); + }); + + it('should have list subcommand', () => { + const listCmd = {type}Command.commands.find(c => c.name() === 'list'); + expect(listCmd).toBeDefined(); + }); + + // Test command structure, not execution + // Execution tests require mocking client +}); +``` + +## Output Formatting Guidelines + +### Emoji Indicators + +- 🔄 - Loading/in progress +- 🔍 - Searching +- 📋 - Listing results +- ✅ - Success +- ❌ - Error +- 💡 - Hint/tip +- 💾 - File saved + +### JSON Output + +**Always add `--json` flag:** + +```typescript +if (options.json) { + console.log(JSON.stringify(data, null, 2)); +} else { + // Human-readable format via page + page.print(); +} +``` + +### Error Handling + +```typescript +try { + // Command logic +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error('❌ Command failed:', message); + process.exit(1); +} +``` + +## Integration with Pages + +Commands should use the router for display: + +```typescript +import { router } from '../../ui/router'; +import '../../ui/pages/{type}'; // Trigger page registration + +// In action handler: +const page = await router.navTo(client, '{TYPE_CODE}', { name: id }); +page.print(); +``` + +This separates: + +- **Command** - Handles CLI args, options, auth, progress +- **Page** - Handles data fetching and rendering + +## Checklist + +- [ ] Command file created with CRUD subcommands +- [ ] Uses `getAdtClientV2()` for client +- [ ] Uses router for page-based display +- [ ] Has `--json` option for machine output +- [ ] Proper error handling with emoji indicators +- [ ] Exported from area index +- [ ] Registered in cli.ts +- [ ] Basic tests for command structure + +## Related + +- `/adt-page` - Page creation (display logic) +- `/adt-adk` - Full object type implementation diff --git a/.agents/commands/adt/contract.md b/.agents/commands/adt/contract.md new file mode 100644 index 000000000..98bb939e3 --- /dev/null +++ b/.agents/commands/adt/contract.md @@ -0,0 +1,392 @@ +# ADT Contract Creation Workflow + +Create type-safe ADT REST API contracts. + +## Fundamental Concepts + +### What are ADT Contracts? + +**Contracts are type-safe API definitions** that describe ADT REST endpoints: + +- **Method** - HTTP method (GET, POST, PUT, DELETE) +- **Path** - URL pattern with parameters +- **Headers** - Accept, Content-Type +- **Query** - URL query parameters +- **Body** - Request body schema (for POST/PUT) +- **Responses** - Response schemas per status code + +### Type Safety Requirements + +**Contracts use speci-compatible schemas** for full type inference: + +```typescript +import { contract } from '../../base'; +import { mySchema } from '@abapify/adt-schemas-xsd'; + +// Contract definition with full type safety +export const myResource = { + get: (id: string) => + contract({ + method: 'GET', + path: `/sap/bc/adt/area/${id}`, + headers: { Accept: 'application/xml' }, + responses: { + 200: mySchema, // Schema provides response type + }, + }), +}; + +// When executed, response is fully typed: +// const result = await client.execute(myResource.get('ID')); +// result is typed as InferXsd +``` + +### Contract Testing Philosophy + +**Tests validate contract definitions, not HTTP calls:** + +- Contracts are plain objects describing endpoints +- No mocks needed - test the definition directly +- Type safety verified at compile time +- If test file compiles, types are correct + +```typescript +// Contract returns a descriptor object +const contract = myResource.get('ID123'); + +// Test the definition directly - no HTTP involved +expect(contract.method).toBe('GET'); +expect(contract.path).toBe('/sap/bc/adt/area/ID123'); +expect(contract.responses[200]).toBe(mySchema); +``` + +## Usage + +```bash +/adt-contract +``` + +**Example:** `/adt-contract transportrequests` + +## Prerequisites + +- Schema created via `/adt-schema` workflow +- Understanding of ADT endpoint structure + +## Workflow Steps + +### Step 1: Identify Endpoint Structure + +**Goal:** Map ADT REST API to contract operations. + +**Actions:** + +1. Discover endpoint via ADT discovery: + ```bash + npx adt discovery + ``` +2. Document endpoint details: + - Base path: `/sap/bc/adt/{area}/{resource}` + - Supported methods: GET, POST, PUT, DELETE + - Query parameters + - Request/response content types + +**Common patterns:** +| Operation | Method | Path | Content-Type | +|-----------|--------|------|--------------| +| Get single | GET | `/{resource}/{id}` | application/xml | +| List | GET | `/{resource}` | application/xml | +| Create | POST | `/{resource}` | application/xml | +| Update | PUT | `/{resource}/{id}` | application/xml | +| Delete | DELETE | `/{resource}/{id}` | - | +| Action | POST | `/{resource}/{id}/{action}` | varies | + +### Step 2: Create Contract File + +**Location:** `adt-contracts/src/adt/{area}/{resource}.ts` + +**Pattern:** + +```typescript +// src/adt/{area}/{resource}.ts +import { contract } from '../../base'; +import { mySchema, myListSchema } from '@abapify/adt-schemas-xsd'; + +/** + * {Resource} Contract + * + * Endpoint: /sap/bc/adt/{area}/{resource} + */ +export const myResource = { + /** + * Get single {resource} + */ + get: (id: string) => + contract({ + method: 'GET', + path: `/sap/bc/adt/{area}/${id}`, + headers: { + Accept: 'application/xml', + }, + responses: { + 200: mySchema, + }, + }), + + /** + * List {resources} + */ + list: (params?: { package?: string; user?: string }) => + contract({ + method: 'GET', + path: '/sap/bc/adt/{area}', + headers: { + Accept: 'application/xml', + }, + query: params, + responses: { + 200: myListSchema, + }, + }), + + /** + * Create {resource} + */ + create: () => + contract({ + method: 'POST', + path: '/sap/bc/adt/{area}', + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml', + }, + body: mySchema, + responses: { + 200: mySchema, + 201: mySchema, + }, + }), + + /** + * Update {resource} + */ + update: (id: string) => + contract({ + method: 'PUT', + path: `/sap/bc/adt/{area}/${id}`, + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml', + }, + body: mySchema, + responses: { + 200: mySchema, + }, + }), + + /** + * Delete {resource} + */ + delete: (id: string) => + contract({ + method: 'DELETE', + path: `/sap/bc/adt/{area}/${id}`, + responses: { + 200: undefined, + 204: undefined, + }, + }), +}; +``` + +### Step 3: Export Contract + +**Actions:** + +1. Create/update area index `src/adt/{area}/index.ts`: + + ```typescript + import { myResource } from './myresource'; + + export const areaContract = { + myResource, + // ... other resources + }; + + export type AreaContract = typeof areaContract; + ``` + +2. Export from main index `src/adt/index.ts`: + ```typescript + export { areaContract } from './{area}'; + ``` + +### Step 4: Create Test Scenario (MANDATORY) + +**Location:** `adt-contracts/tests/contracts/{area}.ts` + +#### 🚨 CRITICAL: Tests Must Be Fully Typed + +**All tests must have full type checking** - not just runtime assertions: + +1. **TypeScript must compile** - `tsc --noEmit` must pass +2. **No `any` types** - all variables must be properly typed +3. **Schema references verified** - TypeScript validates schema compatibility +4. **ContractOperation type required** - use the type helper + +**Pattern:** + +```typescript +// tests/contracts/{area}.ts +import { ContractScenario, type ContractOperation } from './base'; +import { myResource } from '../../src/adt/{area}/myresource'; +import { mySchema, myListSchema } from '@abapify/adt-schemas-xsd'; +import { fixtures } from 'adt-fixtures'; + +export class MyResourceScenario extends ContractScenario { + readonly name = 'My Resource'; + + // ContractOperation[] ensures type safety + readonly operations: ContractOperation[] = [ + { + name: 'get single', + contract: () => myResource.get('ID123'), + method: 'GET', + path: '/sap/bc/adt/{area}/ID123', + headers: { Accept: 'application/xml' }, + response: { + status: 200, + schema: mySchema, // TypeScript validates this is a valid schema + fixture: fixtures.myresource?.single, // Optional + }, + }, + { + name: 'list', + contract: () => myResource.list({ package: 'ZTEST' }), + method: 'GET', + path: '/sap/bc/adt/{area}', + headers: { Accept: 'application/xml' }, + query: { package: 'ZTEST' }, + response: { + status: 200, + schema: myListSchema, + }, + }, + { + name: 'create', + contract: () => myResource.create(), + method: 'POST', + path: '/sap/bc/adt/{area}', + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml', + }, + body: { schema: mySchema }, + response: { status: 200, schema: mySchema }, + }, + // ... more operations + ]; +} +``` + +**Register scenario:** + +```typescript +// tests/contracts/index.ts +import { MyResourceScenario } from './{area}'; +export const SCENARIOS = [...existing, new MyResourceScenario()]; +``` + +### Step 5: Run Tests AND Type Check + +```bash +# Run tests +npx nx test adt-contracts + +# ALSO run type check - MANDATORY +npx tsc --noEmit -p packages/adt-contracts +``` + +**Verify:** + +- Contract has correct method +- Contract has correct path +- Contract has correct headers +- Contract has correct query params (if any) +- Contract has body schema (for POST/PUT) +- Contract has response schema +- **Type check passes** (compile-time verification) + +## Contract Patterns + +### Path Parameters + +```typescript +get: (id: string, subId?: string) => + contract({ + path: subId + ? `/sap/bc/adt/area/${id}/sub/${subId}` + : `/sap/bc/adt/area/${id}`, + // ... + }); +``` + +### Query Parameters + +```typescript +list: (params?: { package?: string; user?: string; maxResults?: number }) => + contract({ + query: params, + // ... + }); +``` + +### Multiple Response Codes + +```typescript +create: () => + contract({ + responses: { + 200: mySchema, // Success with body + 201: mySchema, // Created with body + 204: undefined, // Success no body + }, + }); +``` + +### Actions + +```typescript +release: (id: string) => + contract({ + method: 'POST', + path: `/sap/bc/adt/area/${id}/release`, + headers: { + Accept: 'application/xml', + }, + responses: { + 200: releaseResultSchema, + }, + }); +``` + +## Checklist + +- [ ] Endpoint structure documented +- [ ] Contract file created with all operations +- [ ] Schemas imported from `@abapify/adt-schemas-xsd` +- [ ] Exported from area index +- [ ] Exported from main index +- [ ] Test scenario created with ContractOperation[] type +- [ ] Tests passing: `npx nx test adt-contracts` +- [ ] **Type check passing: `npx tsc --noEmit`** + +## Output + +- Contract file in `adt-contracts/src/adt/{area}/` +- Test scenario in `adt-contracts/tests/contracts/` + +## Related + +- `/adt-schema` - Schema creation (prerequisite) +- `/adt-adk` - Full object type implementation diff --git a/.agents/commands/adt/page.md b/.agents/commands/adt/page.md new file mode 100644 index 000000000..52d2b5434 --- /dev/null +++ b/.agents/commands/adt/page.md @@ -0,0 +1,356 @@ +# ADT CLI Page Creation Workflow + +Create CLI display pages for ADT object types. + +## Usage + +```bash +/adt-page +``` + +**Example:** `/adt-page transport` + +## Prerequisites + +- ADK model implemented via `/adt-adk` workflow +- Understanding of page patterns in `adt-cli/src/lib/ui/` + +## Fundamental Concepts + +### Data Access Preference Hierarchy + +**ALWAYS prefer higher-level abstractions. Use the first applicable option:** + +``` +1. ADK (adk-v2) ← PREFERRED - High-level object facade + │ Full type safety, lazy loading, relationships + │ Example: AdkTransportRequest.get(ctx, number) + ▼ +2. Client Service ← If ADK doesn't exist yet + │ Business logic layer + │ Example: client.services.transports.get(number) + ▼ +3. Contract ← If service doesn't exist + │ Type-safe API definition + │ Example: client.execute(transportContract.get(number)) + ▼ +4. Raw Fetch ← LAST RESORT - Only if nothing else applies + Direct HTTP call + Example: client.fetch('/sap/bc/adt/cts/...') +``` + +**Why this order?** + +- **ADK** - Encapsulates business logic, relationships, lazy loading +- **Service** - Reusable business logic, error handling +- **Contract** - Type-safe but low-level +- **Fetch** - No type safety, manual parsing + +### Page Architecture + +Pages are **self-registering display components** that: + +- Fetch data using ADK objects (preferred) +- Render formatted output for terminal +- Register with the router on import + +``` +┌─────────────────────────────────────┐ +│ Command (get, list, etc.) │ +│ - Handles CLI args, auth, progress │ +└─────────────┬───────────────────────┘ + │ router.navTo(client, type, params) + ▼ +┌─────────────────────────────────────┐ +│ Router │ +│ - Finds registered page by type │ +│ - Calls page.fetch() then render() │ +└─────────────┬───────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ Page │ +│ - fetch: Load data via ADK │ +│ - render: Build component tree │ +│ - print: Output to terminal │ +└─────────────────────────────────────┘ +``` + +### Component System + +Pages use a component tree for rendering: + +```typescript +import { Box, Field, Section, Text, adtLink } from '../components'; + +// Components are composable +const content = Box( + Section( + '▼ Properties', + Field('Name', obj.name), + Field('Description', obj.description), + ), + Section('▼ Details', Text('Additional info...')), +); +``` + +### Self-Registration Pattern + +Pages register themselves when imported: + +```typescript +import { definePage } from '../router'; + +export const myPageDef = definePage({ + type: 'MYTYPE', // Object type code + name: 'My Object', + icon: '📦', + fetch: async (client, params) => { + /* ... */ + }, + render: (data, params) => { + /* ... */ + }, +}); +``` + +## Workflow Steps + +### Step 1: Create Page File + +**Location:** `adt-cli/src/lib/ui/pages/{type}.ts` + +**Pattern:** + +````typescript +/** + * {Type} Page + * + * Self-registering page for {type} objects using ADK. + * + * Note: ADK global context is automatically initialized by getAdtClientV2(). + * No need to create context manually - just use ADK objects directly. + */ + +import type { Page, Component } from '../types'; +import type { NavParams } from '../router'; +import { Adk{Type} } from '@abapify/adk-v2'; +import { Box, Field, Section, Text, adtLink } from '../components'; +import { createPrintFn } from '../render'; +import { definePage } from '../router'; + +// ============================================================================= +// Types +// ============================================================================= + +/** + * Page navigation parameters + */ +export interface {Type}Params extends NavParams { + /** Object name/ID */ + name?: string; + /** Additional display options */ + showDetails?: boolean; +} + +// ============================================================================= +// Helper Functions +// ============================================================================= + +/** + * Format date for display + */ +function formatDate(date: Date | undefined): string { + if (!date) return '-'; + return date.toLocaleString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + }); +} + +// ============================================================================= +// Render Function +// ============================================================================= + +/** + * Render {type} page + */ +function render{Type}Page(data: Adk{Type}, params: NavParams): Page { + const showDetails = (params.showDetails as boolean | undefined) ?? false; + + // Build content sections + const sections: Component[] = []; + + // Properties section with ADT link + const objectLink = adtLink({ name: data.name, type: data.type, uri: data.objectUri }); + + sections.push(Section('▼ Properties', + Field('Name', data.name), + Field('Description', data.description || '-'), + Field('Package', data.package || '-'), + Field('ADT Link', objectLink), + )); + + // Additional sections based on showDetails + if (showDetails) { + sections.push(Section('▼ Details', + Field('Created By', data.createdBy || '-'), + Field('Created At', formatDate(data.createdAt)), + Field('Changed By', data.changedBy || '-'), + Field('Changed At', formatDate(data.changedAt)), + )); + } + + const content = Box(...sections); + + const page: Page = { + title: `{Type}: ${data.name}`, + icon: '📦', + render: () => content.render(), + print: () => {}, + }; + + page.print = createPrintFn(page); + return page; +} + +// ============================================================================= +// Page Definition +// ============================================================================= + +/** + * {Type} Page Definition + * + * Self-registers with the router on import. + * Type: {TYPE_CODE} + * + * Usage: + * ```ts + * const page = await router.navTo(client, '{TYPE_CODE}', { + * name: 'OBJECT_NAME', + * showDetails: true + * }); + * page.print(); + * ``` + */ +export const {type}PageDef = definePage({ + type: '{TYPE_CODE}', + name: '{Type}', + icon: '📦', + + // ADK global context is auto-initialized by getAdtClientV2() + // Just use ADK objects directly - no context needed! + fetch: async (_client, params) => { + if (!params.name) throw new Error('{Type} name is required'); + // Use ADK (PREFERRED) - context is global, no need to pass it + return Adk{Type}.get(params.name); + }, + + render: render{Type}Page, +}); + +export default {type}PageDef; +```` + +### Step 2: Export from Index + +**Location:** `adt-cli/src/lib/ui/pages/index.ts` + +```typescript +export { {type}PageDef } from './{type}'; +``` + +### Step 3: Register Routes (if needed) + +**Location:** `adt-cli/src/lib/ui/routes.ts` + +Routes are auto-registered via `definePage()`, but you can add aliases: + +```typescript +import './{type}'; // Triggers self-registration +``` + +### Step 4: Import in Command + +**Location:** `adt-cli/src/lib/commands/{area}/{type}.ts` + +```typescript +// Import to trigger page registration +import '../../ui/pages/{type}'; + +// Use in action: +const page = await router.navTo(client, '{TYPE_CODE}', { name: id }); +page.print(); +``` + +## Component Reference + +### Available Components + +```typescript +import { Box, Field, Section, Text, adtLink } from '../components'; + +// Box - Container for other components +Box(...children); + +// Section - Titled section with children +Section('▼ Title', ...children); + +// Field - Label: Value pair +Field('Label', 'Value'); + +// Text - Plain text +Text('Some text'); + +// adtLink - Clickable ADT link (terminal hyperlink) +adtLink({ name: 'OBJ', type: 'CLAS', uri: '/sap/bc/adt/...' }); +``` + +### Custom Components + +```typescript +function MyComponent(data: MyData): Component { + return { + render: () => `Custom: ${data.value}`, + }; +} +``` + +## Page Definition Interface + +```typescript +interface PageDefinition { + /** Object type code (e.g., 'CLAS', 'RQRQ') */ + type: string; + + /** Display name */ + name: string; + + /** Icon emoji */ + icon: string; + + /** Fetch data from client */ + fetch: (client: AdtClient, params: NavParams) => Promise; + + /** Render page from data */ + render: (data: T, params: NavParams) => Page; +} +``` + +## Checklist + +- [ ] Page file created with proper structure +- [ ] Uses ADK for data fetching +- [ ] Uses component system for rendering +- [ ] Self-registers via `definePage()` +- [ ] Exported from pages index +- [ ] Imported in command file +- [ ] Supports relevant display options (showDetails, etc.) + +## Related + +- `/adt-command` - Command creation (uses pages) +- `/adt-adk` - Full object type implementation diff --git a/.agents/commands/adt/schema.md b/.agents/commands/adt/schema.md new file mode 100644 index 000000000..1507da229 --- /dev/null +++ b/.agents/commands/adt/schema.md @@ -0,0 +1,333 @@ +# ADT Schema Creation Workflow + +Create type-safe ADT schemas from XSD or manual definitions. + +## Fundamental Concepts + +### What is ts-xsd? + +**ts-xsd is XSD schema written in TypeScript** - it's not just a code generator, it's a complete type system that mirrors XSD semantics: + +- **Full type inference** - TypeScript infers exact types from schema definition +- **Round-trip capability** - `parse(xml) → object` and `build(object) → xml` +- **XSD semantics** - Supports sequences, choices, attributes, extensions, includes +- **Compile-time safety** - Type errors caught at build time, not runtime + +```typescript +// Schema definition IS the type definition +const schema = { + elements: { + Person: { + sequence: [ + { name: 'name', type: 'string' }, + { name: 'age', type: 'number' }, + ], + }, + }, +} as const; + +// TypeScript infers: { name: string; age: number } +type Person = InferXsd; +``` + +### What is speci? + +**speci** is the schema factory that adds parse/build methods to ts-xsd schemas: + +- Wraps raw schema definition with runtime functionality +- Provides `schema.parse(xml)` and `schema.build(obj)` methods +- Requires specific type structure for inference to work +- All ADT schemas MUST be speci-compatible + +```typescript +import schema from '../../speci'; + +// speci wraps the definition and adds parse/build +export default schema({ + ns: 'http://www.sap.com/adt/...', + root: 'MyRoot', + elements: { + /* ... */ + }, +} as const); // as const is REQUIRED for type inference! +``` + +### Why `as const`? + +Without `as const`, TypeScript widens literal types: + +```typescript +// WITHOUT as const - types are widened +const bad = { name: 'foo', type: 'string' }; // type: { name: string; type: string } + +// WITH as const - literal types preserved +const good = { name: 'foo', type: 'string' } as const; // type: { name: 'foo'; type: 'string' } +``` + +speci needs literal types to infer the correct TypeScript types from schema definitions. + +## Usage + +```bash +/adt-schema +``` + +**Example:** `/adt-schema transportmanagment` + +## Prerequisites + +- Sample XML from SAP system +- XSD file (if available from SAP SDK) + +## Workflow Steps + +### Step 1: Capture Sample XML + +**Goal:** Get real XML responses from SAP for the endpoint. + +**Actions:** + +1. Use ADT CLI to fetch sample data: + ```bash + npx adt fetch /sap/bc/adt/{endpoint} --raw > sample.xml + ``` +2. Capture multiple variants: + - Single object response + - Collection/list response + - Create request body + - Update request body + - Error responses +3. Save samples to `adt-schemas-xsd/tests/scenarios/fixtures/` + +**Output:** XML sample files for testing + +### Step 2: Determine Schema Source + +**Decision tree:** + +1. **XSD exists in SAP SDK?** + - Yes → Use generated schema (Step 3a) + - No → Check XML format + +2. **XML format?** + - Standard ADT XML → Create manual schema (Step 3b) + - ABAP XML (`asx:abap`) → Create ABAP XML schema (Step 3c) + +### Step 3a: Generate Schema from XSD + +**When:** XSD file available in SAP SDK. + +**Actions:** + +1. Download XSD to `.xsd/model/`: + ```bash + npx nx run adt-schemas-xsd:download + ``` +2. Add schema to `tsxsd.config.ts`: + ```typescript + const schemas = { + // ... existing + myschema: { root: 'MyRootElement' }, + }; + ``` +3. Generate: + ```bash + npx nx run adt-schemas-xsd:generate + ``` +4. Verify generated file in `src/schemas/generated/sap/` + +#### 🚨 CRITICAL: Never Modify Generated Files + +**Generated schemas are READ-ONLY.** If a generated schema is incorrect: + +1. **NEVER edit files in `src/schemas/generated/`** - changes will be overwritten +2. **Fix the generator** in `ts-xsd/src/codegen/` instead +3. **Requires user confirmation** before modifying ts-xsd generator +4. **Full ts-xsd test coverage required** - all existing tests must pass +5. Regenerate: `npx nx run adt-schemas-xsd:generate` + +```bash +# If generator change needed: +# 1. Get user confirmation +# 2. Modify ts-xsd/src/codegen/ +# 3. Run full ts-xsd tests: npx nx test ts-xsd +# 4. Regenerate schemas: npx nx run adt-schemas-xsd:generate +# 5. Run schema tests: npx nx test adt-schemas-xsd +``` + +### Step 3b: Create Manual Schema (Standard ADT XML) + +**When:** No XSD, standard ADT XML format. + +**Location:** `adt-schemas-xsd/src/schemas/manual/` + +**Pattern:** + +```typescript +// src/schemas/manual/myschema.ts +import schema from '../../speci'; + +export default schema({ + ns: 'http://www.sap.com/adt/myarea', + prefix: 'myprefix', + root: 'MyRootElement', + include: [ + /* imported schemas */ + ], + elements: { + MyRootElement: { + sequence: [ + { name: 'child1', type: 'string' }, + { name: 'child2', type: 'Child2Type' }, + ], + attributes: [{ name: 'attr1', type: 'string' }], + }, + Child2Type: { + sequence: [ + /* ... */ + ], + }, + }, +} as const); // CRITICAL: as const for type inference +``` + +### Step 3c: Create ABAP XML Schema + +**When:** XML uses `asx:abap` envelope format. + +**Key rules:** + +1. **ALWAYS use `as const`** - Critical for TypeScript type inference +2. **Element names WITHOUT namespace prefix** - Use `'abap'`, not `'asx:abap'` +3. **Root element content is parsed directly** - No wrapper in result + +**Pattern:** + +```typescript +// src/schemas/manual/myabapschema.ts +import schema from '../../speci'; + +export default schema({ + ns: 'http://www.sap.com/abapxml', + prefix: 'asx', + root: 'abap', // WITHOUT prefix! + elements: { + abap: { + sequence: [{ name: 'values', type: 'values' }], + attributes: [{ name: 'version', type: 'string' }], + }, + values: { + sequence: [{ name: 'DATA', type: 'DATA' }], + }, + DATA: { + sequence: [ + { name: 'FIELD1', type: 'string' }, + { name: 'FIELD2', type: 'number' }, + ], + }, + }, +} as const); // CRITICAL! +``` + +### Step 4: Export Schema + +**Actions:** + +1. Add export to `src/schemas/index.ts`: + + ```typescript + // For generated schemas - usually auto-exported + export * from './generated/sap'; + + // For manual schemas + export { default as myschema } from './manual/myschema'; + ``` + +### Step 5: Create Test Scenario (MANDATORY) + +**Location:** `adt-schemas-xsd/tests/scenarios/` + +#### 🚨 CRITICAL: Tests Must Be Fully Typed + +**All tests must have full type checking** - not just runtime assertions: + +1. **TypeScript must compile** - `tsc --noEmit` must pass +2. **No `any` types** - all variables must be properly typed +3. **Type inference verified** - accessing properties validates schema correctness +4. **SchemaType required** - use the type helper for parsed data + +**Pattern:** + +```typescript +// tests/scenarios/myschema.ts +import { expect } from 'vitest'; +import { Scenario, type SchemaType } from './base/scenario'; +import { myschema } from '../../src/schemas/index'; + +export class MySchemaScenario extends Scenario { + readonly schema = myschema; + readonly fixtures = ['myschema.xml']; // In fixtures/ folder + + validateParsed(data: SchemaType): void { + // Type-safe assertions - TypeScript validates property access! + // If schema is wrong, this won't compile + expect(data.someField).toBeDefined(); + expect(data.nested?.child).toBe('expected'); + + // ❌ This would cause compile error if 'wrongField' not in schema: + // expect(data.wrongField).toBeDefined(); + } + + validateBuilt(xml: string): void { + expect(xml).toContain('xmlns:'); + expect(xml).toContain('` + `x-sap-security-session: use` → free slot (token survives) + +All subsequent requests include `x-sap-security-session: use`. SAP allows **1 security session per user** — always DELETE after getting the token. + +**Lock flow**: `adt-locks/LockService` is the single lock implementation. All lock/unlock operations in `adk/model.ts`, `adt-export`, and CLI commands delegate to it. See [`packages/adt-client/AGENTS.md`](../packages/adt-client/AGENTS.md) for full protocol details. + +## Rules Index + +All AI agent rules live in `.agents/rules/` (single source of truth). + +### Always On + +| Rule | Description | +| ------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| [`git/no-auto-commit`](rules/git/no-auto-commit.md) | Never commit or push without explicit user approval | +| [`development/coding-conventions`](rules/development/coding-conventions.md) | TS strict, ESM only, naming, formatting, import conventions | +| [`development/file-lifecycle`](rules/development/file-lifecycle.md) | Generated/downloaded file guardrails | +| [`openspec/project-planning-memory`](rules/openspec/project-planning-memory.md) | OpenSpec workflow and project memory | +| [`verification/after-changes`](rules/verification/after-changes.md) | Build, typecheck, test, lint, format checklist | + +### On Demand (model_decision) + +| Rule | Description | +| --------------------------------------------------------------------------- | ---------------------------------- | +| [`openspec/spec-first-then-code`](rules/openspec/spec-first-then-code.md) | Check specs before coding | +| [`development/tmp-folder-testing`](rules/development/tmp-folder-testing.md) | Use `tmp/` for scratch files | +| [`development/package-versions`](rules/development/package-versions.md) | Always install latest versions | +| [`adt/adk-save-logic`](rules/adt/adk-save-logic.md) | ADK upsert/lock edge cases | +| [`adt/adt-ddic-mapping`](rules/adt/adt-ddic-mapping.md) | DDIC object → schema mapping | +| [`adt/xsd-best-practices`](rules/adt/xsd-best-practices.md) | XSD validity and builder rules | +| [`nx/nx-monorepo-setup`](rules/nx/nx-monorepo-setup.md) | Package creation, config templates | +| [`nx/nx-circular-dependencies`](rules/nx/nx-circular-dependencies.md) | Fix false circular dep issues | + +### File-Scoped (glob) + +| Rule | Glob | Description | +| --------------------------------------------------------------------- | --------- | ------------------------------------------ | +| [`development/bundler-imports`](rules/development/bundler-imports.md) | `**/*.ts` | Extensionless imports for bundled packages | + +## Known Gotchas + +### bun.lock excluded from Nx file walker + +`bun.lock` is in `.git/info/exclude` to prevent JFrog Artifactory URLs from reaching GitHub. Nx's Rust file walker uses the `ignore` crate, which reads `.git/info/exclude` but (unlike `git`) has **no concept of the git index** — tracked files matching ignore patterns are still skipped. This means Nx never sees the lockfile and `externalNodes` in the project graph is empty. + +**Consequence**: Any Nx plugin that infers `{ externalDependencies: [...] }` in target inputs will fail. The `@nx/eslint/plugin` does this for the `lint` target. + +**Workaround**: The `lint` target's inputs are overridden in `nx.json` `targetDefaults` to drop `externalDependencies`. If a new plugin introduces similar inputs, add the same override for that target. + +**Do NOT**: Remove `bun.lock` from `.git/info/exclude` — the JFrog constraint is intentional. + +## Package-Level Guides + +Each package has its own `AGENTS.md` with detailed conventions: + +- [`packages/abap-ast/AGENTS.md`](../packages/abap-ast/AGENTS.md) — zero-dependency AST + deterministic printer for ABAP +- [`packages/adk/AGENTS.md`](../packages/adk/AGENTS.md) — ABAP Development Kit, object CRUD, save/lock flow +- [`packages/acds/AGENTS.md`](../packages/acds/AGENTS.md) — ABAP CDS parser +- [`packages/aclass/AGENTS.md`](../packages/aclass/AGENTS.md) — ABAP OO parser (CLAS/INTF) +- [`packages/adt-cli/AGENTS.md`](../packages/adt-cli/AGENTS.md) — CLI commands, service pattern +- [`packages/adt-client/AGENTS.md`](../packages/adt-client/AGENTS.md) — Contract-driven REST client +- [`packages/adt-contracts/AGENTS.md`](../packages/adt-contracts/AGENTS.md) — Contract testing framework +- [`packages/adt-schemas/AGENTS.md`](../packages/adt-schemas/AGENTS.md) — XSD-derived schemas +- [`packages/adt-mcp/AGENTS.md`](../packages/adt-mcp/AGENTS.md) — MCP server (stdio + Streamable HTTP) +- [`packages/adt-pilot`](../packages/adt-pilot/) — abapify Pilot (`package.json`, OpenSpec `add-abapify-pilot`) +- [`packages/adt-plugin-abapgit/AGENTS.md`](../packages/adt-plugin-abapgit/AGENTS.md) — abapGit serialization +- [`packages/openai-codegen/AGENTS.md`](../packages/openai-codegen/AGENTS.md) — OpenAPI → ABAP codegen +- [`packages/ts-xsd/AGENTS.md`](../packages/ts-xsd/AGENTS.md) — W3C XSD parser +- [`packages/adt-auth/AGENTS.md`](../packages/adt-auth/AGENTS.md) — Auth methods, browser SSO +- [`packages/adt-fixtures/AGENTS.md`](../packages/adt-fixtures/AGENTS.md) — Test fixtures diff --git a/.agents/rules/adt/adk-save-logic.md b/.agents/rules/adt/adk-save-logic.md new file mode 100644 index 000000000..6b7b08f52 --- /dev/null +++ b/.agents/rules/adt/adk-save-logic.md @@ -0,0 +1,69 @@ +--- +trigger: model_decision +description: Complex logic handling for ADK object save, upsert, and locking. +--- + +# ADK Save and Upsert Logic + +## Upsert Fallback Handling + +`AdkObject.save()` upsert flow uses a **GET-before-LOCK** strategy: + +1. **Existence Check (GET):** + - Before locking, do a lightweight GET to check if the object exists. + - If 404/405 → go straight to POST (create), skipping LOCK entirely. + - **Why:** On BTP, locking a non-existent DDIC object creates a draft that persists + even after unlock, blocking subsequent POST (create) attempts. + +2. **Lock + PUT (object exists):** + - If GET succeeds → proceed with normal LOCK + PUT + UNLOCK flow. + - If PUT fails with 404/405/S_ABPLNGVS → unlock + fallback to create (safety net). + +3. **Create Fallback:** + - Both the GET-check path and the PUT-failure path use `fallbackToCreate()`. + - Catch `422 "already exists"` → mark unchanged. + +**Note:** Both paths must wrap their `save({ mode: 'create' })` calls in try/catch blocks that check `isAlreadyExistsError(e)`. + +## abapLanguageVersion Must NOT Be Sent in PUT/POST + +`saveViaContract()` strips `abapLanguageVersion` from the payload before sending. +Eclipse ADT never sends this attribute either — SAP infers it from the package. + +**Why:** Including `adtcore:abapLanguageVersion` triggers an `S_ABPLNGVS` authorization +check on the server. On BTP Cloud systems this auth object may not be assigned, +causing a **403 Forbidden** after the lock is already acquired — leading to phantom locks. + +The value is kept in `_data` for reading/display, just excluded from the HTTP body. + +## Endpoint Behaviors + +- **TABL Lock:** Can succeed for existing objects, but subsequent metadata PUT returns `405 Method Not Allowed`. +- **TTYP Lock:** Returns `405` when object doesn't exist (instead of `404`). +- **BTP DDIC Lock:** May succeed for non-existent objects, creating a draft. Unlock does NOT clean up the draft. +- **Create (POST):** Returns `422` (or "Unprocessable") with "already exists" message if object exists. +- **Create (POST) on BTP:** Requires `packageRef` in the body. Without it, SAP can't determine the target package and triggers S_ABPLNGVS. + +## BTP-Specific Notes + +- Use `--package ROOT_PACKAGE` when deploying new objects to BTP so the format plugin resolves `packageRef` for each object. +- SAP's strict `xs:sequence` parser requires ALL elements in a sequence to be present, even when logically empty (e.g., `builtInType` in TTYP with `refToDictionaryType`). Default empty fields to `""` or `0` instead of omitting them. + +## FM processingType Ignored During POST + +SAP ADT ignores `processingType` in the POST body when creating function modules. All FMs are created with `processingType="normal"` regardless of the XML payload. + +**Fix**: `AdkFunctionModule.savePendingSources()` PUTs metadata before source to apply the correct processingType after creation. + +## ETag Refresh After Metadata PUT + +When you PUT metadata on an ADT object, SAP increments the internal object version. This invalidates cached ETags for **all** related endpoints (metadata, source, etc.), but the client only updates the cache for the specific URL that was PUT. + +**Pattern**: After a metadata PUT, always GET sibling endpoints (e.g., `source/main`) to refresh their cached ETags before PUTting to them. Otherwise → HTTP 412 Precondition Failed (If-Match mismatch). + +``` +1. GET source/main → ETag E1 cached +2. PUT metadata → Object version changes silently +3. GET source/main → ETag E2 cached (REQUIRED refresh!) +4. PUT source/main → Uses E2, succeeds +``` diff --git a/.agents/rules/adt/adt-ddic-mapping.md b/.agents/rules/adt/adt-ddic-mapping.md new file mode 100644 index 000000000..477be283a --- /dev/null +++ b/.agents/rules/adt/adt-ddic-mapping.md @@ -0,0 +1,44 @@ +--- +trigger: model_decision +description: Mapping of SAP ADT DDIC objects to their root elements and schemas. +--- + +# SAP ADT DDIC Object Mapping + +SAP ADT wraps DDIC object responses in different root elements depending on the object type. + +## Mappings + +### DOMA (Domain) + +- **Root Element:** `domain` +- **Namespace:** `http://www.sap.com/adt/dictionary/domains` +- **Schema:** `sap/domain.xsd` +- **Notes:** Direct extension of `adtcore:AdtMainObject`. Works out of the box. + +### DTEL (Data Element) + +- **Root Element:** `blue:wbobj` +- **Namespace:** `http://www.sap.com/wbobj/dictionary/dtel` +- **Inner Content:** Wraps `dtel:dataElement`. +- **Schema:** `.xsd/custom/dataelementWrapper.xsd` +- **ADK wrapperKey:** `'wbobj'` + +### TABL (Table) / Structure + +- **Root Element:** `blue:blueSource` +- **Namespace:** `http://www.sap.com/wbobj/blue` +- **Schema:** `.xsd/custom/blueSource.xsd` +- **ADK wrapperKey:** `'blueSource'` +- **Notes:** Extends `abapsource:AbapSourceMainObject`. Contracts use `crud()` helper. + +### TTYP (Table Type) + +- **Root Element:** `tableType` +- **Namespace:** `http://www.sap.com/dictionary/tabletype` +- **Schema:** `sap/tabletype.xsd` +- **Notes:** Direct extension of `adtcore:AdtMainObject`. Works out of the box. + +## Locking + +- **Header:** Lock operations require `Accept: application/*,application/vnd.sap.as+xml;charset=UTF-8;dataname=com.sap.adt.lock.result`. diff --git a/.agents/rules/adt/xsd-best-practices.md b/.agents/rules/adt/xsd-best-practices.md new file mode 100644 index 000000000..52a62b4fb --- /dev/null +++ b/.agents/rules/adt/xsd-best-practices.md @@ -0,0 +1,36 @@ +--- +trigger: model_decision +description: Best practices for working with XSD files and XML parsing in this project. +--- + +# XSD and XML Best Practices + +## XSD Validity + +**CRITICAL:** Never create broken/invalid XSD files and then modify ts-xsd to handle them. XSDs must be valid W3C XML Schema documents FIRST. + +- `xs:import` = cross-namespace. Only ONE `xs:import` per namespace per schema. +- `xs:include` = same-namespace composition/extension. +- Every `ref="prefix:name"` must resolve to an in-scope element declaration. +- Every `type="prefix:TypeName"` must resolve to an in-scope type definition. + +## Custom Extensions + +If SAP's XSD is missing types: + +1. Create a custom extension XSD in `.xsd/custom/` with the **SAME** targetNamespace as the SAP schema. +2. Use `xs:include` to bring in the SAP schema. +3. Add new types/elements in the extension. +4. Have consumers `xs:import` the extension instead of the SAP schema directly. + +## XML Parsing Gotchas + +- **xmldom & Attributes:** The `@xmldom/xmldom` library returns an empty string `""` (instead of `null`) when calling `getAttribute()` for a non-existent attribute. + - **Rule:** Always use `hasAttribute()` to check for existence before reading values to avoid treating missing attributes as present empty strings. + +## TS-XSD Builder Logic (Do Not Regress) + +- **Qualified Attributes:** When building XML with schemas that use `attributeFormDefault="qualified"` and inherit attributes from imported schemas, the builder must collect namespace prefixes from the **ENTIRE** `$imports` chain, not just the root schema's `$xmlns`. + - _Context:_ SAP ADT schemas often import `abapsource.xsd` -> `adtcore.xsd`. Attributes like `name` defined in `adtcore.xsd` need the `adtcore:` prefix even if the root schema doesn't import `adtcore` directly. +- **Inherited Child Elements:** When building XML with inherited child elements (e.g. `packageRef` defined in `adtcore.xsd` but used by `interfaces.xsd`), the builder must use the **defining schema's namespace prefix** for the element tag, not the root schema's prefix. + - _Context:_ `walkElements` must return the schema where the element was defined. `buildElement` must use this schema to resolve the prefix. diff --git a/.agents/rules/development/bundler-imports.md b/.agents/rules/development/bundler-imports.md new file mode 100644 index 000000000..73480e2c6 --- /dev/null +++ b/.agents/rules/development/bundler-imports.md @@ -0,0 +1,38 @@ +--- +trigger: glob +description: Enforce extensionless imports for internal files in bundled TypeScript packages. +globs: '**/*.ts' +--- + +## Rule: Use Extensionless Imports for Internal Files + +When working within a TypeScript package that has `"moduleResolution": "bundler"` in its `tsconfig.json` and is processed by a build tool (like `tsdown`, `vite`, or `rollup`), **all internal, relative imports MUST be extensionless.** + +This applies to any file inside a package's `src` directory importing another file from the same package. + +### Rationale + +- With `"moduleResolution": "bundler"`, TypeScript allows extensionless imports and relies on the build tool to resolve them correctly. +- The build tool (e.g., `tsdown`) bundles the code, making runtime file extensions irrelevant for internal modules. +- This keeps import statements clean and consistent with modern JavaScript/TypeScript module practices. + +### Correct ✅ + +```typescript +// Correct: No file extension for internal, relative import +import { something } from './local-module'; +import { another } from '../utils/helpers'; +``` + +### Incorrect ❌ + +```typescript +// Incorrect: Unnecessary .js extension for an internal module +import { something } from './local-module.js'; +``` + +### Agent Behavior + +- When generating or modifying code in a package that uses a bundler, I will always use extensionless relative paths for internal imports. +- If I encounter existing code with `.js` extensions on internal imports, I will remove them. +- This rule does not apply to imports from external packages (e.g., `from 'fast-xml-parser'`). diff --git a/.agents/rules/development/coding-conventions.md b/.agents/rules/development/coding-conventions.md new file mode 100644 index 000000000..52ed376ee --- /dev/null +++ b/.agents/rules/development/coding-conventions.md @@ -0,0 +1,20 @@ +--- +trigger: always_on +description: Core coding conventions for the abapify monorepo. TypeScript strict, ESM only, naming, formatting. +--- + +# Coding Conventions + +- **TypeScript strict** — no `any` without a comment explaining why +- **ESM only** — no `require()`, no `__dirname` (use `import.meta.url`) +- **No decorators** except in packages that already use them +- **Async/await** over Promises `.then()` chains +- PascalCase for types/classes/interfaces; camelCase for variables/functions. + Module-level immutable primitive constants (e.g. `DEFAULT_TIMEOUT`, + `NS_USAGE`, `PAGES_DIR`) may use `SCREAMING_SNAKE_CASE` — this is the + convention used throughout the codebase and is preferred over camelCase + for top-level values that act as constants rather than mutable state. +- 2-space indentation (Prettier enforced) +- Cross-package imports: `@abapify/` +- Internal file imports: extensionless relative paths — see [bundler-imports](bundler-imports.md) for details +- `workspace:*` protocol for local workspace deps — see `$link-workspace-packages` skill for setup diff --git a/.agents/rules/development/file-lifecycle.md b/.agents/rules/development/file-lifecycle.md new file mode 100644 index 000000000..4deb52d66 --- /dev/null +++ b/.agents/rules/development/file-lifecycle.md @@ -0,0 +1,21 @@ +--- +trigger: always_on +description: Know which files are generated/downloaded before editing. Never edit codegen output or SAP XSD downloads. +--- + +# File Lifecycle — Know Before You Edit + +**Before editing ANY file**, check whether it's generated/downloaded: + +```bash +bunx nx show project --json | grep -i "xsd\|generated\|download" +``` + +| Pattern | Lifecycle | Rule | +| ------------------------------------- | ------------------- | ------------------------------------------------------ | +| `packages/*/src/schemas/generated/**` | Codegen output | Never edit — fix the generator or XSD source | +| `packages/adt-schemas/.xsd/sap/**` | Downloaded from SAP | Never edit — create custom extension in `.xsd/custom/` | +| `packages/adt-schemas/.xsd/custom/**` | Hand-maintained | Safe to edit | +| `packages/*/dist/**` | Build output | Never edit | + +If an edit keeps "reverting": **stop**. Something is regenerating the file. Check Nx targets before using `sed`/force-writes. diff --git a/.agents/rules/development/package-versions.md b/.agents/rules/development/package-versions.md new file mode 100644 index 000000000..ad6b3def9 --- /dev/null +++ b/.agents/rules/development/package-versions.md @@ -0,0 +1,56 @@ +--- +trigger: model_decision +description: Always install latest package versions when adding dependencies. Never hardcode specific versions. +--- + +# Package Versions: Always Install Latest + +## Rule + +When creating new projects, test scripts, or reproduction repos: + +**NEVER** hardcode specific versions in `package.json`. Instead: + +1. Use `bun add ` to get latest +2. Or use `latest` tag: `"tsdown": "latest"` + +## Why + +- Features may already be implemented in newer versions +- Bug reports against old versions waste maintainer time +- Testing against latest ensures accurate issue reproduction + +## Examples + +### ❌ WRONG - Hardcoded versions + +```json +{ + "devDependencies": { + "tsdown": "^0.16.7", + "typescript": "^5.7.2" + } +} +``` + +### ✅ CORRECT - Install fresh + +```bash +# Let bun resolve latest +bun init -y +bun add -D tsdown typescript + +# Or use latest tag +bun add -D tsdown@latest typescript@latest +``` + +## Exception + +When reproducing issues in **existing projects**, match the project's versions to accurately reproduce the environment. + +## Incident + +- **Date**: 2025-12-18 +- **Issue**: Reported tsdown#655 for missing feature that was already fixed in 0.18.x +- **Cause**: Used 0.16.7 from existing workspace instead of installing latest +- **Resolution**: Closed issue, apologized to maintainers diff --git a/.agents/rules/development/tmp-folder-testing.md b/.agents/rules/development/tmp-folder-testing.md new file mode 100644 index 000000000..2e1ee0c9c --- /dev/null +++ b/.agents/rules/development/tmp-folder-testing.md @@ -0,0 +1,36 @@ +--- +trigger: model_decision +description: If you want to create some test output which is not supposed to be commited to git - use tmp folder +--- + +# Temporary Files and Testing Output Rule + +## File Organization for Testing and Experiments + +### Rule: All Temporary Files → `tmp/` Directory + +**Applies to:** + +- CLI output files (e.g., `adt get ZCL_TEST -o tmp/class.xml`) +- Transport import test results +- Experiment outputs +- Debug files +- Any temporary data generated during development or testing + +**Examples:** + +```bash +# ✅ CORRECT - Use tmp/ for experiments +npx adt import transport TR001 ./tmp/test-transport --format abapgit --debug + +# ❌ WRONG - Don't create test files in root +npx adt import transport TR001 ./test-transport-import --format abapgit --debug +``` + +### Exception: E2E Tests + +**E2E test cases** can create new folders in `e2e/` directory: + +- `e2e/transport-import/` - for transport import integration tests +- `e2e/quality-checks/` - for ATC integration tests +- `e2e/[feature-name]/` - for specific feature testing diff --git a/.agents/rules/git/no-auto-commit.md b/.agents/rules/git/no-auto-commit.md new file mode 100644 index 000000000..4242cfa3a --- /dev/null +++ b/.agents/rules/git/no-auto-commit.md @@ -0,0 +1,40 @@ +--- +trigger: always_on +description: Never commit or push without explicit user approval. +--- + +# No Auto-Commit Rule + +## Rule + +**NEVER run `git commit` or `git push` without explicit user approval.** + +Before committing: + +1. Show the planned commit(s): message, staged files, diff summary +2. Wait for the user to confirm +3. Only then execute `git commit` + +## Exception: `$monitor-ci` Skill + +When the `$monitor-ci` skill is active (CI self-healing workflow), autonomous commits and pushes are permitted for: + +- Applying self-healing fixes +- Retrying CI with empty commits +- Updating lockfiles for pre-CI failures + +The skill has its own git safety rules (never `git add -A`, stage only fix-related files). + +## Applies To + +- All AI assistants (Devin, Windsurf, Claude, etc.) +- Both interactive and background/subagent sessions + +## Why + +Commits are permanent project history. The user must review and approve: + +- What files are staged +- The commit message +- Whether changes should be split into multiple commits +- Whether to push after committing diff --git a/.agents/rules/nx/nx-circular-dependencies.md b/.agents/rules/nx/nx-circular-dependencies.md new file mode 100644 index 000000000..099f6e858 --- /dev/null +++ b/.agents/rules/nx/nx-circular-dependencies.md @@ -0,0 +1,90 @@ +--- +trigger: model_decision +description: Fix false circular dependencies in Nx caused by config files. Use .nxignore and ESLint ignores. +--- + +# Nx Circular Dependencies: Config File Exclusions + +## Problem + +Nx analyzes ALL TypeScript files for dependencies, including: + +- `*.config.ts` (tsdown, vitest, vite, etc.) +- `scripts/` directory +- `e2e/` tests + +Config files often import from parent projects (e.g., `../../tsdown.config.ts` for shared base config), which creates **false circular dependencies** in the project graph. + +## Solution + +Exclude non-runtime files from **both** Nx and ESLint analysis. + +### 1. Create `.nxignore` + +```gitignore +# Ignore scripts from Nx source file analysis +scripts/ + +# Ignore e2e tests at root level +e2e/ + +# Ignore all config files from dependency analysis +*.config.ts +*.config.js +*.config.mjs +**/*.config.ts +**/*.config.js +**/*.config.mjs +``` + +### 2. Update `eslint.config.mjs` + +Add to the `ignores` array: + +```javascript +{ + ignores: [ + '**/dist', + // Exclude from dependency analysis + 'scripts/**', + 'e2e/**', + ], +}, +``` + +### 3. After Changes + +Run `nx reset` in the workspace root to clear cached graph: + +```bash +npx nx reset +``` + +Then restart ESLint server in your IDE. + +## Why Two Files? + +- **`.nxignore`**: Affects Nx project graph (build ordering, affected detection) +- **`eslint.config.mjs`**: Affects `@nx/enforce-module-boundaries` lint rule + +Both use separate dependency analysis, so both need configuration. + +## Common Patterns That Cause False Cycles + +| Pattern | Why It's False | +| ----------------------------------------------------- | ----------------------------------- | +| `tsdown.config.ts` importing `../../tsdown.config.ts` | Shared build config, not runtime | +| `scripts/fetch-fixtures.ts` importing `@pkg/client` | Dev tooling, not package dependency | +| `vitest.config.ts` importing test utilities | Test config, not runtime | + +## Verification + +After applying fixes, verify with: + +```bash +npx nx reset +npx nx graph --file=tmp/graph.json +cat tmp/graph.json | jq '.graph.dependencies["your-package"]' +``` + +The root project should have `[]` (no dependencies) if scripts are properly excluded. diff --git a/.agents/rules/nx/nx-monorepo-setup.md b/.agents/rules/nx/nx-monorepo-setup.md new file mode 100644 index 000000000..45cdf75dc --- /dev/null +++ b/.agents/rules/nx/nx-monorepo-setup.md @@ -0,0 +1,170 @@ +--- +trigger: model_decision +description: Nx monorepo setup rules - package creation workflow, config templates, plugin inference, import conventions. +--- + +# Nx Monorepo Setup Rules + +## Quick Reference + +### Core Commands + +```bash +bunx nx build [package] # Build (inferred from tsdown.config.ts) +bunx nx test [package] # Test (inferred from vitest.config.ts) +bunx nx test:coverage [package] # Test with coverage +bunx nx typecheck [package] # Typecheck (inferred from tsconfig.json) +bunx nx lint [package] # Lint (inferred from eslint config) +``` + +### Package Creation Workflow + +**Step 1: Generate Base Package** + +```bash +bunx nx g @nx/js:lib \ + --name=[library-name] \ + --directory=packages/[name] \ + --importPath=@abapify/[library-name] \ + --bundler=none \ + --unitTestRunner=none \ + --linter=eslint +``` + +**Step 2: Add Config Files (Plugins Will Infer Targets)** + +Create these files and Nx plugins will automatically create build/test targets: + +``` +packages/[name]/ +├── src/ +│ └── index.ts +├── package.json +├── tsconfig.json +├── tsdown.config.ts # ← nx-tsdown plugin detects → creates 'build' target +└── vitest.config.ts # ← nx-vitest plugin detects → creates 'test' targets +``` + +**Step 3: Minimal project.json** + +```json +{ + "name": "[package-name]", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/[name]/src", + "projectType": "library", + "tags": ["type:library"], + "targets": { + "nx-release-publish": { + "options": { "packageRoot": "dist/{projectRoot}" } + } + } +} +``` + +**That's it\!** Don't declare `build`, `test`, or `typecheck` targets - they're inferred automatically. + +## Config File Templates + +### tsdown.config.ts + +```typescript +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, +}); +``` + +### vitest.config.ts + +```typescript +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + all: true, + include: ['src/**/*.ts'], + exclude: ['src/**/*.test.ts', 'src/**/*.spec.ts'], + }, + }, +}); +``` + +**Important**: The root `vitest.config.ts` must include your package in the `projects` array for test targets to be inferred. + +## Technology Stack + +- **Language**: TypeScript (ES2015+, strict mode) +- **Build**: tsdown (via nx-tsdown plugin inference) +- **Testing**: Vitest (via nx-vitest plugin inference) +- **Linting**: ESLint (via @nx/eslint plugin) +- **Package Manager**: bun + +## Plugin Package Creation + +For packages in `packages/plugins/`: + +```bash +bunx nx g @nx/js:lib \ + --name=custom-format \ + --directory=packages/plugins/custom-format \ + --importPath=@abapify/custom-format \ + --bundler=none \ + --unitTestRunner=none \ + --linter=eslint +``` + +Then add `tsdown.config.ts` and `vitest.config.ts` - targets will be inferred automatically. + +## Common Mistakes + +### ❌ DON'T: Declare build/test targets manually + +```json +{ + "targets": { + "build": { + "executor": "@abapify/nx-tsdown:build" // ❌ NO SUCH EXECUTOR\! + } + } +} +``` + +### ✅ DO: Let plugins infer targets + +Just create `tsdown.config.ts` and the nx-tsdown plugin will automatically create the `build` target. + +### Verification + +Check what targets were inferred: + +```bash +bunx nx show project [package-name] --json | jq '.targets | keys' +``` + +You should see: `["build", "lint", "nx-release-publish", "test", "test:coverage", "test:watch", "typecheck"]` + +## Import Rules + +> See [coding-conventions](../development/coding-conventions.md) and [bundler-imports](../development/bundler-imports.md) for full details. + +- **Cross-package**: `@abapify/[package-name]` +- **Internal files**: `../relative/path` (extensionless) +- **Workspace deps**: `workspace:*` + +## File Organization + +- **Source code**: `packages/[name]/src/` +- **Temporary files**: `tmp/` — see [tmp-folder-testing](../development/tmp-folder-testing.md) +- **Build output**: `packages/[name]/dist/` +- **Tests**: Co-located with source (`*.test.ts`) diff --git a/.agents/rules/openspec/project-planning-memory.md b/.agents/rules/openspec/project-planning-memory.md new file mode 100644 index 000000000..17a191d80 --- /dev/null +++ b/.agents/rules/openspec/project-planning-memory.md @@ -0,0 +1,45 @@ +--- +trigger: always_on +description: OpenSpec-based project planning. Check openspec/specs/ and openspec/changes/ before making changes. +--- + +# Project Planning and Memory Persistence Rule + +## Planning System + +This workspace uses [OpenSpec](https://openspec.dev/) for project planning and specification management. + +## Key Locations + +- `openspec/specs/` — Source of truth for system specifications (organized by domain) +- `openspec/changes/` — Active proposed changes (one folder per change) +- `openspec/config.yaml` — Project configuration and AI context +- `docs/planning/` — Roadmap, sprint tracking, and project coordination + +## AI Assistant Workflow + +> For the critical review process and spec change severity rules, see [spec-first-then-code](spec-first-then-code.md). + +1. **Check `openspec/specs/`** for existing specifications before making changes +2. **Check `openspec/changes/`** for active change proposals to understand context +3. **Use `/opsx:propose`** to create new change proposals with specs, design, and tasks +4. **Use `/opsx:apply`** to implement tasks from a change proposal +5. **Use `/opsx:archive`** to finalize completed changes and merge specs +6. **Sync with GitHub Issues** using label-based status tracking + +## GitHub Integration + +- GitHub Issues: External collaboration and detailed requirements +- OpenSpec Changes: Structured change proposals with specs and tasks +- Status sync through labels: status:ready → status:in-progress → status:review → status:done + +## Memory Persistence + +The `openspec/` directory serves as persistent project memory that survives: + +- Container rebuilds +- Repository clones +- Session changes +- Team member transitions + +This ensures consistent project understanding across all contributors and AI assistants. diff --git a/.agents/rules/openspec/spec-first-then-code.md b/.agents/rules/openspec/spec-first-then-code.md new file mode 100644 index 000000000..05fb5aeaf --- /dev/null +++ b/.agents/rules/openspec/spec-first-then-code.md @@ -0,0 +1,37 @@ +--- +trigger: model_decision +description: When making any changes for any of spec-driven packages +--- + +# Specification-Driven Development Rule (OpenSpec) + +## Core Philosophy + +> For key locations and memory persistence, see [project-planning-memory](project-planning-memory.md). + +- Specifications are design contracts (stable, versioned, change-resistant) +- Documentation describes implementation (living, refactorable) +- Specs define WHAT and WHY before coding HOW +- This project uses [OpenSpec](https://openspec.dev/) for spec management + +## Critical Review Process + +- **Additions**: New features can be added to specs with standard review +- **Modifications**: Changes to existing specs require critical review and strong justification +- **Breaking Changes**: Must introduce new spec version, never modify existing version +- **AI Assistant Rule**: Be extremely critical of any proposed spec changes vs additions + +## Workflow + +1. Check existing specs in `openspec/specs/` before any code changes +2. To propose a new change, use `/opsx:propose "description"` +3. Negotiate spec updates FIRST if changes conflict with specs +4. Never implement code that contradicts existing specifications +5. After implementation, archive the change with `/opsx:archive` + +## Spec Domains + +- `openspec/specs/adk/` — ADK (ABAP Development Kit, TS native way to work with ABAP code and other objects) +- `openspec/specs/adt-cli/` — ADT CLI (alternative to piper CLI solution to operate with ABAP backend) +- `openspec/specs/adt-client/` — ADT Client (abstracted ADT API communication) +- `openspec/specs/cicd/` — CI/CD pipeline architecture diff --git a/.agents/rules/verification/after-changes.md b/.agents/rules/verification/after-changes.md new file mode 100644 index 000000000..6b617afcb --- /dev/null +++ b/.agents/rules/verification/after-changes.md @@ -0,0 +1,26 @@ +--- +trigger: always_on +description: Verification checklist after making changes. Build, typecheck, test, lint, format. +--- + +# After Making Changes + +**NEVER tell the user to "try it" or "run it again" — verify it yourself first.** +If you changed code, YOU must build and test before declaring it done. + +```bash +bunx nx show projects # list valid project names for +bunx nx build # replace with one of the listed projects (e.g., `bunx nx build api`) +bunx nx typecheck # full type check +bunx nx test # run tests for a specific Nx project (replace with e.g. my-app); for all projects use: bunx nx run-many -t test --all +bunx nx lint # fix lint issues +bunx nx format:write # REQUIRED before every commit — format all files with Prettier +bunx nx format:check # REQUIRED before push — CI runs full-tree check, not just staged files +``` + +**Hook gap:** + +- Husky runs `lint-staged`, so Prettier runs on **staged** paths only. +- Husky does **not** run `package.json` `precommit` (`nx format:check --uncommitted`). +- Commits that touch only `.toml` or other non-staged files can skip Prettier entirely. +- **GitHub Copilot Autofix** commits bypass local Husky; after pulling review autofix commits, always run `bunx nx format:check` locally before pushing. diff --git a/.agents/skills/add-endpoint/SKILL.md b/.agents/skills/add-endpoint/SKILL.md new file mode 100644 index 000000000..d70b11e15 --- /dev/null +++ b/.agents/skills/add-endpoint/SKILL.md @@ -0,0 +1,471 @@ +--- +name: add-endpoint +description: Add a new ADT endpoint call (schema + contract + fixture). USE WHEN the user wants to add support for a new SAP ADT REST endpoint, add a new schema, or add a new API contract. Trigger words - new endpoint, add endpoint, new schema, add contract, support endpoint, SAP ADT API. +--- + +# Add a New ADT Endpoint + +This skill guides adding support for a new SAP ADT REST endpoint. It covers schema definition, contract creation, fixture registration, and integration into the contract index. + +## Overview of the Layers + +``` +SAP ADT endpoint (HTTP) + ↓ +adt-schemas packages/adt-schemas/src/schemas/generated/ + • Schema literal (TypeScript `as const` object from XSD) + • Typed wrapper in typed.ts + ↓ +adt-contracts packages/adt-contracts/src/ + • generated/schemas.ts (toSpeciSchema wrapper) + • adt/{module}/{file}.ts (contract definition) + • adt/{module}/index.ts (module exports + aggregate) + ↓ +adt-fixtures packages/adt-fixtures/src/ + • fixtures/{module}/{name}.xml (real SAP XML sample) + • fixtures/registry.ts (path registration) +``` + +--- + +## Step 1: Gather Endpoint Information + +Before writing any code, obtain the endpoint's HTTP details and XML payload structure. + +> **Tip:** If the endpoint is undocumented or you need to research it first, use `$adt-reverse-engineering` for systematic discovery via live system, open source projects, and Sourcegraph code search. + +### Option A: Live System Available + +If you have a running SAP system connection: + +1. Make a real HTTP request to the endpoint and capture the response XML: + ```bash + # Example using curl or adt-cli: + # GET /sap/bc/adt/{path}/{objectname} + ``` +2. Save the raw response to `tmp/` for reference (see [tmp-folder-testing](../../rules/development/tmp-folder-testing.md)): + ```bash + # adt get ZOBJECT_NAME -o tmp/response.xml + ``` +3. Use the actual XML to design the schema and write the fixture. + +### Option B: No Live System — Web Research + +If no system connection is available, research the endpoint: + +1. **Search SAP ADT documentation and community resources:** + - SAP Help Portal: https://help.sap.com/docs/abap-cloud/abap-development-tools-user-guide + - abapGit source (has XSD/schema reference implementations): https://github.com/abapGit/abapGit + - SAP ADT API examples: search GitHub for `sap/bc/adt/{your-path}` + - Eclipse ADT plugin source: https://github.com/SAP/abap-adt-api + +2. **Search for XSD schema definitions:** + - Look in the existing XSD files under `packages/adt-schemas/` for similar patterns + - Search the abapGit repository for matching serializer names or object types + +3. **Look at similar existing schemas** in `packages/adt-schemas/src/schemas/generated/schemas/sap/` for structural patterns. + +4. **Construct a representative XML fixture** based on research, clearly marked as synthesized: + ```xml + + + + ``` + +--- + +## Step 2: Create the Schema in adt-schemas + +Schemas live in `packages/adt-schemas/src/schemas/generated/schemas/`. + +### 2a: Add the schema literal + +Create `packages/adt-schemas/src/schemas/generated/schemas/sap/{schemaName}.ts`: + +> **Note on the `Source:` comment:** Existing generated files reference `xsd/sap/{name}.xsd` pointing to XSD sources in `packages/adt-schemas/.xsd/`. For manually created schemas (derived from a live system response or research), use a descriptive comment instead. + +```typescript +/** + * Schema for {description} + * + * Manually created — derived from live system response / research + * Reference: GET /sap/bc/adt/{your-path} + */ + +// Import dependencies (only schemas you actually use) +import adtcore from './adtcore'; + +export default { + $xmlns: { + adtcore: 'http://www.sap.com/adt/core', + xsd: 'http://www.w3.org/2001/XMLSchema', + myns: 'http://www.sap.com/adt/my-namespace', // ← your namespace + }, + $imports: [adtcore], + targetNamespace: 'http://www.sap.com/adt/my-namespace', + attributeFormDefault: 'qualified', + elementFormDefault: 'qualified', + element: [ + { + name: 'myRootElement', // ← root element name (camelCase) + type: 'myns:MyRootType', + }, + ], + complexType: [ + { + name: 'MyRootType', + complexContent: { + extension: { + base: 'adtcore:AdtMainObject', // ← extend appropriate base type + sequence: { + element: [ + { + name: 'myChild', + type: 'xsd:string', + minOccurs: '0', + }, + ], + }, + attribute: [ + { + name: 'myAttr', + type: 'xsd:string', + }, + ], + }, + }, + }, + ], +} as const; +``` + +**Key rules for schema literals:** + +> See [xsd-best-practices](../../rules/adt/xsd-best-practices.md) for XSD validity rules and edge cases. + +- Must end with `} as const` +- One root element per schema document +- Use `$imports` for schemas you depend on (adtcore, abapoo, abapsource, etc.) +- Names follow XSD conventions: `complexType` names are PascalCase +- Look at existing schemas (e.g. `interfaces.ts`, `packagesV1.ts`) for common base types +- Existing schemas were auto-generated from XSD sources in `packages/adt-schemas/.xsd/`; manually created schemas should be documented accordingly (see [file-lifecycle](../../rules/development/file-lifecycle.md)) + +### 2b: Add the typed wrapper in typed.ts + +Edit `packages/adt-schemas/src/schemas/generated/typed.ts` and add your schema: + +```typescript +// Add the type import at the top with other imports: +import type { MySchemaNameSchema } from './types/sap/mySchemaName.types'; + +// Add the raw schema import and typed export in the SAP schemas section: +import _mySchemaName from './schemas/sap/mySchemaName'; +export const mySchemaName: TypedSchema = + typedSchema(_mySchemaName); +``` + +> **Note on types**: The `types/` files are auto-generated from XSD schemas by the codegen tool. For manually created schemas, you can either: +> +> - Run `npx nx build adt-schemas` to trigger codegen (if XSD source exists) +> - Or manually create a minimal types file at `packages/adt-schemas/src/schemas/generated/types/sap/mySchemaName.types.ts` following the pattern of existing types files. + +--- + +## Step 3: Add the Schema to adt-contracts + +Edit `packages/adt-contracts/src/generated/schemas.ts` to add the `toSpeciSchema` wrapper: + +```typescript +// Add in alphabetical order with existing entries: +export const mySchemaName = toSpeciSchema(adtSchemas.mySchemaName); +``` + +This wraps the typed schema to make it speci-compatible (adds `_infer` property for response type inference). + +--- + +## Step 4: Create the Contract + +Contracts live in `packages/adt-contracts/src/adt/{module}/`. Choose the appropriate module or create a new one. + +### Simple GET contract (non-CRUD, like packages): + +Create `packages/adt-contracts/src/adt/{module}/{name}.ts`: + +```typescript +/** + * ADT {Module} - {Description} Contract + * + * Endpoint: /sap/bc/adt/{your-path} + * {description of what this endpoint does} + */ + +import { http } from '../../base'; +import { mySchemaName, type InferTypedSchema } from '../../schemas'; + +/** + * Response type for consumers (ADK, etc.) + */ +export type MyObjectResponse = InferTypedSchema; + +export const myObjectContract = { + /** + * Get {object description} + * + * @param name - Object name (will be URL-encoded) + * @returns {what is returned} + * + * Note: SAP ADT URLs use lowercase object names (ABAP objects are case-insensitive + * in URLs, and SAP conventionally normalizes to lowercase in the URL path). + */ + get: (name: string) => + http.get( + `/sap/bc/adt/{your-path}/${encodeURIComponent(name.toLowerCase())}`, + { + responses: { 200: mySchemaName }, + headers: { Accept: 'application/vnd.sap.adt.{mimetype}.v1+xml' }, + }, + ), +}; + +export type MyObjectContract = typeof myObjectContract; +``` + +### Full CRUD contract (like classes, interfaces): + +```typescript +/** + * ADT {Module} Contract + * + * Endpoint: /sap/bc/adt/{your-path} + */ + +import { crud } from '../../base'; +import { mySchemaName, type InferTypedSchema } from '../../schemas'; + +export type MyObjectResponse = InferTypedSchema; + +/** + * Full CRUD operations for {object type} + * + * Includes: get, post, put, delete, lock, unlock, objectstructure + * Sources: source.main.get/put + */ +export const myObjectContract = crud({ + basePath: '/sap/bc/adt/{your-path}', + schema: mySchemaName, + contentType: 'application/vnd.sap.adt.{mimetype}.v1+xml', + sources: ['main'] as const, + // For objects with multiple includes: + // includes: ['definitions', 'implementations', 'macros'] as const, +}); + +export type MyObjectContractType = typeof myObjectContract; +``` + +### Create or update the module index + +If adding to an existing module (e.g., `oo`), edit `packages/adt-contracts/src/adt/{module}/index.ts`: + +```typescript +// Re-export new contract +export { + myObjectContract, + type MyObjectContractType, + type MyObjectResponse, +} from './myObject'; + +// Import for aggregated contract +import { myObjectContract } from './myObject'; + +// Update the module interface: +export interface OoContract { + classes: typeof classesContract; + interfaces: typeof interfacesContract; + myObject: typeof myObjectContract; // ← add here +} + +// Update the module aggregate: +export const ooContract: OoContract = { + classes: classesContract, + interfaces: interfacesContract, + myObject: myObjectContract, // ← add here +}; +``` + +If creating a new module, also register it in `packages/adt-contracts/src/adt/index.ts`: + +```typescript +// Add export +export * from './myModule'; + +// Import for aggregate +import { myModuleContract, type MyModuleContract } from './myModule'; + +// Add to AdtContract interface +export interface AdtContract { + // ... existing modules ... + myModule: MyModuleContract; +} + +// Add to adtContract object +export const adtContract: AdtContract = { + // ... existing modules ... + myModule: myModuleContract, +}; +``` + +--- + +## Step 5: Add a Fixture + +Fixtures are real (or representative) SAP XML responses used in tests. + +### 5a: Create the fixture file + +Create `packages/adt-fixtures/src/fixtures/{module}/{name}.xml`: + +```xml + + + + + +``` + +**Tips for realistic fixtures:** + +- Use `ZTEST_` or `$TMP` names (standard test object convention in SAP) +- Include all namespace declarations (`xmlns:`) +- Match the exact XML structure the schema expects +- Include representative values (not empty) + +### 5b: Register the fixture + +Edit `packages/adt-fixtures/src/fixtures/registry.ts`: + +```typescript +export const registry = { + // ... existing entries ... + myModule: { + myObject: 'myModule/myObject.xml', + }, + // Or add to existing group: + oo: { + class: 'oo/class.xml', + interface: 'oo/interface.xml', + myObject: 'oo/myObject.xml', // ← add here + }, +} as const; +``` + +--- + +## Step 6: Build and Verify + +> Follow the verification checklist in [after-changes](../../rules/verification/after-changes.md). + +```bash +# Build affected packages in dependency order +bunx nx build adt-schemas +bunx nx build adt-contracts +bunx nx build adt-fixtures + +# Type check, test, lint +bunx nx typecheck +bunx nx test adt-schemas adt-contracts +bunx nx lint +``` + +### Quick smoke test (optional) + +Add a minimal test alongside your contract to verify parsing works: + +```typescript +// packages/adt-schemas/src/schemas/generated/schemas/sap/__tests__/mySchemaName.test.ts +import { describe, it, expect } from 'vitest'; +import { fixtures } from '@abapify/adt-fixtures'; +import { mySchemaName } from '../..'; + +describe('mySchemaName schema', () => { + it('parses fixture XML', async () => { + const xml = await fixtures.myModule.myObject.load(); + const data = mySchemaName.parse(xml); + expect(data).toBeDefined(); + // Check a specific field: + // expect(data.myRootElement?.['adtcore:name']).toBe('ZTEST_OBJECT'); + }); +}); +``` + +--- + +## Common Patterns Reference + +### Base types to extend (from adtcore.ts) + +| Base type | Purpose | +| ----------------------------- | ---------------------------------------------------------- | +| `adtcore:AdtObject` | Any ADT object (name, type, description, version, links) | +| `adtcore:AdtMainObject` | Repository object (+ package, responsible, masterLanguage) | +| `abapoo:AbapOoObject` | OO base (+ modeled, syntaxConfiguration) | +| `abapsource:AbapSourceObject` | Source-enabled object (+ sourceUri, fixPointArithmetic) | + +### Namespace URIs (for `$xmlns`) + +| Prefix | URI | +| ------------ | ----------------------------------- | +| `adtcore` | `http://www.sap.com/adt/core` | +| `abapsource` | `http://www.sap.com/adt/abapsource` | +| `abapoo` | `http://www.sap.com/adt/oo` | +| `atom` | `http://www.w3.org/2005/Atom` | +| `xsd` | `http://www.w3.org/2001/XMLSchema` | + +### Content-Type patterns for SAP ADT + +``` +application/vnd.sap.adt.oo.classes.v4+xml +application/vnd.sap.adt.oo.interfaces.v5+xml +application/vnd.sap.adt.packages.v1+xml +application/vnd.sap.adt.{object-type}.v{version}+xml +``` + +### URL patterns for SAP ADT + +``` +/sap/bc/adt/oo/classes/{name} → OO classes +/sap/bc/adt/oo/interfaces/{name} → OO interfaces +/sap/bc/adt/packages/{name} → Packages +/sap/bc/adt/cts/transportrequests/{id} → Transport requests +/sap/bc/adt/programs/programs/{name} → Programs +/sap/bc/adt/ddic/tables/{name} → Database tables +/sap/bc/adt/ddic/domains/{name} → Domains +/sap/bc/adt/ddic/dataelements/{name} → Data elements +/sap/bc/adt/ddic/structures/{name} → Structures +/sap/bc/adt/functions/groups/{name} → Function groups +``` + +--- + +## Checklist + +- [ ] Endpoint details gathered (live system or web research) +- [ ] Schema literal created in `adt-schemas/src/schemas/generated/schemas/sap/` +- [ ] Typed wrapper added to `adt-schemas/src/schemas/generated/typed.ts` +- [ ] `toSpeciSchema` export added to `adt-contracts/src/generated/schemas.ts` +- [ ] Contract created in `adt-contracts/src/adt/{module}/` +- [ ] Module index updated (or new module registered in main `adt/index.ts`) +- [ ] Fixture XML created in `adt-fixtures/src/fixtures/{module}/` +- [ ] Fixture registered in `adt-fixtures/src/fixtures/registry.ts` +- [ ] `bunx nx build adt-schemas adt-contracts adt-fixtures` passes +- [ ] `bunx nx typecheck` passes +- [ ] `bunx nx test adt-schemas adt-contracts` passes diff --git a/.agents/skills/add-object-type/SKILL.md b/.agents/skills/add-object-type/SKILL.md new file mode 100644 index 000000000..ddd66acfb --- /dev/null +++ b/.agents/skills/add-object-type/SKILL.md @@ -0,0 +1,880 @@ +--- +name: add-object-type +description: Add a new ABAP object type with full ADK model and abapGit serialization support. USE WHEN adding a new ABAP type (PROG, FUGR, TABL, DOMA, DTEL, MSAG, etc.), ADK support, or abapGit handler. Trigger words - new object type, add object type, support PROG, add abapGit handler. +--- + +# Add a New ABAP Object Type + +This skill guides adding full support for a new ABAP object type. It covers all layers: ADT endpoint (schema + contract + fixture), ADK model, and abapGit serialization handler. + +## Overview of All Layers + +``` +ABAP Object Type (e.g., PROG/P, FUGR/F, TABL/DT) + ↓ +adt-schemas Schema literal + typed wrapper +adt-contracts Contract (CRUD or simple GET) for the ADT endpoint +adt-fixtures XML fixture for tests + ↓ +adk ADK kind constant + object model class + ↓ +adt-plugin-abapgit abapGit schema literal + TypeScript types + handler +``` + +--- + +## Step 1: Gather Object Type Information + +Before writing code, gather details about the object type from live system or research. + +> **Tip:** For undocumented object types, use `$adt-reverse-engineering` first to systematically discover endpoint details, XML structure, and content-types via live system discovery, open source projects, and Sourcegraph code search. + +### Information to collect + +| Item | Example (for PROG) | Where to find | +| ------------------------ | -------------------------------------------------- | ----------------------------- | +| ADT object type | `PROG/P` | SAP ADT documentation | +| ADT endpoint path | `/sap/bc/adt/programs/programs` | Live system or community | +| ADT content-type | `application/vnd.sap.adt.programs.programs.v2+xml` | Live system response headers | +| ADT XML namespace | `http://www.sap.com/adt/programs` | Live system XML | +| ADT root element | `abapProgram` | Live system XML | +| abapGit serializer | `LCL_OBJECT_PROG` | abapGit source | +| abapGit main table | `TRDIR` | abapGit source | +| abapGit structure fields | `NAME, SECU, EDTX, ...` | SAP DDIC / abapGit XML sample | +| Has source code | Yes (`.abap` file) | abapGit repo samples | + +### Option A: Live System Available + +```bash +# Capture the ADT XML response: +# GET /sap/bc/adt/{path}/{objectname} +# → save to tmp/response.xml + +# Capture a real abapGit XML file: +# Check a .prog.xml file from a real abapGit repository +``` + +### Option B: No Live System — Web Research + +1. **ADT endpoint details:** + - SAP Help Portal: https://help.sap.com/docs/abap-cloud/abap-development-tools-user-guide + - Search GitHub for `sap/bc/adt/{path}` to find client implementations + - Look at `packages/adt-schemas/src/schemas/generated/schemas/sap/` for existing patterns + +2. **abapGit serializer and schema:** + - abapGit repository: https://github.com/abapGit/abapGit + - Look in `src/objects/` for the relevant handler class (`ZCL_ABAPGIT_OBJECT_{TYPE}`) + - Find `.abap` or `.xml` sample files in abapGit test fixtures + - Search abapGit issues/PRs for the object type + +3. **SAP DDIC table structure:** + - abapGit XML files reveal the exact fields used + - SAP community/SE11 screenshots often available online + +--- + +## Step 2: Create the ADT Schema and Contract + +Follow the full **$add-endpoint** skill for this step. Here is a summary specific to object types: + +> **Rules:** See [xsd-best-practices](../../rules/adt/xsd-best-practices.md) for XSD validity, [file-lifecycle](../../rules/development/file-lifecycle.md) for generated vs hand-maintained files, and [adt-ddic-mapping](../../rules/adt/adt-ddic-mapping.md) for DDIC object specifics. + +### Schema (adt-schemas) + +Create `packages/adt-schemas/src/schemas/generated/schemas/sap/{schemaName}.ts`: + +```typescript +/** + * Schema for {description} + * + * Manually created — derived from live system response / research + * Reference: GET /sap/bc/adt/{your-path} + * (XSD source files for adt-schemas live in packages/adt-schemas/.xsd/) + */ + +import adtcore from './adtcore'; +import abapsource from './abapsource'; + +export default { + $xmlns: { + adtcore: 'http://www.sap.com/adt/core', + abapsource: 'http://www.sap.com/adt/abapsource', + xsd: 'http://www.w3.org/2001/XMLSchema', + prog: 'http://www.sap.com/adt/programs', // ← your namespace + }, + $imports: [adtcore, abapsource], + targetNamespace: 'http://www.sap.com/adt/programs', + attributeFormDefault: 'qualified', + elementFormDefault: 'qualified', + element: [ + { + name: 'abapProgram', // ← root element name + type: 'prog:AbapProgram', + }, + ], + complexType: [ + { + name: 'AbapProgram', + complexContent: { + extension: { + base: 'abapsource:AbapSourceMainObject', + attribute: [ + { + name: 'programType', + type: 'xsd:string', + }, + ], + }, + }, + }, + ], +} as const; +``` + +Then add to `packages/adt-schemas/src/schemas/generated/typed.ts`: + +```typescript +import type { AbapProgramSchema } from './types/sap/abapProgram.types'; +import _abapProgram from './schemas/sap/abapProgram'; +export const abapProgram: TypedSchema = + typedSchema(_abapProgram); +``` + +And add to `packages/adt-contracts/src/generated/schemas.ts`: + +```typescript +export const abapProgram = toSpeciSchema(adtSchemas.abapProgram); +``` + +### Contract (adt-contracts) + +Create `packages/adt-contracts/src/adt/{module}/{name}.ts`: + +```typescript +/** + * ADT Programs Contract + * Endpoint: /sap/bc/adt/programs/programs + */ + +import { crud } from '../../base'; +import { abapProgram, type InferTypedSchema } from '../../schemas'; + +export type ProgramResponse = InferTypedSchema; + +export const programsContract = crud({ + basePath: '/sap/bc/adt/programs/programs', + schema: abapProgram, + contentType: 'application/vnd.sap.adt.programs.programs.v2+xml', + sources: ['main'] as const, +}); + +export type ProgramsContract = typeof programsContract; +``` + +Register in the module index (create new module or add to existing). + +### Fixture (adt-fixtures) + +Create `packages/adt-fixtures/src/fixtures/{module}/{name}.xml` and register in `registry.ts`. + +--- + +## Step 3: Add ADK Kind + +Edit `packages/adk/src/base/kinds.ts` to add the new kind constant: + +```typescript +// Repository Objects — add new repository (ABAP) object types here. +// CTS objects (transport, tasks) go in a different section. +export const Package = 'Package' as const; +export const Class = 'Class' as const; +export const Interface = 'Interface' as const; +export const Program = 'Program' as const; // ← add your kind here, alphabetically + +// Update the AdkKind union type: +export type AdkKind = + | typeof TransportRequest + | typeof TransportTask + | typeof Package + | typeof Class + | typeof Interface + | typeof Program; // ← add here +// ... +``` + +--- + +## Step 4: Create the ADK Object Model + +Create the directory and files for the new object type: + +``` +packages/adk/src/objects/repository/{typeLower}/ +├── {typeLower}.model.ts ← ADK class +├── {typeLower}.types.ts ← Type definitions +└── index.ts ← Re-exports +``` + +### {typeLower}.types.ts + +```typescript +/** + * PROG - ABAP Program + * Type definitions + */ + +// Source types for this object (if it has multiple includes) +export type ProgramIncludeType = 'main'; +// For objects with multiple includes, add more types: +// export type ClassIncludeType = 'main' | 'definitions' | 'implementations' | 'testclasses'; +``` + +### {typeLower}.model.ts + +```typescript +/** + * PROG - ABAP Program + * + * ADK object for ABAP programs (PROG). + */ + +import { AdkMainObject } from '../../../base/model'; +import { Program as ProgramKind } from '../../../base/kinds'; +import { getGlobalContext } from '../../../base/global-context'; +import type { AdkContext } from '../../../base/context'; + +// Import response type from ADT integration layer +import type { ProgramResponse } from '../../../base/adt'; + +/** + * Program data type - unwrap from root element + */ +export type ProgramXml = ProgramResponse['abapProgram']; + +/** + * ADK Program object + */ +export class AdkProgram extends AdkMainObject { + static readonly kind = ProgramKind; + readonly kind = AdkProgram.kind; + + // ADT object URI + get objectUri(): string { + return `/sap/bc/adt/programs/programs/${encodeURIComponent(this.name.toLowerCase())}`; + } + + // Source code access + async getSource(): Promise { + return this.lazy('source', async () => { + return this.crudContract.source.main.get(this.name); + }); + } + + // ============================================ + // CRUD contract config - enables save() + // ============================================ + + protected override get wrapperKey() { + return 'abapProgram'; + } + // Note: `any` return type is intentional here — this is an established pattern + // in the ADK codebase (see clas.model.ts, intf.model.ts). The base class defines + // crudContract as `any` to support different contract structures per object type. + protected override get crudContract(): any { + return this.ctx.client.adt.programs.programs; // ← your module path + } + + // ============================================ + // Static Factory Method + // ============================================ + + static async get(name: string, ctx?: AdkContext): Promise { + const context = ctx ?? getGlobalContext(); + return new AdkProgram(context, name).load(); + } +} + +// Self-register with ADK registry +import { registerObjectType } from '../../../base/registry'; +registerObjectType('PROG', ProgramKind, AdkProgram); +``` + +**For objects with source code**, also implement the save lifecycle methods: + +```typescript + // ============================================ + // Source Save Lifecycle (required for source-based objects) + // ============================================ + + /** + * Save pending source (set via _pendingSource) + * Used by export workflow after deserialization from abapGit + */ + protected override async savePendingSources(options?: { + lockHandle?: string; + transport?: string; + }): Promise { + const pendingSource = (this as unknown as { _pendingSource?: string }) + ._pendingSource; + if (!pendingSource) return; + + await this.saveMainSource(pendingSource, options); + + // Clear pending source after save + delete (this as unknown as { _pendingSource?: string })._pendingSource; + } + + /** + * Pre-lock comparison: check if pending source matches SAP + * If identical, sets _unchanged = true so save() skips locking and PUT + */ + protected override async checkPendingSourcesUnchanged(): Promise { + const pendingSource = (this as unknown as { _pendingSource?: string }) + ._pendingSource; + if (!pendingSource) return; + + try { + const currentSource = await this.getSource(); + if (this.normalizeSource(currentSource) === this.normalizeSource(pendingSource)) { + this._unchanged = true; + delete (this as unknown as { _pendingSource?: string })._pendingSource; + } + } catch { + // Source doesn't exist on SAP (404) — needs saving + } + } + + private normalizeSource(source: string): string { + return source.replace(/\s+$/gm, '').trimEnd(); + } + + protected override hasPendingSources(): boolean { + return !!(this as unknown as { _pendingSource?: string })._pendingSource; + } +``` + +**IMPORTANT:** Both `savePendingSources()` AND `checkPendingSourcesUnchanged()` must be implemented. + +> See [adk-save-logic](../../rules/adt/adk-save-logic.md) for edge cases around upsert, lock failures (405), and 422 "already exists" handling. + +- `checkPendingSourcesUnchanged()` runs **before** lock — compares pending source with SAP, sets `_unchanged = true` if identical +- `savePendingSources()` runs **after** lock — does the actual PUT +- Without `checkPendingSourcesUnchanged()`, unchanged objects will still be locked and PUT unnecessarily +- See `AdkClass` (clas.model.ts) for multi-include example, `AdkInterface` (intf.model.ts) for single-source example + +**Notes:** + +- The `wrapperKey` matches the root element name in the schema (e.g., `abapProgram`) +- The `crudContract` path must match where you registered the contract in `adtContract` (e.g., `ctx.client.adt.programs.programs`) +- `registerObjectType('PROG', ...)` uses the 4-letter ABAP type code (not the full `PROG/P`) + +### index.ts + +```typescript +export { AdkProgram } from './program.model'; +export type { ProgramXml } from './program.model'; +export type { ProgramIncludeType } from './program.types'; +``` + +### Update `packages/adk/src/base/adt.ts` + +This file is the ADT integration layer bridge. ADK object models import response types from here (via `../../../base/adt`) rather than directly from `@abapify/adt-client`. Add the response type re-export: + +```typescript +// Import raw type from adt-client: +import type { ProgramResponse as _ProgramResponse } from '@abapify/adt-client'; + +// When to use Extract vs direct re-export: +// +// USE Extract when the schema has multiple root elements and the contract response +// type is a union (e.g., ClassResponse = { abapClass } | { abapClassInclude }). +// Each ADK object only uses one variant of the union. +// → export type ProgramResponse = Extract<_ProgramResponse, { abapProgram: unknown }>; +// +// USE direct re-export when the schema has a single root element and the response +// type is NOT a union (e.g., InterfaceResponse = { abapInterface }). +// → export type { ProgramResponse } from '@abapify/adt-client'; +// +// To check: look at what crud() generates for your contract and see if the inferred +// response type is a union or a single object type. + +// Example - use whichever form is correct for your contract: +export type ProgramResponse = Extract< + _ProgramResponse, + { abapProgram: unknown } +>; +// OR: +// export type { ProgramResponse } from '@abapify/adt-client'; +``` + +Then in the model file, import as: + +```typescript +import type { ProgramResponse } from '../../../base/adt'; +``` + +### Update the ADK index + +Edit `packages/adk/src/index.ts` to export the new object: + +```typescript +export { AdkProgram } from './objects/repository/prog'; +export type { ProgramXml } from './objects/repository/prog'; +``` + +### Update `packages/adk/src/base/kinds.ts` KindToObject mapping + +```typescript +export type AdkObjectForKind = K extends typeof Class + ? AdkClass + : K extends typeof Interface + ? AdkInterface + : K extends typeof Package + ? AdkPackage + : K extends typeof Program + ? AdkProgram // ← add + : AdkObject; +``` + +--- + +## Step 5: Create the abapGit Schema + +The abapGit schema captures the structure of the XML file that abapGit writes for this object type. + +### 5a: Research the abapGit XML format + +Find out what fields the abapGit serializer writes. Resources: + +- **abapGit repository**: https://github.com/abapGit/abapGit — look in `src/objects/` for `ZCL_ABAPGIT_OBJECT_{TYPE}.clas.abap` +- Look for `CREATE_VSEO*`, `ZIF_ABAPGIT_OBJECT~DESERIALIZE`, or `SERIALIZE` methods +- Look at `.xml` files in abapGit test repositories +- Search for `{TYPE}` in abapGit's `docs/` directory + +A typical abapGit XML looks like: + +```xml + + + + + ZTEST_PROGRAM + S + X + 1 + + + + +``` + +### 5b: Create the abapGit schema literal + +Create `packages/adt-plugin-abapgit/src/schemas/generated/schemas/{type}.ts`: + +```typescript +/** + * Auto-generated schema from XSD + * + * DO NOT EDIT - Generated by ts-xsd codegen + * Source: abapgit/{type}.xsd + */ + +export default { + $xmlns: { + xs: 'http://www.w3.org/2001/XMLSchema', + asx: 'http://www.sap.com/abapxml', + }, + targetNamespace: 'http://www.sap.com/abapxml', + elementFormDefault: 'unqualified', + element: [ + { + name: 'abapGit', + complexType: { + sequence: { + element: [ + { + ref: 'asx:abap', + }, + ], + }, + attribute: [ + { + name: 'version', + type: 'xs:string', + use: 'required', + }, + { + name: 'serializer', + type: 'xs:string', + use: 'required', + }, + { + name: 'serializer_version', + type: 'xs:string', + use: 'required', + }, + ], + }, + }, + { + name: 'Schema', + abstract: true, + }, + { + name: 'abap', + type: 'asx:AbapType', + }, + ], + complexType: [ + { + name: 'AbapValuesType', + all: { + element: [ + { + name: 'TRDIR', // ← main SAP table + type: 'asx:TrdirType', + minOccurs: '0', + }, + ], + }, + }, + { + name: 'TrdirType', // ← table structure type + all: { + element: [ + { + name: 'NAME', + type: 'xs:string', + }, + { + name: 'SECU', + type: 'xs:string', + minOccurs: '0', + }, + { + name: 'EDTX', + type: 'xs:string', + minOccurs: '0', + }, + { + name: 'SUBC', + type: 'xs:string', + minOccurs: '0', + }, + // Add all relevant fields from abapGit XML + ], + }, + }, + { + name: 'AbapType', + sequence: { + element: [ + { + name: 'values', + type: 'asx:AbapValuesType', + }, + ], + }, + attribute: [ + { + name: 'version', + type: 'xs:string', + default: '1.0', + }, + ], + }, + ], +} as const; +``` + +**Pattern rules for abapGit schemas (follow existing schemas like `intf.ts`, `dtel.ts`):** + +- All fields optional (`minOccurs: '0'`) except primary key field +- Use `asx:` prefix for types in the `AbapValuesType` and type definitions +- Structure name matches SAP DDIC table/structure name (e.g., `TRDIR`, `VSEOCLASS`, `DD04V`) + +### 5c: Create TypeScript types for the abapGit schema + +Create `packages/adt-plugin-abapgit/src/schemas/generated/types/{type}.ts`: + +```typescript +/** + * Auto-generated TypeScript interfaces from XSD + * DO NOT EDIT - Generated by ts-xsd codegen + * Source: abapgit/{type}.xsd + * Mode: Flattened + */ + +export type ProgSchema = + | { + abapGit: { + abap: { + values: { + TRDIR?: { + NAME: string; + SECU?: string; + EDTX?: string; + SUBC?: string; + ABAP_LANGUAGE_VERSION?: string; + }; + }; + version?: string; + }; + version: string; + serializer: string; + serializer_version: string; + }; + } + | { + abap: { + values: { + TRDIR?: { + NAME: string; + SECU?: string; + EDTX?: string; + SUBC?: string; + ABAP_LANGUAGE_VERSION?: string; + }; + }; + version?: string; + }; + }; +``` + +**Pattern:** The type is always a union of two variants: + +1. `{ abapGit: { abap: { values: ... }, version, serializer, serializer_version } }` — full document +2. `{ abap: { values: ... } }` — inner abap fragment + +### 5d: Register the schema in the abapGit index + +Edit `packages/adt-plugin-abapgit/src/schemas/generated/index.ts`: + +```typescript +// Add raw schema import +import _prog from './schemas/prog'; + +// Add type imports +import type { ProgSchema as _ProgSchema } from './types/prog'; + +// Add extraction type +type ProgAbapGitType = Extract<_ProgSchema, { abapGit: unknown }>; + +// Add schema export +export const prog = abapGitSchema< + ProgAbapGitType, + ProgAbapGitType['abapGit']['abap']['values'] +>(_prog); +``` + +--- + +## Step 6: Create the abapGit Handler + +Create `packages/adt-plugin-abapgit/src/lib/handlers/objects/{type}.ts`: + +```typescript +/** + * Program (PROG) object handler for abapGit format + */ + +import { AdkProgram } from '../adk'; +import { prog } from '../../../schemas/generated'; +import { createHandler } from '../base'; + +export const programHandler = createHandler(AdkProgram, { + schema: prog, + version: 'v1.0.0', + serializer: 'LCL_OBJECT_PROG', // ← from abapGit source + serializer_version: 'v1.0.0', + + // SAP → Git: Map ADK object to abapGit values + toAbapGit: (prog) => ({ + TRDIR: { + NAME: prog.name ?? '', + SECU: 'S', + EDTX: 'X', + SUBC: '1', + }, + }), + + // Single source file + getSource: (prog) => prog.getSource(), + + // Git → SAP: Map abapGit values to ADK data + fromAbapGit: ({ TRDIR }) => ({ + name: (TRDIR?.NAME ?? '').toUpperCase(), + type: 'PROG/P', // ← full ADT type code + description: undefined, // TRDIR has no description field + }), + + // Git → SAP: Set source files on ADK object + // Note: `_pendingSource` and `_pendingSources` are not declared on the public + // ADK interface - they are implementation details for deferred source saving + // (used by the export/deploy workflow). The `as unknown as` double cast is + // the established pattern in this codebase (see clas.ts, intf.ts handlers). + setSources: (prog, sources) => { + if (sources.main) { + (prog as unknown as { _pendingSource: string })._pendingSource = + sources.main; + } + }, +}); +``` + +**For objects with multiple source files (like CLAS):** + +```typescript +// Multiple sources - use getSources instead of getSource +getSources: (obj) => + obj.includes.map((inc) => ({ + suffix: SUFFIX_MAP[inc.includeType], + content: obj.getIncludeSource(inc.includeType), + })), + +// And map suffixes during deserialization +suffixToSourceKey: { + 'locals_def': 'definitions', + 'locals_imp': 'implementations', + 'testclasses': 'testclasses', + 'macros': 'macros', +}, + +setSources: (obj, sources) => { + (obj as unknown as { _pendingSources: Record })._pendingSources = sources; + if (sources.main) { + (obj as unknown as { _pendingSource: string })._pendingSource = sources.main; + } +}, +``` + +**For objects without source (like DEVC):** + +- Do not provide `getSource` or `getSources` +- `setSources` is not needed +- Only XML file will be serialized + +### Register handler in the handlers index + +Edit `packages/adt-plugin-abapgit/src/lib/handlers/objects/index.ts`: + +```typescript +// Add the import (side effect: auto-registers on import) +export * from './clas'; +export * from './devc'; +export * from './doma'; +export * from './dtel'; +export * from './intf'; +export * from './prog'; // ← add here +``` + +--- + +## Step 7: Update adk.ts re-exports in abapGit plugin + +Edit `packages/adt-plugin-abapgit/src/lib/handlers/adk.ts` to re-export the new ADK class: + +```typescript +// Add export +export { AdkProgram } from '@abapify/adk'; +``` + +--- + +## Step 8: Build and Verify + +> Follow the verification checklist in [after-changes](../../rules/verification/after-changes.md). + +```bash +# Build all affected packages in dependency order +bunx nx build adt-schemas adt-contracts adk adt-plugin-abapgit adt-fixtures + +# Full type check, tests, lint +bunx nx typecheck +bunx nx test adt-schemas adt-contracts adk adt-plugin-abapgit +bunx nx lint +``` + +### Quick smoke tests + +**Schema parsing test:** + +```typescript +// packages/adt-fixtures tests +import { fixtures } from '@abapify/adt-fixtures'; +import { abapProgram } from '@abapify/adt-schemas'; + +const xml = await fixtures.programs.program.load(); +const data = abapProgram.parse(xml); +expect(data.abapProgram?.['adtcore:name']).toBe('ZTEST_PROGRAM'); +``` + +**abapGit handler test:** + +```typescript +import { prog } from '@abapify/adt-plugin-abapgit/schemas/generated'; +import { getHandler } from '@abapify/adt-plugin-abapgit/lib/handlers/base'; + +const handler = getHandler('PROG'); +expect(handler).toBeDefined(); +``` + +--- + +## Common Object Type Reference + +| ABAP Type | ADT Path | abapGit Serializer | Main Table | +| --------- | ------------------------------- | ------------------ | ------------ | +| `CLAS/OC` | `/sap/bc/adt/oo/classes` | `LCL_OBJECT_CLAS` | `VSEOCLASS` | +| `INTF/OI` | `/sap/bc/adt/oo/interfaces` | `LCL_OBJECT_INTF` | `VSEOINTERF` | +| `DEVC/K` | `/sap/bc/adt/packages` | `LCL_OBJECT_DEVC` | `DEVC` | +| `DTEL/DE` | `/sap/bc/adt/ddic/dataelements` | `LCL_OBJECT_DTEL` | `DD04V` | +| `DOMA/DO` | `/sap/bc/adt/ddic/domains` | `LCL_OBJECT_DOMA` | `DD01V` | +| `PROG/P` | `/sap/bc/adt/programs/programs` | `LCL_OBJECT_PROG` | `TRDIR` | +| `FUGR/F` | `/sap/bc/adt/functions/groups` | `LCL_OBJECT_FUGR` | `ENLFDIR` | +| `TABL/DT` | `/sap/bc/adt/ddic/tables` | `LCL_OBJECT_TABL` | `DD02V` | +| `MSAG/E` | `/sap/bc/adt/messageclass` | `LCL_OBJECT_MSAG` | `T100A` | + +--- + +## Checklist + +### ADT Endpoint Layer + +- [ ] Endpoint details gathered (live system or web research) +- [ ] Schema literal created in `adt-schemas/src/schemas/generated/schemas/sap/` +- [ ] Typed wrapper added to `adt-schemas/src/schemas/generated/typed.ts` +- [ ] `toSpeciSchema` export added to `adt-contracts/src/generated/schemas.ts` +- [ ] Contract created in `adt-contracts/src/adt/{module}/` +- [ ] Module index updated (or new module added to main `adt/index.ts`) +- [ ] Fixture XML created in `adt-fixtures/src/fixtures/{module}/` +- [ ] Fixture registered in `adt-fixtures/src/fixtures/registry.ts` + +### ADK Layer + +- [ ] Kind constant added to `adk/src/base/kinds.ts` +- [ ] `AdkKind` union type updated in `kinds.ts` +- [ ] `AdkObjectForKind` mapping updated in `kinds.ts` +- [ ] Object model created in `adk/src/objects/repository/{type}/` +- [ ] Object exported from `adk/src/index.ts` +- [ ] `adk/src/base/adt.ts` exports the response type +- [ ] (Source objects) `savePendingSources()` implemented +- [ ] (Source objects) `checkPendingSourcesUnchanged()` implemented for skip-unchanged support +- [ ] (Source objects) `hasPendingSources()` implemented + +### abapGit Layer + +- [ ] abapGit schema literal created in `adt-plugin-abapgit/src/schemas/generated/schemas/{type}.ts` +- [ ] TypeScript types created in `adt-plugin-abapgit/src/schemas/generated/types/{type}.ts` +- [ ] Schema registered in `adt-plugin-abapgit/src/schemas/generated/index.ts` +- [ ] Handler created in `adt-plugin-abapgit/src/lib/handlers/objects/{type}.ts` +- [ ] Handler exported from `adt-plugin-abapgit/src/lib/handlers/objects/index.ts` +- [ ] ADK re-export added to `adt-plugin-abapgit/src/lib/handlers/adk.ts` + +### Verification + +- [ ] `bunx nx build adt-schemas adt-contracts adk adt-plugin-abapgit adt-fixtures` passes +- [ ] `bunx nx typecheck` passes +- [ ] `bunx nx test adt-schemas adt-contracts adk adt-plugin-abapgit` passes +- [ ] `bunx nx lint` passes diff --git a/.agents/skills/adt-export/SKILL.md b/.agents/skills/adt-export/SKILL.md new file mode 100644 index 000000000..1ebdc357d --- /dev/null +++ b/.agents/skills/adt-export/SKILL.md @@ -0,0 +1,330 @@ +--- +name: adt-export +description: ADT export, deploy, roundtrip, diff, and unlock commands. USE WHEN working with abapGit file export/import, deploying to SAP, comparing local vs remote, roundtrip testing, or unlocking stuck objects. Trigger words - export, deploy, diff, roundtrip, unlock, compare SAP, push to SAP, abapGit sync. +--- + +# ADT Export & Roundtrip Commands + +CLI commands for the abapGit-to-SAP workflow: export (deploy), diff, roundtrip test, and unlock. + +## Command Overview + +| Command | Package | Purpose | +| --------------------------- | --------------------- | ------------------------------------------ | +| `adt export` / `adt deploy` | `@abapify/adt-export` | Push local abapGit files to SAP | +| `adt diff` | `@abapify/adt-diff` | Compare local files against SAP remote | +| `adt roundtrip` | `@abapify/adt-export` | Deploy + reimport + compare (verification) | +| `adt activate` | `@abapify/adt-export` | Bulk-activate inactive objects | +| `adt unlock` | `@abapify/adt-cli` | Force-unlock stuck SAP objects | + +All commands are registered in `adt.config.ts` and loaded by the plugin loader. + +--- + +## 1. Export / Deploy + +```bash +adt export [files...] [options] +adt deploy [files...] [options] # alias +``` + +### Options + +| Flag | Description | Default | +| ------------------------------ | --------------------------------------------------- | --------- | +| `-s, --source ` | Source directory with serialized files | `.` | +| `-f, --format ` | Format plugin (`abapgit` / `ag`) | `abapgit` | +| `-t, --transport ` | Transport request (optional for `$TMP`) | | +| `-p, --package ` | Target SAP package for new objects | | +| `--types ` | Filter by types, comma-separated (e.g. `CLAS,TABL`) | all | +| `--dry-run` | Scan only, don't save | `false` | +| `--activate` / `--no-activate` | Activate after deploy | `true` | +| `--unlock` | Force-unlock before saving | `false` | +| `--abap-language-version ` | `2`=keyUser, `5`=cloud (BTP) | | + +### Workflow + +``` +Local abapGit files (.xml + .abap) + | format.export() yields AdkObjects + v +[Create missing subpackages if --package] +[Reassign objects to correct package if needed] +[Force-unlock if --unlock] + | + v objectSet.deploy(mode: 'upsert') +SAP System (saved + activated) +``` + +Phases: + +1. **Discovery** -- format plugin parses file tree into ADK objects +2. **Subpackage creation** -- inherits transport layer from parent package +3. **Package reassignment** -- deletes + recreates objects in wrong package +4. **Unlock** -- POST `?_action=UNLOCK` per object (if `--unlock`) +5. **Deploy** -- upsert (lock/update or create), then bulk activate + +### Examples + +```bash +# Deploy everything in current dir +adt export -p ZPACKAGE -t NPLK900042 + +# Deploy specific files +adt export zage_tabl.tabl.xml zcl_myclass.clas.xml -p ZPACKAGE + +# Deploy only domains and data elements +adt export --types DOMA,DTEL -p ZPACKAGE + +# Dry run (validate without saving) +adt export --dry-run -p ZPACKAGE + +# Deploy without activation +adt export --no-activate -p ZPACKAGE -t NPLK900042 + +# Force-unlock before deploy +adt export --unlock -p ZPACKAGE +``` + +--- + +## 2. Diff + +```bash +adt diff [files...] [options] +``` + +### Options + +| Flag | Description | Default | +| ----------------------- | --------------------------------------------- | -------- | +| `--no-color` | Disable colored output | color on | +| `-c, --context ` | Context lines in diff | `3` | +| `-s, --source` | Compare ADK source instead of XML (TABL only) | `false` | + +### Supported Types + +CLAS, INTF, PROG, FUGR, TABL, DOMA, DTEL, TTYP, DEVC + +### Two Modes + +**XML mode (default)** -- works for all types: + +``` +Local .xml file --> parse via schema + | +Remote from SAP --> serialize via handler --> parse via schema + | + projectOnto(remote, local) -- strip fields local doesn't have + | + rebuild both --> unified diff +``` + +**Source mode (`--source`)** -- TABL only: + +``` +Local .xml --> tablXmlToCdsDdl() --> CDS DDL text +Remote SAP --> getSource() --> CDS DDL text + | + project remote annotations onto local's set + | + unified diff +``` + +### Projection Concept + +**Local is source of truth.** Remote is projected onto local's shape: + +- XML mode: `projectOnto()` drops keys/fields from remote that local doesn't have +- Source mode: annotations in remote CDS that don't exist in local CDS are stripped +- This eliminates false positives from fields SAP auto-adds (LANGDEP, POSITION, MATEFLAG, SHLPEXI) + +### Examples + +```bash +# Diff a single file +adt diff zage_tabl.tabl.xml + +# Diff all tables +adt diff *.tabl.xml + +# Source-level diff (human-readable CDS) +adt diff *.tabl.xml --source + +# Diff with more context +adt diff zcl_myclass.clas.xml -c 5 +``` + +### Exit Codes + +- `0` -- no differences +- `1` -- differences found, or errors + +--- + +## 3. Roundtrip + +```bash +adt roundtrip [files...] [options] +``` + +### Options + +Same as export, plus: + +| Flag | Description | +| ------------ | ---------------------------------- | +| `--keep-tmp` | Keep temp directory for inspection | + +### Workflow (3 phases) + +``` +Phase 1: Deploy -- local files --> SAP (same as export) +Phase 2: Reimport -- SAP --> adk.get() --> format.import() --> temp dir +Phase 3: Compare -- normalize XML --> diff original vs reimported +``` + +Exit `0` = all files match (roundtrip fidelity proven), exit `1` = mismatches found. + +### Examples + +```bash +# Roundtrip test all files +adt roundtrip -p ZPACKAGE -t NPLK900042 + +# Roundtrip specific files, keep temp for inspection +adt roundtrip zage_tabl.tabl.xml -p ZPACKAGE --keep-tmp + +# Roundtrip only domains +adt roundtrip --types DOMA -p ZPACKAGE +``` + +### Excluded from comparison + +- `.abapgit.xml` (repo metadata) +- `package.devc.xml` (package definition) + +--- + +## 4. Unlock + +```bash +adt unlock [options] +``` + +### Options + +| Flag | Description | +| ------------------------ | ----------------------------------------- | +| `--type ` | Object type hint (CLAS, TABL, DOMA, etc.) | +| `--uri ` | Direct ADT URI (single object only) | +| `--lock-handle ` | Specific lock handle string | + +### Resolution Order + +1. `--uri` provided -- use verbatim +2. `--type` provided -- construct URI from ADK registry +3. Neither -- search via `quickSearch()`, exact name match + +### Examples + +```bash +# Unlock by name (auto-resolves via search) +adt unlock ZCL_MY_CLASS + +# Unlock multiple objects +adt unlock ZAGE_TABL ZAGE_STRUCTURE + +# Unlock with type hint (faster, no search) +adt unlock ZAGE_TABL --type TABL + +# Unlock with direct URI +adt unlock ZAGE_TABL --uri /sap/bc/adt/ddic/tables/zage_tabl +``` + +--- + +## 5. Activate + +```bash +adt activate [files...] [options] +``` + +Three selectors (mutually exclusive): + +- **files** -- parse via format plugin +- `--package ` -- read objects from package +- `--transport ` -- read objects from transport request + +--- + +## Troubleshooting + +### Authentication + +| Symptom | Fix | +| ----------------------------- | -------------------------------------------------------------- | +| `ADT client not available` | Run `adt auth login` first | +| `401 Unauthorized` | Session expired -- run `adt auth login` again | +| SAML/SSO popup doesn't appear | Check `adt auth status`, try `adt auth login --method browser` | + +### Export / Deploy + +| Symptom | Fix | +| ------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `Object is locked by user X` | Use `--unlock` flag, or run `adt unlock ` first | +| `Transport request required` | Add `-t ` (objects not in `$TMP` need a transport) | +| `Package does not exist` | Create the package in SAP first, or use `-p` with an existing root package (subpackages auto-created) | +| `Plugin not found` | Run `bun add @abapify/adt-plugin-abapgit` | +| Object ends up in wrong package | Export deletes + recreates when `-p` is set. Ensure `-p` is the correct target. | +| `--types` not filtering | Types are uppercase and match full ADK type or prefix (e.g., `CLAS` matches `CLAS/OC`) | +| Partial failures | Check output for per-object errors. Re-run with `--dry-run` to validate. | + +### Diff + +| Symptom | Fix | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Cannot parse filename` | Must be abapGit format: `name.type.xml` | +| `Expected .xml file, got .abap` | Pass the `.xml` metadata file, not `.abap` source | +| `--source` errors on non-TABL | Source mode only supports TABL. Use default XML mode for other types. | +| False positives (extra fields) | This is the projection system's job. If you see spurious diffs on fields like LANGDEP/POSITION/SHLPEXI, the local XML is likely using a newer schema that includes those fields -- the projection should strip them. File a bug if not. | +| `Unsupported object type: XXX` | Type not registered in abapGit plugin. Check `getSupportedTypes()`. | + +### Roundtrip + +| Symptom | Fix | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Mismatches in roundtrip | Expected: the serializer isn't perfectly round-tripping. Use `--keep-tmp` and diff the temp dir manually. Common causes: field ordering, auto-computed fields (POSITION), missing optional fields. | +| Objects not activated | Check activation errors in output. Some objects need dependencies activated first. | + +### Unlock + +| Symptom | Fix | +| ------------------------------------ | -------------------------------------------------------------------- | +| `Object not found via search` | Use `--type TABL` or `--uri /sap/bc/adt/ddic/tables/name` | +| `Type X has no registered endpoint` | Falls back to search. Use `--uri` if search also fails. | +| Object not locked (info, not error) | Normal -- means it was already unlocked or never locked | +| Unlock fails for another user's lock | Can only unlock your own locks via ADT. Admin unlock needed in SM12. | + +### General + +| Symptom | Fix | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `nx build` fails with doubled path | Run `bunx nx reset` then rebuild | +| Stale CLI behavior after code changes | Rebuild: `bunx nx build ` | +| `.abapgit.xml` not found | Commands that need repo root walk up directories looking for it. Ensure you're inside an abapGit repo. | + +--- + +## Key Files + +| File | Description | +| ----------------------------------------------- | ------------------------------- | +| `packages/adt-export/src/commands/export.ts` | Export/deploy command | +| `packages/adt-export/src/commands/roundtrip.ts` | Roundtrip command | +| `packages/adt-export/src/commands/activate.ts` | Activate command | +| `packages/adt-diff/src/commands/diff.ts` | Diff command | +| `packages/adt-diff/src/lib/abapgit-to-cds.ts` | TABL XML to CDS DDL converter | +| `packages/adt-cli/src/lib/commands/unlock.ts` | Unlock command | +| `packages/adt-cli/src/lib/plugin-loader.ts` | Plugin-to-Commander translation | +| `adt.config.ts` | Command registration | diff --git a/.agents/skills/adt-reverse-engineering/SKILL.md b/.agents/skills/adt-reverse-engineering/SKILL.md new file mode 100644 index 000000000..7d92e1efe --- /dev/null +++ b/.agents/skills/adt-reverse-engineering/SKILL.md @@ -0,0 +1,498 @@ +--- +name: adt-reverse-engineering +description: Reverse-engineer SAP ADT REST endpoints using discovery, open source projects, and Sourcegraph code search. USE WHEN the user wants to understand an unknown ADT endpoint, figure out request/response formats, find content-types, discover URL patterns, or research how other tools integrate with SAP ADT. Trigger words - reverse engineer, discover endpoint, what does this endpoint return, ADT API research, find ADT endpoint, how does SAP ADT work, sniff endpoint, endpoint investigation. +--- + +# ADT Reverse Engineering + +This skill guides the systematic reverse engineering of SAP ADT REST endpoints. Use it when you need to understand an undocumented endpoint's behavior, request/response format, content types, or URL patterns before implementing support via `$add-endpoint` or `$add-object-type`. + +## When to Use This Skill + +- **Before** `$add-endpoint` — to gather endpoint details when no documentation exists +- **Before** `$add-object-type` — to understand the full ADT surface for an object type +- When the user asks "what endpoints exist for X?" or "how does Y work in ADT?" +- When you need to find content-types, XML namespaces, or URL patterns for an endpoint + +## Skill Chain + +``` +$adt-reverse-engineering → Research & discover endpoint details + ↓ +$add-endpoint → Create schema + contract + fixture + ↓ +$add-object-type → Full ADK model + abapGit handler (if applicable) +``` + +--- + +## Overview of Research Sources + +| Priority | Source | Best For | Tool | +| -------- | --------------------------------- | -------------------------------------------------- | ---------------------- | +| 1 | **Live SAP system** (discovery) | Definitive URL paths, content-types, XML structure | `adt discovery` CLI | +| 2 | **This repo** (`abapify/adt-cli`) | Existing patterns, schemas, contracts | Local grep/read | +| 3 | **Open source projects** | Implementation patterns, undocumented behaviors | Sourcegraph `src` CLI | +| 4 | **SAP documentation** | Official API specs (often incomplete) | Web search | +| 5 | **Eclipse ADT network capture** | Full HTTP request/response details | Web search / community | + +--- + +## Step 1: Start with Discovery (Live System) + +The ADT discovery endpoint is the **authoritative source** for available services. + +### 1a: Fetch the discovery document + +```bash +# Using adt-cli (preferred) +npx adt discovery -o tmp/discovery.json +npx adt discovery -o tmp/discovery.xml + +# Filter to a specific area +npx adt discovery --filter "CDS" +npx adt discovery --filter "programs" +npx adt discovery --filter "objects" +``` + +### 1b: Understand the discovery structure + +The discovery document is an AtomPub service document: + +``` +service + └── workspace[] + ├── title → Service area name (e.g., "Object Inspector") + └── collection[] + ├── title → Collection name (e.g., "Classes") + ├── href → Base URL path (e.g., "/sap/bc/adt/oo/classes") + ├── category → Object type terms (e.g., "CLAS/OC") + └── templateLinks + └── templateLink[] + ├── template → URL template with {placeholders} + ├── rel → Link relation type + └── type → Content-Type for the response +``` + +### 1c: Key things to extract from discovery + +For each endpoint you're researching, extract: + +| Field | Where to Find | Example | +| ------------------ | ------------------------------------------ | ------------------------------------------- | +| **Base URL** | `collection.href` | `/sap/bc/adt/oo/classes` | +| **Content-Type** | `templateLink.type` or `collection.accept` | `application/vnd.sap.adt.oo.classes.v4+xml` | +| **URL template** | `templateLink.template` | `/sap/bc/adt/oo/classes/{objectName}` | +| **Object type** | `category.term` | `CLAS/OC` | +| **Link relations** | `templateLink.rel` | `http://www.sap.com/adt/relations/source` | + +### 1d: Make actual requests to the endpoint + +```bash +# Fetch a specific object and save response +npx adt get ZCL_MY_CLASS -o tmp/class.xml + +# Or use curl directly for raw HTTP inspection +curl -u "$SAP_USER:$SAP_PASSWORD" \ + "https://$SAP_HOST/sap/bc/adt/oo/classes/zcl_my_class" \ + -H "Accept: application/vnd.sap.adt.oo.classes.v4+xml" \ + -v 2>tmp/headers.txt >tmp/response.xml +``` + +**Save ALL raw responses to `tmp/`** (see [tmp-folder-testing](../../rules/development/tmp-folder-testing.md)) — they become fixtures later. + +--- + +## Step 2: Search Open Source Projects + +When no live system is available, or to understand implementation patterns and edge cases, search these open source projects that implement SAP ADT clients. + +### Reference Projects + +| Project | Language | Strength | Repository | +| ------------------------- | ---------- | ----------------------------------------------------------------- | ------------------------------------------------- | +| **vscode_abap_remote_fs** | TypeScript | Most complete ADT client, great for content-types and XML parsing | `github.com/marcellourbani/vscode_abap_remote_fs` | +| **abap-adt-api** | TypeScript | Standalone ADT API lib extracted from above | `github.com/marcellourbani/abap-adt-api` | +| **sapcli** | Python | Clean endpoint definitions, good for URL patterns | `github.com/jfilak/sapcli` | +| **jenkins-library** | Go | SAP's own CI/CD lib, has ADT client for ATC/CTS | `github.com/SAP/jenkins-library` | +| **erpl-adt** | C++ | DuckDB ADT extension, different perspective on parsing | `github.com/DataZooDE/erpl-adt` | +| **vibing-steampunk** | Mixed | Community ADT experiments | `github.com/oisee/vibing-steampunk` | +| **abapify/adt-cli** | TypeScript | **This repo** — existing schemas, contracts, patterns | Local codebase | + +### 2a: Using Sourcegraph `src` CLI (Preferred) + +The `src` CLI (v6.9.0 available) searches across public GitHub repositories. Use it as the primary tool for cross-project research. + +#### Search for endpoint URL patterns + +```bash +# Find all references to a specific ADT endpoint path +src search 'repo:^github\.com/(marcellourbani/abap-adt-api|jfilak/sapcli|SAP/jenkins-library|DataZooDE/erpl-adt) /sap/bc/adt/programs' -json | jq '.Results[].file.name' + +# Search for a specific endpoint across all ADT projects +src search 'repo:^github\.com/(marcellourbani|jfilak|SAP/jenkins-library) /sap/bc/adt/cts/transport' + +# Find content-type patterns for an object type +src search 'repo:^github\.com/marcellourbani/abap-adt-api application/vnd.sap.adt.programs' +``` + +#### Search for XML namespace patterns + +```bash +# Find namespace declarations for a specific ADT area +src search 'repo:^github\.com/marcellourbani/abap-adt-api xmlns.*sap.com/adt/programs' + +# Find XML parsing code for a specific object type +src search 'repo:^github\.com/marcellourbani/abap-adt-api abapProgram' -json +``` + +#### Search for request/response handling + +```bash +# Find how content-types are used +src search 'repo:^github\.com/marcellourbani/abap-adt-api vnd.sap.adt' --count 50 + +# Find lock/unlock patterns (important for write operations) +src search 'repo:^github\.com/marcellourbani/abap-adt-api X-sap-adt-sessiontype' + +# Find CSRF token handling +src search 'repo:^github\.com/(marcellourbani/abap-adt-api|jfilak/sapcli) x-csrf-token' +``` + +#### Project-specific search patterns + +**vscode_abap_remote_fs / abap-adt-api (TypeScript — most comprehensive):** + +```bash +# Find endpoint definitions +src search 'repo:^github\.com/marcellourbani/abap-adt-api path.*=.*"/sap/bc/adt' + +# Find content-type constants +src search 'repo:^github\.com/marcellourbani/abap-adt-api CONTENT_TYPE' --count 30 + +# Find XML parsing for specific elements +src search 'repo:^github\.com/marcellourbani/abap-adt-api fullParse.*class' +``` + +**sapcli (Python — clean structure):** + +```bash +# Find endpoint definitions (Python uses class-based definitions) +src search 'repo:^github\.com/jfilak/sapcli adt_uri.*=.*"/sap/bc/adt' + +# Find content-type definitions +src search 'repo:^github\.com/jfilak/sapcli content_type.*vnd.sap' + +# Find object type handling +src search 'repo:^github\.com/jfilak/sapcli class.*ADTObject' --count 20 +``` + +**jenkins-library (Go — SAP's official):** + +```bash +# Find ADT endpoint URLs +src search 'repo:^github\.com/SAP/jenkins-library /sap/bc/adt' --count 30 + +# Find ATC-specific endpoints (jenkins-library specializes in ATC) +src search 'repo:^github\.com/SAP/jenkins-library /sap/bc/adt/atc' + +# Find CTS transport endpoints +src search 'repo:^github\.com/SAP/jenkins-library /sap/bc/adt/cts' +``` + +**erpl-adt (C++ — different parsing approach):** + +```bash +# Find endpoint definitions +src search 'repo:^github\.com/DataZooDE/erpl-adt /sap/bc/adt' + +# Find XML parsing patterns +src search 'repo:^github\.com/DataZooDE/erpl-adt ParseXML' +``` + +### 2b: Using Sourcegraph MCP Tools (Alternative) + +If the repos are indexed on the connected Sourcegraph instance, use MCP tools directly: + +``` +mcp0_sourcegraph-http__keyword_search + query: "repo:marcellourbani/abap-adt-api /sap/bc/adt/programs" + +mcp0_sourcegraph-http__nls_search + query: "repo:marcellourbani/abap-adt-api ADT program endpoint content type" +``` + +**Note:** The Sourcegraph instance may not have all external repos indexed. If MCP search returns empty, fall back to `src` CLI. + +### 2c: Manual GitHub Search (Fallback) + +If neither Sourcegraph approach works: + +```bash +# GitHub code search via web +# https://github.com/search?type=code&q=%2Fsap%2Fbc%2Fadt%2Fprograms+org%3Amarcellourbani + +# Or clone and grep locally +git clone --depth=1 https://github.com/marcellourbani/abap-adt-api.git tmp/abap-adt-api +grep -r "/sap/bc/adt/programs" tmp/abap-adt-api/ +``` + +--- + +## Step 3: Search This Repository + +Always check what already exists locally before creating new endpoints. + +### 3a: Check existing schemas + +```bash +# List all existing schemas +ls packages/adt-schemas/src/schemas/generated/schemas/sap/ + +# Check if XSD source exists for your endpoint +ls packages/adt-schemas/.xsd/sap/ | grep -i +ls packages/adt-schemas/.xsd/custom/ | grep -i +``` + +### 3b: Check existing contracts + +```bash +# List all existing contracts +find packages/adt-contracts/src/adt/ -name "*.ts" -not -name "index.ts" + +# Search for endpoint paths +grep -r "/sap/bc/adt/" packages/adt-contracts/src/ | grep -v node_modules +``` + +### 3c: Check existing fixtures + +```bash +# List all existing fixtures +find packages/adt-fixtures/src/fixtures/ -name "*.xml" + +# Search for XML namespace patterns +grep -r 'xmlns.*sap.com/adt' packages/adt-fixtures/ +``` + +--- + +## Step 4: SAP Documentation & Community + +### Official sources + +| Source | URL | Best For | +| -------------------- | ---------------------------------------------------------------------- | -------------------------------- | +| SAP Help - ADT API | https://help.sap.com/docs/abap-cloud/abap-development-tools-user-guide | Official endpoint docs | +| SAP Community | https://community.sap.com | Blog posts with endpoint details | +| SAP API Business Hub | https://api.sap.com | Some ADT-related APIs | + +### Web search patterns + +``` +# Search for specific endpoint documentation +"site:help.sap.com /sap/bc/adt/{endpoint}" + +# Search for community blog posts +"site:community.sap.com ADT REST API {object-type}" + +# Search for Eclipse ADT plugin source (has XSD definitions) +"github.com SAP abap-adt xsd {object-type}" +``` + +--- + +## Step 5: Analyze and Document Findings + +After gathering data from the sources above, create a structured analysis in `tmp/` (see [tmp-folder-testing](../../rules/development/tmp-folder-testing.md)). + +### 5a: Create endpoint analysis file + +````bash +# Save analysis to tmp/ +cat > tmp/endpoint-analysis-{name}.md << 'EOF' +# ADT Endpoint Analysis: {Name} + +## Endpoint Details +- **URL**: /sap/bc/adt/{path} +- **Methods**: GET, POST, PUT, DELETE +- **Content-Type**: application/vnd.sap.adt.{type}.v{n}+xml +- **XML Namespace**: http://www.sap.com/adt/{namespace} +- **Root Element**: {elementName} + +## Discovery Info +- **Workspace**: {workspace title} +- **Collection**: {collection title} +- **Category**: {OBJTYPE/SUBTYPE} + +## Sources +- [ ] Discovery document +- [ ] Live system response +- [ ] vscode_abap_remote_fs / abap-adt-api +- [ ] sapcli +- [ ] jenkins-library +- [ ] erpl-adt +- [ ] SAP documentation + +## XML Structure (from research) +```xml + + +```` + +## Key Attributes + +| Attribute | Namespace | Type | Required | +| --------- | --------- | ------ | -------- | +| name | adtcore | string | yes | +| type | adtcore | string | yes | + +## Notes + +- {any edge cases, version differences, etc.} + EOF + +``` + +### 5b: Save raw artifacts + +``` + +tmp/ +├── endpoint-analysis-{name}.md ← Analysis document +├── discovery.json ← Full discovery response +├── {name}-response.xml ← Raw XML response (if available) +├── {name}-headers.txt ← HTTP headers (if captured) +└── {name}-research/ ← Code snippets from other projects +├── abap-adt-api.ts ← Relevant code from abap-adt-api +├── sapcli.py ← Relevant code from sapcli +└── jenkins-library.go ← Relevant code from jenkins-library + +``` + +--- + +## Step 6: Proceed to Implementation + +Once you have sufficient endpoint details, chain to the appropriate skill: + +### For a simple endpoint (read-only, no ADK model needed): +→ Use `$add-endpoint` + +### For a full object type (CRUD + ADK + abapGit): +→ Use `$add-object-type` + +### Minimum information needed before proceeding: + +| Required | Field | Source Priority | +|----------|-------|----------------| +| **Yes** | Base URL path | Discovery > open source > docs | +| **Yes** | Content-Type (Accept header) | Discovery > open source > live capture | +| **Yes** | Root XML element name | Live response > open source > docs | +| **Yes** | XML namespace URI | Live response > open source > XSD | +| **Recommended** | Full XML sample | Live response > open source fixtures | +| **Recommended** | Object type code (e.g., CLAS/OC) | Discovery > open source | +| **Optional** | XSD schema source | `.xsd/sap/` directory > SAP Eclipse plugins | + +--- + +## Common ADT Endpoint Patterns + +### URL structure + +``` + +/sap/bc/adt/{area}/{sub-area}/{object-name} +/sap/bc/adt/{area}/{sub-area}/{object-name}/source/{include-type} +/sap/bc/adt/{area}/{sub-area}/{object-name}/objectstructure + +``` + +### Known areas and their endpoints + +| Area | Endpoints | Notes | +|------|-----------|-------| +| `oo/classes` | GET, POST, PUT, DELETE, lock, source | OO classes, multiple includes | +| `oo/interfaces` | GET, POST, PUT, DELETE, lock, source | OO interfaces | +| `programs/programs` | GET, POST, PUT, DELETE, lock, source | ABAP programs | +| `functions/groups` | GET, POST, PUT, DELETE, lock | Function groups | +| `functions/groups/{name}/fmodules/{fm}` | Nested | Function modules within groups | +| `packages` | GET, POST, PUT, DELETE | Packages (DEVC) | +| `ddic/tables` | GET, POST, PUT, DELETE | Database tables | +| `ddic/domains` | GET, POST, PUT, DELETE | DDIC domains | +| `ddic/dataelements` | GET, POST, PUT, DELETE | Data elements | +| `ddic/structures` | GET, POST, PUT, DELETE | DDIC structures | +| `messageclass` | GET, POST, PUT, DELETE | Message classes | +| `cts/transportrequests` | GET, POST | Transport requests | +| `cts/transportrequests/{id}/tasks` | GET | Transport tasks | +| `atc/runs` | POST | ATC check runs | +| `atc/worklists/{id}` | GET | ATC results | +| `discovery` | GET | Service catalog | +| `repository/informationsystem` | POST | Object search | +| `activation` | POST | Object activation | + +### HTTP headers to watch + +| Header | Purpose | Example | +|--------|---------|---------| +| `Accept` | Response content-type | `application/vnd.sap.adt.oo.classes.v4+xml` | +| `Content-Type` | Request body format | `application/vnd.sap.adt.oo.classes.v4+xml` | +| `X-sap-adt-sessiontype` | Lock session type | `stateful` | +| `X-CSRF-Token` | CSRF protection | `Fetch` (request) / token value (response) | +| `x-csrf-token` | CSRF token (response) | Token value returned by server | +| `If-Match` | Optimistic locking (ETag) | `202412201234560011` | + +### Content-Type patterns + +``` + +application/vnd.sap.adt.{area}.{sub}.v{version}+xml → Object metadata +application/vnd.sap.adt.{area}.{sub}.source.v{version} → Source code +application/vnd.sap.adt.discovery+xml → Discovery +application/atomsvc+xml → Discovery (AtomPub) +application/atom+xml → Search results +application/xml → Generic XML + +```` + +--- + +## Quick Reference: Sourcegraph Search Patterns + +### By research goal + +| Goal | Search Pattern | +|------|---------------| +| Find endpoint URL | `src search 'repo:marcellourbani/abap-adt-api /sap/bc/adt/{area}'` | +| Find content-type | `src search 'repo:marcellourbani/abap-adt-api vnd.sap.adt.{area}'` | +| Find XML namespace | `src search 'repo:marcellourbani/abap-adt-api xmlns.*sap.com/adt/{ns}'` | +| Find XML element | `src search 'repo:marcellourbani/abap-adt-api {elementName}' --count 20` | +| Find lock handling | `src search 'repo:marcellourbani/abap-adt-api lock.*{area}'` | +| Find Python impl | `src search 'repo:jfilak/sapcli /sap/bc/adt/{area}'` | +| Find Go impl | `src search 'repo:SAP/jenkins-library /sap/bc/adt/{area}'` | +| Find across ALL | `src search '(repo:marcellourbani/abap-adt-api OR repo:jfilak/sapcli OR repo:SAP/jenkins-library) /sap/bc/adt/{area}'` | + +### Multi-repo shorthand + +```bash +# Define an alias for common ADT repos (add to shell profile) +ADT_REPOS='repo:^github\.com/(marcellourbani/abap-adt-api|jfilak/sapcli|SAP/jenkins-library|DataZooDE/erpl-adt|oisee/vibing-steampunk)' + +# Then use: +src search "$ADT_REPOS /sap/bc/adt/programs" +src search "$ADT_REPOS application/vnd.sap.adt.programs" +```` + +--- + +## Checklist + +- [ ] Discovery document fetched and analyzed (if live system available) +- [ ] Endpoint URL path identified +- [ ] Content-Type(s) identified +- [ ] XML namespace(s) identified +- [ ] Root element name identified +- [ ] At least 2 open source projects consulted via Sourcegraph +- [ ] Existing schemas/contracts in this repo checked for overlap +- [ ] Endpoint analysis saved to `tmp/endpoint-analysis-{name}.md` +- [ ] Sample XML saved to `tmp/` (live or reconstructed from research) +- [ ] Ready to proceed with `$add-endpoint` or `$add-object-type` diff --git a/.agents/skills/docs-sync/SKILL.md b/.agents/skills/docs-sync/SKILL.md new file mode 100644 index 000000000..835bab50c --- /dev/null +++ b/.agents/skills/docs-sync/SKILL.md @@ -0,0 +1,157 @@ +--- +name: docs-sync +description: Detect and close drift between code and the Docusaurus site in `website/`. Use when the user asks to sync docs with code, audit docs drift, or after adding/removing packages, CLI commands, or MCP tools. Trigger words - sync docs, docs drift, update docs, website out of date, missing docs page. +--- + +# docs-sync + +Three-level pipeline for keeping `website/` in sync with the code. Each level +runs independently — **do not** skip level 1 before running the later ones. + +``` +1. check-structure (deterministic, no LLM) → find structural drift +2. generate-stubs (template, no LLM) → create missing doc files from code facts +3. refresh-content (agentic, diff-scoped) → suggest updates to existing pages [not implemented yet] +``` + +Nx targets (on the `adt-cli-docs` project): + +```bash +bunx nx docs-check adt-cli-docs # level 1 — fails CI on drift +bunx nx docs-stubs adt-cli-docs # level 2 — materialises missing pages and patches sidebars.ts +``` + +## Why this shape + +Most of `website/docs/` is a **1-to-1 projection of the code**: + +| Docs path | Code source | +| --------------------------------- | ------------------------------------- | +| `website/docs/sdk/packages/*.md` | `packages/*` | +| `website/docs/cli/*.md` | `packages/adt-cli/src/lib/commands/*` | +| `website/docs/mcp/tools/*.md` | `server.tool('name', …)` in `adt-mcp` | +| `website/docs/sdk/contracts/*.md` | `packages/adt-contracts/src/adt/*` | +| `website/sidebars.ts` | Index of all of the above | + +For this 80% of the docs, drift is a **set-difference problem**, not an +LLM problem. Level 1 computes that difference in a few milliseconds with no +model calls and no false positives. Run it first, always. + +The remaining 20% (`guides/`, `architecture/`, `plugins/writing-*`) is genuine +prose that requires human judgement — level 3 handles that, but only as an +**advisor** that flags sections for review. It does not rewrite prose. + +## Level 1 — check-structure (implemented) + +```bash +bun .agents/skills/docs-sync/scripts/check-structure.ts +``` + +Exit code `0` if clean, `1` if drift. Emits a machine-readable-ish report with: + +- `missing in docs` — exists in code, absent from docs/sidebar +- `orphan in docs` — exists in docs/sidebar, absent from code + +Detection rules: + +- **Packages**: every dir under `packages/` must have + `website/docs/sdk/packages/.md` and an entry under `sdkPackages` in + `sidebars.ts`. +- **MCP tools**: every tool name registered in `packages/adt-mcp/src/lib/tools/` + (both direct `server.tool('name', …)` and indirect + `toolName: 'name'` wrappers) must have `website/docs/mcp/tools/.md` + and an entry under `mcpTools` in `sidebars.ts`. +- **Sidebar ↔ files**: every doc id quoted in `sidebars.ts` must resolve + to an `.md`/`.mdx` file, and every doc file must be referenced from + `sidebars.ts` (except `generated-index` categories). + +Not yet covered (known gaps): + +- CLI commands (name mapping is fuzzy — `cli/cts-transport` in sidebar vs + `commands/cts/` in code). Needs an alias table before it can be automated + without false positives. +- Contracts (`sdk/contracts/*`). Mapping is one-to-many per domain dir; + needs a convention decision first. + +When extending, add new rules to `scripts/check-structure.ts` only if the +mapping is unambiguous. Fuzzy mappings belong in level 3. + +## Level 2 — generate-stubs (implemented) + +```bash +bun .agents/skills/docs-sync/scripts/generate-stubs.ts # dry-run +bun .agents/skills/docs-sync/scripts/generate-stubs.ts --write # apply +# or: bunx nx docs-stubs adt-cli-docs +``` + +For each `missing in docs` item from level 1, creates a stub file populated +from code facts only: + +- **Package**: title/description from `package.json`, links to the package + source and `AGENTS.md`/`README.md` if present. +- **MCP tool**: title and description from the `server.tool(name, description, …)` + call itself. Input schema extracted from the third argument — both object + literals (`{ ...connectionShape, foo: z.string() }`) and bare identifiers + referencing shared shapes (`sessionOrConnectionShape`) are supported. + Fields keep their `.optional()` and `.describe(…)` annotations. +- **Sidebar**: new ids are inserted into `sdkPackages` / `mcpTools` in + `website/sidebars.ts`, then the array is re-sorted alphabetically + (keeping `*/overview` first). + +Stubs carry zero hallucinated facts. When extraction is ambiguous — for +example a schema expression that is neither a recognised shared shape nor an +object literal — the stub links to the source instead of guessing. + +Known limits of the Zod extractor (regex-based, no TS AST): + +- Types beyond `string`/`number`/`boolean`/`array`/`object`/`enum` + fall back to `unknown`. +- Complex refinements (`z.string().min(1).regex(…)`) are reported as the + base type; the extra constraints are not shown. +- Conditional schemas (`z.union`, `z.discriminatedUnion`) are not supported + yet — add a branch in `zodTypeOf` if needed. + +When the extractor cannot determine the shape, it emits +`See [source](…) for the full Zod schema.` instead of a fake block. This is +intentional — **do not** teach the extractor to guess. + +## Level 3 — refresh-content (planned, not yet implemented) + +Diff-scoped advisor for **existing** pages. Runs only when level 1 is clean. + +Baseline: last commit carrying a `Docs-Sync: true` trailer (fallback: last +commit touching `website/`, with a warning). + +For each page whose backing code region changed since baseline, emit an +advisory: `: possibly stale because of `. Does **not** rewrite +prose — the human decides whether the page actually drifted. + +Rewriting is deliberately out of scope. The failure mode of +"confidently-written but subtly-wrong prose" is worse than the failure mode +of "prose is a bit stale". Stubs close the former; advisories catch the +latter without creating new lies. + +## Invocation recipe + +When the user asks to "sync docs" or "check docs drift": + +1. Run `bun .agents/skills/docs-sync/scripts/check-structure.ts`. +2. Report the result verbatim — do not summarise away items. +3. For each `missing` item, confirm with the user before creating any files. + Level 2 stubs are safe, but even stubs land in a published site. +4. For each `orphan` item, ask: _was this renamed, or removed?_ Renames need + redirects in `docusaurus.config.ts`; removals need the sidebar entry and + file both deleted. + +Never run level 3 on a repo with level 1 drift — the advisor will point at +sections that should not exist yet. + +## Don'ts + +- **Don't** write to `website/docs/` based on LLM output alone. Every claim + in a generated stub must trace to a code symbol or configuration value. +- **Don't** treat `sidebars.ts` as an append-only log. The category arrays + (`cliCommands`, `mcpTools`, `sdkPackages`, `sdkContracts`) are the spec — + keep them alphabetically sorted when editing so diffs stay small. +- **Don't** add detection rules for fuzzy mappings (CLI aliases, multi-file + contracts) until the mapping is decided. Better no rule than a noisy rule. diff --git a/.agents/skills/docs-sync/scripts/check-structure.ts b/.agents/skills/docs-sync/scripts/check-structure.ts new file mode 100644 index 000000000..18bd18797 --- /dev/null +++ b/.agents/skills/docs-sync/scripts/check-structure.ts @@ -0,0 +1,185 @@ +#!/usr/bin/env bun +/** + * docs-sync: check-structure + * + * Detects structural drift between code and Docusaurus docs (`website/`). + * Exits with code 1 if any drift is found. No LLM, no writes. + * + * Checks: + * 1. packages/* ↔ website/docs/sdk/packages/*.md ↔ sidebars.ts + * 2. MCP tools (server.tool) ↔ website/docs/mcp/tools/*.md ↔ sidebars.ts + * 3. Every doc file under website/docs is listed somewhere in sidebars.ts + * (except the root `index.md`, which is the generated-index landing page) + * 4. Every sidebar entry resolves to an existing doc file + */ +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const rootDir = fileURLToPath(new URL('../../../../', import.meta.url)).replace( + /[/\\]$/, + '', +); +const websiteDir = join(rootDir, 'website'); +const docsDir = join(websiteDir, 'docs'); +const sidebarFile = join(websiteDir, 'sidebars.ts'); +const packagesDir = join(rootDir, 'packages'); + +type Report = { section: string; missing: string[]; orphan: string[] }; +const reports: Report[] = []; + +function listDirs(p: string): string[] { + return readdirSync(p).filter((n) => { + try { + return statSync(join(p, n)).isDirectory(); + } catch { + return false; + } + }); +} + +function walk(p: string, acc: string[] = []): string[] { + for (const n of readdirSync(p)) { + const full = join(p, n); + const st = statSync(full); + if (st.isDirectory()) walk(full, acc); + else acc.push(full); + } + return acc; +} + +// --- Parse sidebars.ts: collect every quoted doc id --- +const sidebarText = readFileSync(sidebarFile, 'utf8'); +const sidebarIds = new Set(); +for (const m of sidebarText.matchAll(/['"`]([a-z0-9][a-z0-9/_-]*?)['"`]/gi)) { + const id = m[1]; + // Heuristic: doc ids contain at least one path char or match a known root doc + if (/[/]/.test(id) || id === 'index') sidebarIds.add(id); +} + +// --- 1. Packages --- +{ + const codePkgs = new Set(listDirs(packagesDir)); + const docPkgsDir = join(docsDir, 'sdk/packages'); + const docPkgs = new Set( + readdirSync(docPkgsDir) + .filter((f) => f.endsWith('.md') && f !== 'overview.md') + .map((f) => f.replace(/\.md$/, '')), + ); + const sidebarPkgs = new Set( + [...sidebarIds] + .filter( + (id) => + id.startsWith('sdk/packages/') && id !== 'sdk/packages/overview', + ) + .map((id) => id.slice('sdk/packages/'.length)), + ); + + reports.push({ + section: 'packages: code → docs file', + missing: [...codePkgs].filter((p) => !docPkgs.has(p)).sort(), + orphan: [...docPkgs].filter((p) => !codePkgs.has(p)).sort(), + }); + reports.push({ + section: 'packages: code → sidebar', + missing: [...codePkgs].filter((p) => !sidebarPkgs.has(p)).sort(), + orphan: [...sidebarPkgs].filter((p) => !codePkgs.has(p)).sort(), + }); +} + +// --- 2. MCP tools --- +{ + const toolDir = join(packagesDir, 'adt-mcp/src/lib/tools'); + const toolFiles = walk(toolDir).filter( + (f) => f.endsWith('.ts') && !f.endsWith('.test.ts'), + ); + const codeTools = new Set(); + for (const file of toolFiles) { + const text = readFileSync(file, 'utf8'); + // Direct: server.tool('name', ...) + for (const m of text.matchAll( + /server\.tool\(\s*['"`]([a-z_][a-z0-9_]*)['"`]/g, + )) { + codeTools.add(m[1]); + } + // Indirect via config wrapper: `toolName: 'name'` (see call-hierarchy.ts) + for (const m of text.matchAll( + /\btoolName\s*:\s*['"`]([a-z_][a-z0-9_]*)['"`]/g, + )) { + codeTools.add(m[1]); + } + } + + const docToolsDir = join(docsDir, 'mcp/tools'); + const docTools = new Set( + readdirSync(docToolsDir) + .filter((f) => f.endsWith('.md')) + .map((f) => f.replace(/\.md$/, '')), + ); + const sidebarTools = new Set( + [...sidebarIds] + .filter((id) => id.startsWith('mcp/tools/')) + .map((id) => id.slice('mcp/tools/'.length)), + ); + + reports.push({ + section: 'mcp tools: code → docs file', + missing: [...codeTools].filter((t) => !docTools.has(t)).sort(), + orphan: [...docTools].filter((t) => !codeTools.has(t)).sort(), + }); + reports.push({ + section: 'mcp tools: code → sidebar', + missing: [...codeTools].filter((t) => !sidebarTools.has(t)).sort(), + orphan: [...sidebarTools].filter((t) => !codeTools.has(t)).sort(), + }); +} + +// --- 3 + 4. Cross-check sidebar ↔ files for every doc under website/docs --- +{ + // The root `index.md` is the docs landing page and is always reachable + // through Docusaurus's category/generated-index machinery, so don't + // treat it as a sidebar drift candidate. + const allDocFiles = walk(docsDir) + .filter((f) => f.endsWith('.md') || f.endsWith('.mdx')) + .map((f) => relative(docsDir, f).replace(/\.(md|mdx)$/, '')) + .filter((id) => id !== 'index'); + const docSet = new Set(allDocFiles); + const sidebarIdsForCrossCheck = [...sidebarIds].filter( + (id) => id !== 'index', + ); + + reports.push({ + section: 'sidebar ↔ doc files', + // missing: sidebar references a doc that does not exist on disk + missing: sidebarIdsForCrossCheck.filter((id) => !docSet.has(id)).sort(), + // orphan: doc file on disk but not referenced anywhere in sidebars.ts + orphan: allDocFiles + .filter((id) => !sidebarIdsForCrossCheck.includes(id)) + .sort(), + }); +} + +// --- Output --- +let drift = 0; +for (const r of reports) { + const total = r.missing.length + r.orphan.length; + drift += total; + const header = total === 0 ? `✓ ${r.section}` : `✗ ${r.section} (${total})`; + console.log(header); + if (r.missing.length) { + console.log(` missing in docs: ${r.missing.length}`); + for (const m of r.missing) console.log(` - ${m}`); + } + if (r.orphan.length) { + console.log(` orphan in docs: ${r.orphan.length}`); + for (const o of r.orphan) console.log(` - ${o}`); + } +} +console.log(''); +if (drift === 0) { + console.log('No drift detected.'); + process.exit(0); +} else { + console.log(`Drift detected: ${drift} item(s). See above.`); + process.exit(1); +} diff --git a/.agents/skills/docs-sync/scripts/generate-stubs.ts b/.agents/skills/docs-sync/scripts/generate-stubs.ts new file mode 100644 index 000000000..4f996466d --- /dev/null +++ b/.agents/skills/docs-sync/scripts/generate-stubs.ts @@ -0,0 +1,592 @@ +#!/usr/bin/env bun +/** + * docs-sync: generate-stubs + * + * For every `missing in docs` item reported by check-structure, create a + * stub page from code facts and add it to `website/sidebars.ts`. Honest by + * design: when extraction is ambiguous, the stub links to source instead of + * guessing. + * + * Usage: + * bun .agents/skills/docs-sync/scripts/generate-stubs.ts # dry-run + * bun .agents/skills/docs-sync/scripts/generate-stubs.ts --write # apply + */ +import { + readdirSync, + readFileSync, + statSync, + writeFileSync, + existsSync, +} from 'node:fs'; +import { join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const rootDir = fileURLToPath(new URL('../../../../', import.meta.url)).replace( + /[/\\]$/, + '', +); +const websiteDir = join(rootDir, 'website'); +const docsDir = join(websiteDir, 'docs'); +const sidebarFile = join(websiteDir, 'sidebars.ts'); +const packagesDir = join(rootDir, 'packages'); +const githubBlobBase = 'https://github.com/abapify/adt-cli/blob/main/'; + +const writeMode = process.argv.includes('--write'); + +// ──────────────────────────────────────────────────────────────────────────── +// Utilities +// ──────────────────────────────────────────────────────────────────────────── + +function listDirs(p: string): string[] { + return readdirSync(p).filter((n) => { + try { + return statSync(join(p, n)).isDirectory(); + } catch { + return false; + } + }); +} + +function walk(p: string, acc: string[] = []): string[] { + for (const n of readdirSync(p)) { + const full = join(p, n); + if (statSync(full).isDirectory()) walk(full, acc); + else acc.push(full); + } + return acc; +} + +/** Find the balanced-paren slice starting at `open` (an opening char). */ +function sliceBalanced( + src: string, + open: number, + openCh = '(', + closeCh = ')', +): string { + let depth = 0; + for (let i = open; i < src.length; i++) { + const c = src[i]; + const next = src[i + 1]; + // Line comment + if (c === '/' && next === '/') { + while (i < src.length && src[i] !== '\n') i++; + continue; + } + // Block comment + if (c === '/' && next === '*') { + i += 2; + while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i++; + i++; // skip the '*', for-loop increments past '/' + continue; + } + if (c === openCh) depth++; + else if (c === closeCh) { + depth--; + if (depth === 0) return src.slice(open, i + 1); + } else if (c === '"' || c === "'" || c === '`') { + // skip string literal + const quote = c; + i++; + while (i < src.length && src[i] !== quote) { + if (src[i] === '\\') i++; + i++; + } + } + } + throw new Error('unbalanced'); +} + +/** Split top-level comma-separated args inside a paren/brace slice. */ +function splitTopLevel(body: string): string[] { + const out: string[] = []; + let depth = 0; + let start = 0; + for (let i = 0; i < body.length; i++) { + const c = body[i]; + if (c === '(' || c === '{' || c === '[') depth++; + else if (c === ')' || c === '}' || c === ']') depth--; + else if ((c === '"' || c === "'" || c === '`') && depth >= 0) { + const q = c; + i++; + while (i < body.length && body[i] !== q) { + if (body[i] === '\\') i++; + i++; + } + } else if (c === ',' && depth === 0) { + out.push(body.slice(start, i).trim()); + start = i + 1; + } + } + const tail = body.slice(start).trim(); + if (tail) out.push(tail); + return out; +} + +/** Parse "'foo' + \n 'bar'" style string concatenation into a single string. */ +function parseStringExpr(expr: string): string | null { + const parts: string[] = []; + // Match any sequence of string literals separated by + + const re = /['"`]((?:[^'"`\\]|\\.)*)['"`]/g; + let m; + let lastEnd = 0; + while ((m = re.exec(expr)) !== null) { + const between = expr.slice(lastEnd, m.index).trim(); + if (between && between !== '+') return null; // non-literal content + parts.push( + m[1].replace(/\\n/g, '\n').replace(/\\'/g, "'").replace(/\\"/g, '"'), + ); + lastEnd = m.index + m[0].length; + } + const tail = expr.slice(lastEnd).trim(); + if (tail && tail !== '+') return null; + return parts.length ? parts.join('') : null; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Shared Zod shapes (resolved from shared-schemas.ts) +// ──────────────────────────────────────────────────────────────────────────── + +type SchemaField = { + name: string; + type: string; // 'string' | 'number' | 'boolean' | 'unknown[]' | ... + optional: boolean; + describe: string | null; +}; + +function zodTypeOf(expr: string): { type: string; optional: boolean } { + // Zod method chains may be broken across lines: `z\n .string()` — so tolerate + // whitespace between `z`, `.`, and the type name. + const norm = expr.replace(/\s+/g, ' '); + const optional = /\.\s*optional\s*\(\s*\)/.test(norm); + if (/\bz\s*\.\s*string\s*\(/.test(norm)) return { type: 'string', optional }; + if (/\bz\s*\.\s*number\s*\(/.test(norm)) return { type: 'number', optional }; + if (/\bz\s*\.\s*boolean\s*\(/.test(norm)) + return { type: 'boolean', optional }; + if (/\bz\s*\.\s*array\s*\(/.test(norm)) + return { type: 'unknown[]', optional }; + if (/\bz\s*\.\s*object\s*\(/.test(norm)) return { type: 'object', optional }; + if (/\bz\s*\.\s*enum\s*\(/.test(norm)) { + const m = norm.match(/z\s*\.\s*enum\s*\(\s*\[([^\]]+)\]/); + if (m) { + const vals = [...m[1].matchAll(/['"`]([^'"`]+)['"`]/g)].map( + (x) => `'${x[1]}'`, + ); + return { type: vals.join(' | ') || 'string', optional }; + } + return { type: 'string', optional }; + } + return { type: 'unknown', optional }; +} + +function extractDescribe(expr: string): string | null { + // Find first `.describe('...')` — supports template-style `+` concat. + const m = expr.match(/\.describe\(([\s\S]*?)\)\s*(?:,|$|\.)/); + if (!m) return null; + return parseStringExpr(m[1].trim().replace(/\)\s*$/, '')); +} + +function parseSchemaObject( + objectLiteral: string, + sharedShapes: Map, +): SchemaField[] { + // objectLiteral starts with `{` and ends with `}` + const inner = objectLiteral.slice(1, -1).trim(); + if (!inner) return []; + const entries = splitTopLevel(inner); + const out: SchemaField[] = []; + for (const e of entries) { + const trimmed = e.trim(); + if (!trimmed) continue; + // Handle spread: ...someShape + const spread = trimmed.match(/^\.\.\.\s*([A-Za-z_$][\w$]*)/); + if (spread) { + const shape = sharedShapes.get(spread[1]); + if (shape) out.push(...shape); + else + out.push({ + name: `...${spread[1]}`, + type: 'unknown', + optional: false, + describe: 'unresolved spread', + }); + continue; + } + // Handle `name: z.xxx(...).optional().describe(...)` + const kv = trimmed.match(/^([A-Za-z_$][\w$]*)\s*:\s*([\s\S]+)$/); + if (!kv) continue; + const name = kv[1]; + const expr = kv[2]; + const { type, optional } = zodTypeOf(expr); + const describe = extractDescribe(expr); + out.push({ name, type, optional, describe }); + } + return out; +} + +function loadSharedShapes(): Map { + const file = join(packagesDir, 'adt-mcp/src/lib/tools/shared-schemas.ts'); + const src = readFileSync(file, 'utf8'); + const shapes = new Map(); + // Find `export const = { ... };` — two-pass: resolve dependencies. + const order = [ + 'connectionShape', + 'optionalConnectionShape', + 'sessionOrConnectionShape', + ]; + for (const name of order) { + const re = new RegExp(`export const ${name}\\s*=\\s*\\{`); + const m = src.match(re); + if (!m || m.index === undefined) continue; + const braceStart = m.index + m[0].length - 1; + const slice = sliceBalanced(src, braceStart, '{', '}'); + shapes.set(name, parseSchemaObject(slice, shapes)); + } + return shapes; +} + +// ──────────────────────────────────────────────────────────────────────────── +// MCP tool extraction +// ──────────────────────────────────────────────────────────────────────────── + +type ToolInfo = { + name: string; + description: string | null; + schema: SchemaField[] | null; + sourceRelPath: string; // relative to repo root +}; + +function extractToolsFromFile( + file: string, + sharedShapes: Map, +): ToolInfo[] { + const src = readFileSync(file, 'utf8'); + const results: ToolInfo[] = []; + const sourceRelPath = relative(rootDir, file); + + // Find every `server.tool(` and also detect indirect registration + // via `config.toolName` by pairing `toolName: 'X'` + `toolDescription: '...'` + // inside the same object literal. + + // 1. Direct literal form + const literalRe = /server\.tool\(/g; + let m: RegExpExecArray | null; + while ((m = literalRe.exec(src)) !== null) { + const openParen = m.index + m[0].length - 1; + let slice: string; + try { + slice = sliceBalanced(src, openParen, '(', ')'); + } catch { + continue; + } + const body = slice.slice(1, -1); + const args = splitTopLevel(body); + if (args.length < 3) continue; + const nameExpr = args[0]; + const descExpr = args[1]; + const schemaExpr = args[2]; + const nameLit = parseStringExpr(nameExpr); + if (!nameLit) continue; // indirect — handled below + const desc = parseStringExpr(descExpr); + let schema: SchemaField[] | null = null; + const trimmedSchema = schemaExpr.trim(); + if (trimmedSchema.startsWith('{')) { + try { + schema = parseSchemaObject(trimmedSchema, sharedShapes); + } catch { + schema = null; + } + } else { + // Bare identifier referencing a shared shape (e.g. `sessionOrConnectionShape`). + const identMatch = trimmedSchema.match(/^([A-Za-z_$][\w$]*)$/); + if (identMatch && sharedShapes.has(identMatch[1])) { + schema = [...sharedShapes.get(identMatch[1])!]; + } + } + results.push({ name: nameLit, description: desc, schema, sourceRelPath }); + } + + // 2. Indirect form: objects with `toolName: '...'` + const configRe = /\btoolName\s*:\s*['"`]([a-z_][a-z0-9_]*)['"`]/g; + while ((m = configRe.exec(src)) !== null) { + const name = m[1]; + if (results.some((r) => r.name === name)) continue; + // Find toolDescription within ~500 chars around the match + const window = src.slice(Math.max(0, m.index - 1000), m.index + 1000); + const descMatch = window.match( + /toolDescription\s*:\s*([\s\S]*?)(,|\n\s*toolName|\n\s*\})/, + ); + const desc = descMatch ? parseStringExpr(descMatch[1]) : null; + results.push({ name, description: desc, schema: null, sourceRelPath }); + } + + return results; +} + +function collectAllTools(): Map { + const shapes = loadSharedShapes(); + const toolDir = join(packagesDir, 'adt-mcp/src/lib/tools'); + const files = walk(toolDir).filter( + (f) => f.endsWith('.ts') && !f.endsWith('.test.ts'), + ); + const all = new Map(); + for (const f of files) { + for (const t of extractToolsFromFile(f, shapes)) { + if (!all.has(t.name)) all.set(t.name, t); + } + } + return all; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Package extraction +// ──────────────────────────────────────────────────────────────────────────── + +type PackageInfo = { + dirName: string; + pkgName: string; + description: string; + hasAgentsMd: boolean; + hasReadme: boolean; + sourceRelPath: string; +}; + +function extractPackageInfo(dirName: string): PackageInfo | null { + const dir = join(packagesDir, dirName); + const pkgJsonPath = join(dir, 'package.json'); + if (!existsSync(pkgJsonPath)) return null; + const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); + return { + dirName, + pkgName: pkg.name ?? `@abapify/${dirName}`, + description: pkg.description ?? '', + hasAgentsMd: existsSync(join(dir, 'AGENTS.md')), + hasReadme: existsSync(join(dir, 'README.md')), + sourceRelPath: relative(rootDir, dir), + }; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Rendering +// ──────────────────────────────────────────────────────────────────────────── + +function escapeFrontmatter(s: string): string { + return s.replace(/'/g, "''").replace(/\n/g, ' ').trim(); +} + +function renderMcpTool(info: ToolInfo): string { + const desc = info.description?.trim() ?? ''; + const sourceLink = `${githubBlobBase}${info.sourceRelPath}`; + const lines: string[] = []; + lines.push('---'); + lines.push(`title: ${info.name}`); + lines.push(`sidebar_label: ${info.name}`); + if (desc) lines.push(`description: '${escapeFrontmatter(desc)}'`); + lines.push('---'); + lines.push(''); + lines.push(`# \`${info.name}\``); + lines.push(''); + if (desc) { + lines.push(desc); + lines.push(''); + } + lines.push(`Defined in [\`${info.sourceRelPath}\`](${sourceLink}).`); + lines.push(''); + lines.push('## Input schema'); + lines.push(''); + if (info.schema && info.schema.length) { + lines.push('```ts'); + lines.push('{'); + for (const f of info.schema) { + const opt = f.optional ? '?' : ''; + const comment = f.describe ? ` // ${f.describe}` : ''; + lines.push(` ${f.name}${opt}: ${f.type};${comment}`); + } + lines.push('}'); + lines.push('```'); + } else if (info.schema && info.schema.length === 0) { + lines.push('_This tool takes no parameters._'); + } else { + lines.push(`See [source](${sourceLink}) for the full Zod schema.`); + } + lines.push(''); + lines.push('## Output'); + lines.push(''); + lines.push( + 'The tool returns a single text content item whose body is a JSON-serialised object (`content[0].text`). On error, the response has `isError: true` and a human-readable message.', + ); + lines.push(''); + lines.push('```json'); + lines.push('{'); + lines.push( + ' "content": [{ "type": "text", "text": "" }]', + ); + lines.push('}'); + lines.push('```'); + lines.push(''); + lines.push('See the source for the exact shape of `result`.'); + lines.push(''); + return lines.join('\n'); +} + +function renderPackage(info: PackageInfo): string { + const sourceLink = `${githubBlobBase}${info.sourceRelPath}`; + const agentsLink = info.hasAgentsMd ? `${sourceLink}/AGENTS.md` : null; + const readmeLink = info.hasReadme ? `${sourceLink}/README.md` : null; + const lines: string[] = []; + lines.push('---'); + lines.push(`title: '${info.pkgName}'`); + if (info.description) + lines.push(`description: '${escapeFrontmatter(info.description)}'`); + lines.push('---'); + lines.push(''); + lines.push(`# \`${info.pkgName}\``); + lines.push(''); + if (info.description) { + lines.push(info.description); + lines.push(''); + } + lines.push('## Install'); + lines.push(''); + lines.push('```bash'); + lines.push(`bun add ${info.pkgName}`); + lines.push('```'); + lines.push(''); + lines.push('## Source'); + lines.push(''); + lines.push(`- Source: [\`${info.sourceRelPath}\`](${sourceLink})`); + if (agentsLink) + lines.push( + `- Internal guide: [\`${info.sourceRelPath}/AGENTS.md\`](${agentsLink})`, + ); + if (readmeLink) + lines.push( + `- README: [\`${info.sourceRelPath}/README.md\`](${readmeLink})`, + ); + lines.push(''); + lines.push( + '> Stub page generated by `docs-sync`. Expand with usage examples, public API, and dependency notes.', + ); + lines.push(''); + return lines.join('\n'); +} + +// ──────────────────────────────────────────────────────────────────────────── +// Sidebar patching +// ──────────────────────────────────────────────────────────────────────────── + +function patchSidebarArray( + src: string, + arrayName: string, + newIds: string[], +): { src: string; added: string[] } { + const re = new RegExp(`const ${arrayName}\\s*=\\s*\\[`); + const m = src.match(re); + if (!m || m.index === undefined) + throw new Error(`sidebar array ${arrayName} not found`); + const openBracket = m.index + m[0].length - 1; + const slice = sliceBalanced(src, openBracket, '[', ']'); + const inner = slice.slice(1, -1); + // Extract existing string-literal entries to determine what's already present + const entryRe = /['"`]([^'"`]+)['"`]/g; + const existing: string[] = []; + let em; + while ((em = entryRe.exec(inner)) !== null) existing.push(em[1]); + const existingSet = new Set(existing); + const added: string[] = []; + for (const id of newIds) { + if (!existingSet.has(id)) added.push(id); + } + // If nothing to add, return original source unchanged + if (added.length === 0) return { src, added: [] }; + // Inject added entries before the closing ']' + const indent = ' '; + const addedLines = added.map((id) => `${indent}'${id}',`).join('\n') + '\n'; + const modifiedSlice = slice.slice(0, -1) + addedLines + ']'; + const before = src.slice(0, openBracket); + const after = src.slice(openBracket + slice.length); + return { src: before + modifiedSlice + after, added }; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Main +// ──────────────────────────────────────────────────────────────────────────── + +function main() { + const mcpTools = collectAllTools(); + + // Discover missing items by comparing to current docs. + const missingPackages: PackageInfo[] = []; + const docPackagesDir = join(docsDir, 'sdk/packages'); + const existingPackageDocs = existsSync(docPackagesDir) + ? new Set( + readdirSync(docPackagesDir) + .filter((f) => f.endsWith('.md')) + .map((f) => f.replace(/\.md$/, '')), + ) + : new Set(); + for (const dir of listDirs(packagesDir)) { + if (existingPackageDocs.has(dir)) continue; + const info = extractPackageInfo(dir); + if (info) missingPackages.push(info); + } + + const missingTools: ToolInfo[] = []; + const docToolsDir = join(docsDir, 'mcp/tools'); + const existingToolDocs = existsSync(docToolsDir) + ? new Set( + readdirSync(docToolsDir) + .filter((f) => f.endsWith('.md')) + .map((f) => f.replace(/\.md$/, '')), + ) + : new Set(); + for (const [name, info] of mcpTools) { + if (!existingToolDocs.has(name)) missingTools.push(info); + } + + console.log(`Missing packages: ${missingPackages.length}`); + for (const p of missingPackages) + console.log(` - ${p.dirName} (${p.pkgName})`); + console.log(`Missing MCP tools: ${missingTools.length}`); + for (const t of missingTools) + console.log( + ` - ${t.name} (schema: ${t.schema ? `${t.schema.length} fields` : 'unresolved'})`, + ); + + if (!writeMode) { + console.log( + '\n(dry run — pass --write to create files and patch sidebars.ts)', + ); + return; + } + + // Write pages. + for (const p of missingPackages) { + const target = join(docsDir, 'sdk/packages', `${p.dirName}.md`); + writeFileSync(target, renderPackage(p)); + console.log(`wrote ${relative(rootDir, target)}`); + } + for (const t of missingTools) { + const target = join(docsDir, 'mcp/tools', `${t.name}.md`); + writeFileSync(target, renderMcpTool(t)); + console.log(`wrote ${relative(rootDir, target)}`); + } + + // Patch sidebars.ts. + let sidebar = readFileSync(sidebarFile, 'utf8'); + if (missingPackages.length) { + const ids = missingPackages.map((p) => `sdk/packages/${p.dirName}`); + const res = patchSidebarArray(sidebar, 'sdkPackages', ids); + sidebar = res.src; + for (const id of res.added) console.log(`sidebar += ${id}`); + } + if (missingTools.length) { + const ids = missingTools.map((t) => `mcp/tools/${t.name}`); + const res = patchSidebarArray(sidebar, 'mcpTools', ids); + sidebar = res.src; + for (const id of res.added) console.log(`sidebar += ${id}`); + } + writeFileSync(sidebarFile, sidebar); + console.log(`patched ${relative(rootDir, sidebarFile)}`); +} + +main(); diff --git a/.agents/skills/link-workspace-packages/SKILL.md b/.agents/skills/link-workspace-packages/SKILL.md new file mode 100644 index 000000000..de1313497 --- /dev/null +++ b/.agents/skills/link-workspace-packages/SKILL.md @@ -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 `/node_modules/@org/` +- **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 diff --git a/.agents/skills/monitor-ci/SKILL.md b/.agents/skills/monitor-ci/SKILL.md new file mode 100644 index 000000000..c90f54422 --- /dev/null +++ b/.agents/skills/monitor-ci/SKILL.md @@ -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 /scripts/ci-poll-decide.mjs '' \ + [--wait-mode] \ + [--prev-cipe-url ] \ + [--expected-sha ] \ + [--prev-status ] \ + [--timeout ] \ + [--new-cipe-timeout ] \ + [--env-rerun-count ] \ + [--no-progress-count ] \ + [--prev-cipe-status ] \ + [--prev-sh-status ] \ + [--prev-verification-status ] \ + [--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 /scripts/ci-state-update.mjs post-action \ + --action \ + --cipe-url \ + --commit-sha +``` + +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 /scripts/ci-state-update.mjs cycle-check \ + --code \ + [--agent-triggered] \ + --cycle-count --max-cycles \ + --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) | diff --git a/.agents/skills/monitor-ci/references/fix-flows.md b/.agents/skills/monitor-ci/references/fix-flows.md new file mode 100644 index 000000000..38daa65e4 --- /dev/null +++ b/.agents/skills/monitor-ci/references/fix-flows.md @@ -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`, `bun.lock`/`bun.lockb` → `bunx 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 ` (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(): + +Failed tasks: , +Local verification: passed|enhanced|failed-pushing-to-ci" +``` diff --git a/.agents/skills/monitor-ci/scripts/ci-poll-decide.mjs b/.agents/skills/monitor-ci/scripts/ci-poll-decide.mjs new file mode 100644 index 000000000..55a07ae1e --- /dev/null +++ b/.agents/skills/monitor-ci/scripts/ci-poll-decide.mjs @@ -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 '' \ + * [--wait-mode] [--prev-cipe-url ] [--expected-sha ] \ + * [--prev-status ] [--timeout ] [--new-cipe-timeout ] \ + * [--env-rerun-count ] [--no-progress-count ] \ + * [--prev-cipe-status ] [--prev-sh-status ] \ + * [--prev-verification-status ] [--prev-failure-classification ] + */ + +// --- 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 (err) { + 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()); diff --git a/.agents/skills/monitor-ci/scripts/ci-state-update.mjs b/.agents/skills/monitor-ci/scripts/ci-state-update.mjs new file mode 100644 index 000000000..90fa71423 --- /dev/null +++ b/.agents/skills/monitor-ci/scripts/ci-state-update.mjs @@ -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 [counter args] + * node ci-state-update.mjs post-action --action [--cipe-url ] [--commit-sha ] + * node ci-state-update.mjs cycle-check --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}` }); +} diff --git a/.agents/skills/nx-generate/SKILL.md b/.agents/skills/nx-generate/SKILL.md new file mode 100644 index 000000000..af7ba80a4 --- /dev/null +++ b/.agents/skills/nx-generate/SKILL.md @@ -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//generators.json'));"` to find the generators.json, then locate the source from there +- If that fails, read directly from `node_modules//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 --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. diff --git a/.agents/skills/nx-import/SKILL.md b/.agents/skills/nx-import/SKILL.md new file mode 100644 index 000000000..b1cd381d3 --- /dev/null +++ b/.agents/skills/nx-import/SKILL.md @@ -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 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 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 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 `jar` or `war` and a `main` class; Gradle `application` plugin or `mainClass` setting + - _.NET_: `.csproj`/`.fsproj` with `Exe` or `WinExe` + - _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 `Library`, named exports intended for import by other packages. + +**Destination directory rules**: + +- Applications → `apps/`. 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 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. diff --git a/.agents/skills/nx-import/references/ESLINT.md b/.agents/skills/nx-import/references/ESLINT.md new file mode 100644 index 000000000..223406253 --- /dev/null +++ b/.agents/skills/nx-import/references/ESLINT.md @@ -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. diff --git a/.agents/skills/nx-import/references/GRADLE.md b/.agents/skills/nx-import/references/GRADLE.md new file mode 100644 index 000000000..30dface2e --- /dev/null +++ b/.agents/skills/nx-import/references/GRADLE.md @@ -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 diff --git a/.agents/skills/nx-import/references/JEST.md b/.agents/skills/nx-import/references/JEST.md new file mode 100644 index 000000000..64de5b7a9 --- /dev/null +++ b/.agents/skills/nx-import/references/JEST.md @@ -0,0 +1,228 @@ +## 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` diff --git a/.agents/skills/nx-import/references/NEXT.md b/.agents/skills/nx-import/references/NEXT.md new file mode 100644 index 000000000..d9ec1f0b5 --- /dev/null +++ b/.agents/skills/nx-import/references/NEXT.md @@ -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/` (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/` (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 diff --git a/.agents/skills/nx-import/references/TURBOREPO.md b/.agents/skills/nx-import/references/TURBOREPO.md new file mode 100644 index 000000000..b322b5446 --- /dev/null +++ b/.agents/skills/nx-import/references/TURBOREPO.md @@ -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/.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/` 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 diff --git a/.agents/skills/nx-import/references/VITE.md b/.agents/skills/nx-import/references/VITE.md new file mode 100644 index 000000000..d1874bfba --- /dev/null +++ b/.agents/skills/nx-import/references/VITE.md @@ -0,0 +1,397 @@ +## 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` 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; + 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/` (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/` (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/` (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/` +- 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 packages/ --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 diff --git a/.agents/skills/nx-plugins/SKILL.md b/.agents/skills/nx-plugins/SKILL.md new file mode 100644 index 000000000..ff6a23337 --- /dev/null +++ b/.agents/skills/nx-plugins/SKILL.md @@ -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: `bunx nx list` +- Install plugins `bunx nx add `. Example: `bunx nx add @nx/react`. diff --git a/.agents/skills/nx-run-tasks/SKILL.md b/.agents/skills/nx-run-tasks/SKILL.md new file mode 100644 index 000000000..7f1263a57 --- /dev/null +++ b/.agents/skills/nx-run-tasks/SKILL.md @@ -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 --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 : +``` + +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=` — use a specific configuration (e.g. `production`) diff --git a/.agents/skills/nx-workspace/SKILL.md b/.agents/skills/nx-workspace/SKILL.md new file mode 100644 index 000000000..4b5110ad0 --- /dev/null +++ b/.agents/skills/nx-workspace/SKILL.md @@ -0,0 +1,286 @@ +--- +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 --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 +``` diff --git a/.agents/skills/nx-workspace/references/AFFECTED.md b/.agents/skills/nx-workspace/references/AFFECTED.md new file mode 100644 index 000000000..e30f18f6a --- /dev/null +++ b/.agents/skills/nx-workspace/references/AFFECTED.md @@ -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 +``` diff --git a/.agents/skills/openapi-to-abap/SKILL.md b/.agents/skills/openapi-to-abap/SKILL.md new file mode 100644 index 000000000..e17bca72d --- /dev/null +++ b/.agents/skills/openapi-to-abap/SKILL.md @@ -0,0 +1,224 @@ +--- +name: openapi-to-abap +description: Generate an ABAP REST client from an OpenAPI document using the v2 pipeline (3 interfaces + 1 class + bundled locals). USE WHEN the user wants to convert OpenAPI / Swagger into ABAP, work on the abap-ast or openai-codegen packages, add a new target profile, add a JSON-Schema to ABAP type mapping, or deploy a generated client to BTP Steampunk. Trigger phrases - openapi to abap, generate abap client from openapi, openai-codegen, abap rest client, swagger abap client, petstore abap, abap-ast, typed ABAP AST, ABAP pretty printer, add target profile, add schema mapping, ZIF_types, ZIF_ops, ZCX_error. +--- + +# OpenAPI → ABAP Client Codegen (v2) + +This skill guides work on the `abap-ast` and `openai-codegen` packages: +generating ABAP REST client classes from OpenAPI 3.0/3.1 documents, +adding new target profiles, extending JSON-Schema → ABAP type mappings, +and deploying the generated output to a live BTP Steampunk system. + +## When to use + +Invoke this skill when the user asks to: + +- Convert an OpenAPI / Swagger document into an ABAP client. +- Work inside `packages/abap-ast/` or `packages/openai-codegen/`. +- Add a new target profile (for example `s4-onprem-modern`, + `on-prem-classic`). +- Add or change a JSON-Schema → ABAP type mapping rule. +- Deploy a generated client (e.g. Petstore3) to a BTP Steampunk tenant + via `adt-cli`. +- Debug parser errors reported by `@abaplint/core` against the + generated source. + +Do **not** use this skill for generic ADT endpoint work (use +`add-endpoint`), object CRUD plumbing (use `add-object-type`), or +format plugins (use `adt-export`). + +## Architecture summary (v2) + +```text +ZIF__TYPES ── all schemas as ABAP TYPES (+ @openapi-schema markers) + ▲ + │ zif__types=>… + │ +ZIF_ ── all operations as typed METHODS (+ @openapi-operation markers) + │ RAISING ZCX__ERROR + │ +ZCL_ ── implements ZIF_ + one private attribute: client TYPE REF TO lcl_http + each method = fetch(...) + CASE response->status( ) + ───────────────────────────────────────────── + local classes (in the same .clas, locals_def/imp): + lcl_http, lcl_response, json, lcl_json_parser + +ZCX__ERROR ── inherits cx_static_check + carries status / description / body / headers +``` + +Pipeline (one direction only): + +```text +loadSpec (oas/) ─► planTypes (types/) ─► resolveNames (emit/naming.ts) + ─► emitTypesInterface / emitOperationsInterface / emitExceptionClass + ─► emitImplementationClass / emitLocalClasses + ─► print (abap-ast) ─► writeClientBundle (format/) +``` + +Two packages: + +- `abap-ast` — zero-dependency typed AST + deterministic pretty-printer. + Emit-only. Declaration nodes carry an optional `abapDoc: readonly +string[]` field; the printer emits `"! ` above each declaration. +- `openai-codegen` — pipeline + profile registry + CLI + (`packages/openai-codegen/src/cli.ts`). Depends only on `abap-ast`. + +Reference example: `samples/petstore3-client/` — the generator output +is the 11-file abapGit layout in `samples/petstore3-client/generated/abapgit/`. + +## Adding a new target profile + +The locals bundle is shared in v2, so adding a profile is now mostly a +registry + dispatcher change: + +1. Create `packages/openai-codegen/src/profiles/.ts` exporting a + `TargetProfile` (id, ABAP language version, allowed kernel classes). + Mirror `cloud.ts`. +2. Register it in `packages/openai-codegen/src/profiles/registry.ts` + (`PROFILES`, `ALL_PROFILES`) and add the id to `TargetProfileId` in + `profiles/types.ts`. +3. Remove the short-circuit in + `packages/openai-codegen/src/generate.ts` (`assertSupportedTarget`) + or extend it to accept the new id. +4. If the new profile needs a different kernel class (e.g. + `cl_http_client` for classic on-prem), branch inside + `packages/openai-codegen/src/emit/templates/locals-*.abap.ts`. Avoid + forking the whole bundle — prefer a small conditional. +5. Update the CLI whitelist in `packages/openai-codegen/src/cli.ts` + (`parseTarget`) if needed. +6. Add a Vitest suite under `packages/openai-codegen/tests/` that runs + the pipeline with `--target ` and parse-checks the output via + `@abaplint/core`. + +## Adding a new JSON-Schema → ABAP mapping rule + +Type mapping lives in `packages/openai-codegen/src/types/map.ts`. To +add a rule (e.g. `format: "uuid"` → `sysuuid_x16`): + +1. Extend the mapping in `src/types/map.ts`. Keep it pure: + `JsonSchema → AbapTypeRef | AbapTypeDef`, no side effects. +2. If the new shape introduces a named `TypeDef`, allocate via the + planner's `NameAllocator` in `src/types/plan.ts`. +3. If cycles are possible, confirm the back-edge handling: the planner + breaks cycles with `REF TO data` and attaches a `"! @openapi-ref +:` marker. Add a fixture. +4. Add a test in `packages/openai-codegen/tests/types-emit.test.ts` + asserting both the AST shape and the printed ABAP. +5. Update the mapping table in `packages/openai-codegen/README.md`. + +## Adding a new CLI flag + +1. Extend `RawCliOptions` + `buildCliRunOptions` in + `packages/openai-codegen/src/cli.ts`. +2. Thread through `GenerateOptions` in + `packages/openai-codegen/src/generate.ts` if it affects emission. +3. Add help text and a unit test in the CLI test suite. + +## Deploying to BTP Steampunk + +Short recipe: + +```bash +bunx adt auth login +bunx openai-codegen \ + --input ./spec/openapi.json \ + --out ./generated/abapgit \ + --format abapgit \ + --base petstore3 \ + --target s4-cloud +bunx adt deploy \ + --source ./generated/abapgit \ + --package ZMY_PACKAGE \ + --activate --unlock +``` + +Observed layout (11 files): + +```text +generated/abapgit/ +├── package.devc.xml +└── src/ + ├── zif_petstore3_types.intf.abap / .intf.xml + ├── zif_petstore3.intf.abap / .intf.xml + ├── zcx_petstore3_error.clas.abap / .clas.xml + ├── zcl_petstore3.clas.abap / .clas.xml + ├── zcl_petstore3.clas.locals_def.abap + └── zcl_petstore3.clas.locals_imp.abap +``` + +`S_ABPLNGVS` is granted per package on BTP. Deploy into a **user-owned** +Z package (for example `ZMY_PACKAGE` or `ZPEPL`) — `$TMP` often returns +HTTP 403. + +Verify the class landed and activated: + +```bash +bunx adt fetch /sap/bc/adt/oo/classes/ZCL_PETSTORE3/source/main +``` + +Authoritative walk-through: +`samples/petstore3-client/e2e/README.md`. + +## Common pitfalls + +- **Do not re-introduce the monolithic class.** The v1 design (one + `ZCL_` with inline tokenizer, per-op `_des_*` / `_ser_*` stubs, + private `_send` helper) was rejected. Stay in the v2 3-layer split: + types interface + operations interface + thin implementation class. +- **Never emit `"` line comments in structural positions.** ADT rejects + them as unstorable. The writer strips them at emit time. Use ABAPDoc + (`"!` via the `abapDoc` field) or trailing comments on code lines. +- **ZCX is its own global class file.** Emit `zcx__error.clas.abap` + - envelope as a separate artifact. Do not concatenate it into the + main class file. +- **`VSEOINTERF` / `VSEOCLASS` must carry + `5`** for cloud + systems. Without it, Steampunk import fails with a language-version + mismatch. +- **Use `--package` with a user-owned Z package on BTP.** `$TMP` + typically fails with `S_ABPLNGVS` 403. +- **`lcl_http->fetch` is transport-only.** Do NOT add JSON awareness, + status-code dispatch, or operation knowledge to it. Those belong in + the per-operation method body in `ZCL_`. +- **Schema TYPES live on `ZIF__TYPES`, never on the class.** ABAP's + unified component namespace on a class collides types with attributes + and methods. Keep the types interface as their only home; reference + them as `zif__types=>…`. +- **No hungarian prefixes (`iv_`, `rv_`, `ls_`) in generated names.** + Names come from `operationId` / schema property names, sanitized for + ABAP. Do not re-introduce legacy prefixes. + +## Testing + +All of the following must be green before a PR lands: + +```bash +bunx nx run-many -t typecheck,test,build,lint -p abap-ast,openai-codegen +``` + +Regression gate — regenerate the Petstore3 fixture and confirm the diff +is empty: + +```bash +cd samples/petstore3-client && bun run generate +git diff --exit-code samples/petstore3-client/generated +``` + +Parse-check — the emitted source must parse under `@abaplint/core` with +zero fatal errors. Non-fatal style lints are tolerated. + +## Do NOT + +- Emit a monolithic `ZCL_` with inline JSON tokenizer or per-op + `_des_*` / `_ser_*` private stubs. That was v1; v2 forbids it. +- Introduce Z-dependencies in the bundled locals (`/ui2/cl_json` and + other standard kernel classes are fine; any `Z*` reference is not). +- Add private helper methods to the generated `ZCL_` beyond + `constructor`. +- Commit internal SAP system IDs, tenant hostnames, or company names + to public files. Keep live-system specifics in local `.env` / auth + config only. +- Push feature work directly to `main`. Always use a feature branch. diff --git a/.agents/skills/openspec-apply-change/SKILL.md b/.agents/skills/openspec-apply-change/SKILL.md new file mode 100644 index 000000000..dd05a967d --- /dev/null +++ b/.agents/skills/openspec-apply-change/SKILL.md @@ -0,0 +1,159 @@ +--- +name: openspec-apply-change +description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks. +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: '1.0' + generatedBy: '1.2.0' +--- + +Implement tasks from an OpenSpec change. + +**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. + +**Steps** + +1. **Select the change** + + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select + + Always announce: "Using change: " and how to override (e.g., `/opsx:apply `). + +2. **Check status to understand the schema** + + ```bash + openspec status --change "" --json + ``` + + Parse the JSON to understand: + - `schemaName`: The workflow being used (e.g., "spec-driven") + - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others) + +3. **Get apply instructions** + + ```bash + openspec instructions apply --change "" --json + ``` + + This returns: + - Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs) + - Progress (total, complete, remaining) + - Task list with status + - Dynamic instruction based on current state + + **Handle states:** + - If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change + - If `state: "all_done"`: congratulate, suggest archive + - Otherwise: proceed to implementation + +4. **Read context files** + + Read the files listed in `contextFiles` from the apply instructions output. + The files depend on the schema being used: + - **spec-driven**: proposal, specs, design, tasks + - Other schemas: follow the contextFiles from CLI output + +5. **Show current progress** + + Display: + - Schema being used + - Progress: "N/M tasks complete" + - Remaining tasks overview + - Dynamic instruction from CLI + +6. **Implement tasks (loop until done or blocked)** + + For each pending task: + - Show which task is being worked on + - Make the code changes required + - Keep changes minimal and focused + - Mark task complete in the tasks file: `- [ ]` → `- [x]` + - Continue to next task + + **Pause if:** + - Task is unclear → ask for clarification + - Implementation reveals a design issue → suggest updating artifacts + - Error or blocker encountered → report and wait for guidance + - User interrupts + +7. **On completion or pause, show status** + + Display: + - Tasks completed this session + - Overall progress: "N/M tasks complete" + - If all done: suggest archive + - If paused: explain why and wait for guidance + +**Output During Implementation** + +``` +## Implementing: (schema: ) + +Working on task 3/7: +[...implementation happening...] +✓ Task complete + +Working on task 4/7: +[...implementation happening...] +✓ Task complete +``` + +**Output On Completion** + +``` +## Implementation Complete + +**Change:** +**Schema:** +**Progress:** 7/7 tasks complete ✓ + +### Completed This Session +- [x] Task 1 +- [x] Task 2 +... + +All tasks complete! Ready to archive this change. +``` + +**Output On Pause (Issue Encountered)** + +``` +## Implementation Paused + +**Change:** +**Schema:** +**Progress:** 4/7 tasks complete + +### Issue Encountered + + +**Options:** +1.