npm package: @oamm/textor
Description: Safe, deterministic scaffolding and refactoring tool for Astro projects
Latest version: 1.0.16

# Textor

A safe, deterministic scaffolding and refactoring CLI tool for Astro + modern frontend projects.

Textor enforces strict separation of concerns:
- **Pages** = Thin routing adapters
- **Features** = Self-contained implementation modules
- **Components** = Reusable UI primitives

The tool is designed to be production-grade, principal-engineer approved, and optimized for large teams and long-lived codebases.

## 🔒 Core Principles

1.  **Explicit over implicit** — No magic behavior.
2.  **Safe by default** — Never delete or overwrite hand-written code.
3.  **Configurable over hardcoded** — Everything driven by configuration.
4.  **Reversible operations** — Add, move, and remove operations are always supported.
5.  **Deterministic output** — Same input always produces the same structure.

## 🚀 Installation

```bash
# In your Astro project
pnpm add -D @oamm/textor

# Initialize configuration
pnpm textor init
```

This creates .textor/config.json with default settings and initializes the state tracker.

## 🏗️ Scaffolding Presets

Control structural complexity using presets. Presets apply to both add-section and create-component.

-   **minimal**: Lean defaults, minimal folders. Ideal for simple components and features.
-   **standard**: Balanced structure. Includes hooks, tests, and types by default.
-   **senior**: Full enterprise-grade layout including API, services, schemas, and documentation.

Select a preset via the --preset flag:
```bash
pnpm textor add-section /users users/catalog --preset senior
```

## 🗺️ Routing Mode

Textor supports explicit routing strategies to avoid ambiguity in Astro projects.

-   **flat**: /users → src/pages/users.astro (Default)
-   **nested**: /users → src/pages/users/index.astro

Configure this in .textor/config.json:
```json
{
  "routing": {
    "mode": "nested",
    "indexFile": "index.astro"
  }
}
```

Textor supports multiple frameworks (React, Astro). By default, components are created as React components (`.tsx`), while features and pages are Astro components (`.astro`).

Configure this in `.textor/config.json`:
```json
{
  "components": {
    "framework": "react"
  },
  "features": {
    "framework": "astro"
  }
}
```

## 📦 Feature Entry Strategy

Avoid filename collisions for feature entry files.

-   **pascal**: src/features/users/catalog/UsersCatalog.astro (Default)
-   **index**: src/features/users/catalog/index.astro

Configure this in .textor/config.json:
```json
{
  "features": {
    "entry": "pascal"
  }
}
```

## File Naming Patterns

You can override generated file names for feature and component sub-files (api, services, hooks, tests, etc.) using simple patterns. Patterns support `{{componentName}}`, `{{hookName}}`, `{{hookExtension}}`, `{{testExtension}}`, `{{componentExtension}}`, and `{{featureExtension}}`.

Example:
```json
{
  "filePatterns": {
    "features": {
      "api": "{{componentName}}.route.ts"
    },
    "components": {
      "api": "{{componentName}}.route.ts"
    }
  }
}
```

More examples:
```json
{
  "filePatterns": {
    "features": {
      "hook": "use{{componentName}}.ts",
      "test": "{{componentName}}.spec{{testExtension}}",
      "readme": "{{componentName}}.md"
    },
    "components": {
      "api": "{{componentName}}.route{{testExtension}}",
      "services": "{{componentName}}.service{{hookExtension}}",
      "stories": "{{componentName}}.stories.tsx"
    }
  }
}
```

## 🛡️ Safety & File Tracking

Textor uses a multi-layered safety approach to protect your codebase.

### 1. File Signatures
Every file generated by Textor includes a signature (e.g., <!-- @generated by Textor -->). Textor will refuse to delete or move any file missing this signature unless --force is used.

### 2. State Tracking & Hashing
Textor maintains a .textor/state.json file that tracks:
- File paths and kind (route, feature, component)
- Templates used
- **Content hashes**
- Creation timestamps

### 3. Integrity Verification
Safe deletion and moves require:
1.  Valid Textor signature in the file.
2.  Presence of the file in the state.
3.  **Hash match** (verifying the file hasn't been manually edited).

If you have manually edited a Textor-generated file and wish to remove or move it, use --accept-changes or --force.

## 🛠️ Commands

### add-section
Create a route + feature binding, or a standalone feature.

```bash
pnpm textor add-section [route] <featurePath> [options]
```

If `route` is provided, Textor creates both a route adapter (e.g., in `src/pages`) and a feature module. If `route` is omitted, Textor scaffolds only the feature module. This is useful for features that are shared across multiple pages or used as sub-parts of other features.

**Examples:**
```bash
# Create a section with a route
pnpm textor add-section /users users/catalog

# Create a standalone feature (no route file)
pnpm textor add-section auth/login
```

**Options:**
- --preset <name>: scaffolding preset (minimal, standard, senior)
- --layout <name>: Layout component name (use "none" for no layout)
- --name <name>: Custom name for easier state lookup
- --endpoint: Create an API endpoint (.ts) instead of an Astro page
- --dry-run: Preview changes without writing to disk

### create-component
Create reusable UI components.

```bash
pnpm textor create-component <ComponentName> [options]
```

**Options:**
- --preset <name>: scaffolding preset (minimal, standard, senior)
- --dry-run: Preview changes without writing to disk

### move-section
Move and rename sections safely. Textor performs **deep renaming**: if the feature name changes, all internal files and component signatures are updated automatically.

```bash
# Using state lookup
pnpm textor move-section /old /new
```

### rename
Rename a route, feature, or component. Ideal for correcting typos. Supports **deep renaming** and optional repo-wide import updates.

```bash
# Rename a route (URL)
pnpm textor rename route /lgoin /login

# Rename a feature module (includes internal files and component names)
pnpm textor rename feature auth/lgoin auth/login

# Rename a shared component
pnpm textor rename component Header SiteHeader
```

Use `--scan` to update imports across the entire project.

### add
Add a new sub-item (api, hook, test, etc.) to an existing feature or component. This command is additive and will not overwrite existing files unless `--force` is used.

```bash
# Add API to an existing feature
pnpm textor add api auth/login

# Add hooks to an existing component
pnpm textor add hook Button

# Add tests and readme to a feature
pnpm textor add test auth/login
pnpm textor add readme auth/login
```

Textor automatically detects if the target is a feature or a component based on its state.

You can also use the original `add-section` or `create-component` commands with the same flags to add items; they now also support additive mode.

### remove-section / remove-component
Safely remove Textor-managed modules.

```bash
# Remove by route
pnpm textor remove-section /users

# Remove a standalone feature by its name or path
pnpm textor remove-section auth/login
```

### list-sections
List all Textor-managed modules, including their architectural capabilities (API, Hooks, etc.).

### status
Show drift between state and disk. This is a read-only command used to build trust and identify manual changes or missing files.

```bash
pnpm textor status
```

**Categories:**
- **SYNCED**: File content matches state exactly.
- **MODIFIED**: File exists but content differs from state hash.
- **MISSING**: File is registered in state but missing on disk.
- **UNTRACKED**: File has a Textor signature but is not in state.
- **ORPHANED**: File is in a managed directory but has no Textor signature and is not in state.

### validate-state
Validate that the state file matches the project files.

### sync
Synchronize the state with the actual files in managed directories. This is useful for including existing files into Textor's state or updating hashes after manual edits.

```bash
pnpm textor sync [options]
```

**Options:**
- `--include-all`: Include all files in managed directories, even without the Textor signature.
- `--force`: Update hashes for modified files even if they don't have the Textor signature.
- `--dry-run`: Preview what would be synchronized without making changes.

### adopt
Adopt untracked files into Textor's state and add the Textor signature to them. This is the recommended way to bring manually created components or sections under Textor's management.

```bash
pnpm textor adopt <path-or-identifier> [options]
pnpm textor adopt component <name> [options]
pnpm textor adopt feature <path> <name> [options]
```

**Options:**
- `--all`: Scan all managed directories for untracked files and adopt them.
- `--dry-run`: Preview which files would be adopted and modified.

**Examples:**
```bash
# Adopt a specific component directory
pnpm textor adopt src/components/MyNewButton

# Adopt a component by its name
pnpm textor adopt component MyNewButton

# Adopt a directory as a new feature (moves files to src/features/my-feat)
pnpm textor adopt feature path/to/legacy-code my-feat

# Adopt a section by its route
pnpm textor adopt /users

# Adopt all untracked files in the project
pnpm textor adopt --all
```

### upgrade-config
Upgrade `.textor/config.json` to the latest schema version without recreating it.

```bash
pnpm textor upgrade-config
```

**Options:**
- `--dry-run`: Print the upgraded config without writing it.

### normalize-state
Normalize `.textor/state.json` to use project-relative paths (helpful when moving between machines).

```bash
pnpm textor normalize-state
```

**Options:**
- `--dry-run`: Print the normalized state without writing it.

### prune-missing
Remove missing references from Textor state. This command identifies files that are tracked in the state but are no longer present on disk and removes them from `.textor/state.json`. 

**Safe-by-default**: This command never deletes any files from your disk; it only updates the state tracker.

```bash
pnpm textor prune-missing
```

**Options:**
- `--dry-run`: Preview what would be removed from the state without making changes.
- `--yes`: Skip confirmation prompt.
- `--no-interactive`: Disable interactive prompts (useful for CI).

## 🏗️ Technical Architecture

Textor is designed with enterprise-grade robustness, moving beyond simple scaffolding to provide a reliable refactoring engine.

### 1. Atomic State Operations
State updates to `.textor/state.json` are atomic. Textor writes to a temporary file (`state.json.tmp`) and performs a cross-platform rename to the final destination. This prevents state corruption during crashes or interrupted operations.

### 2. Robust Hashing & Normalization
Textor uses SHA-256 for file integrity. It supports multiple normalization strategies:
- **normalizeEOL** (Default): Converts `\r\n` to `\n` before hashing. Ensures Git "autocrlf" settings don't trigger false alerts.
- **stripGeneratedRegions**: Hashes only the content within `@generated by Textor:begin` and `@generated by Textor:end` markers. This allows developers to add hand-written code outside these regions without breaking Textor's integrity checks.
- **none**: Strict hashing of the entire file.

### 3. Explicit Ownership Model
Every file in the state has an **owner**. 
- A **Section** owns its route adapter and its associated feature directory.
- A **Component** owns its specific component directory.
Textor enforces ownership boundaries to prevent accidental deletion of shared resources and to ensure deep refactors only touch relevant files. Use `--force` to override ownership checks.

### 4. Kind Inference & Rules
Textor can infer the "kind" of a file (e.g., `route`, `feature`, `component-file`) during synchronization. You can define custom rules in `.textor/config.json`:
```json
{
  "kindRules": [
    { "match": "src/features/custom/**", "kind": "custom-logic" },
    { "match": "**/special.ts", "kind": "special-file" }
  ]
}
```

### 5. Git Safety Integration
Textor integrates with Git to provide a "safety net":
- `requireCleanRepo`: When enabled, Textor refuses to perform destructive operations (remove/move) if the repository has uncommitted changes.
- `stageChanges`: When enabled, Textor automatically stages (`git add`) all created or modified files after a successful command.

### 6. Scoped & Repo-wide Import Rewriting
When moving or renaming sections, Textor performs scoped AST-like updates:
- **Scope**: Updates imports in Textor-managed files and route adapters by default.
- **Repo-wide**: Use the `--scan` flag to scan the entire repository for imports that need to be updated.
- **Exclusions**: String literals, markdown documentation (unless registered), and complex dynamic imports are preserved to avoid breaking hand-written logic.

---

## ⚙️ Configuration

The .textor/config.json file allows full control over the tool's behavior.
`configVersion` tracks schema changes and is updated by `textor upgrade-config`.

```json
{
  "configVersion": 2,
  "paths": {
    "pages": "src/pages",
    "features": "src/features",
    "components": "src/components",
    "layouts": "src/layouts"
  },
  "routing": {
    "mode": "flat",
    "indexFile": "index.astro"
  },
  "importAliases": {
    "layouts": "@/layouts",
    "features": "@/features"
  },
  "naming": {
    "routeExtension": ".astro",
    "featureExtension": ".astro",
    "componentExtension": ".tsx",
    "hookExtension": ".ts",
    "testExtension": ".test.tsx"
  },
  "signatures": {
    "astro": "<!-- @generated by Textor -->",
    "typescript": "// @generated by Textor",
    "javascript": "// @generated by Textor",
    "tsx": "// @generated by Textor"
  },
  "features": {
    "framework": "astro",
    "entry": "pascal",
    "createSubComponentsDir": true,
    "createScriptsDir": true,
    "scriptsIndexFile": "scripts/index.ts",
    "createApi": false,
    "createServices": false,
    "createSchemas": false,
    "createHooks": false,
    "createContext": false,
    "createTests": false,
    "createTypes": false,
    "createReadme": false,
    "createStories": false,
    "createIndex": false,
    "layout": "Main"
  },
  "components": {
    "framework": "react",
    "createSubComponentsDir": true,
    "createContext": true,
    "createHook": true,
    "createTests": true,
    "createConfig": true,
    "createConstants": true,
    "createTypes": true,
    "createApi": false,
    "createServices": false,
    "createSchemas": false,
    "createReadme": false,
    "createStories": false
  },
  "formatting": {
    "tool": "none"
  },
  "hashing": {
    "normalization": "normalizeEOL"
  },
  "git": {
    "requireCleanRepo": false,
    "stageChanges": false
  },
  "defaultPreset": "standard",
  "presets": {
    "minimal": {
      "features": { "createSubComponentsDir": false, "createScriptsDir": false },
      "components": {
        "createSubComponentsDir": false,
        "createContext": false,
        "createHook": false,
        "createTests": false,
        "createConfig": false,
        "createConstants": false,
        "createTypes": false
      }
    },
    "standard": {
      "features": { "createSubComponentsDir": true, "createScriptsDir": true },
      "components": {
        "createSubComponentsDir": true,
        "createContext": true,
        "createHook": true,
        "createTests": true,
        "createConfig": true,
        "createConstants": true,
        "createTypes": true
      }
    },
    "senior": {
      "features": {
        "createSubComponentsDir": true,
        "createScriptsDir": true,
        "createApi": true,
        "createServices": true,
        "createSchemas": true,
        "createHooks": true,
        "createContext": true,
        "createTests": true,
        "createTypes": true,
        "createReadme": true,
        "createStories": true,
        "createIndex": true
      },
      "components": {
        "createSubComponentsDir": true,
        "createContext": true,
        "createHook": true,
        "createTests": true,
        "createConfig": true,
        "createConstants": true,
        "createTypes": true,
        "createApi": true,
        "createServices": true,
        "createSchemas": true,
        "createReadme": true,
        "createStories": true
      }
    }
  }
}
```
*Supported formatting tools: prettier, biome, none.*

### 7. Layout Parameters

You can pass parameters to your layout component by defining `layoutProps` in `.textor/config.json`. These props support variable substitution.

```json
{
  "features": {
    "layout": "AppLayout",
    "layoutProps": {
      "title": "{{componentName}}",
      "description": "Description for {{componentName}}"
    }
  }
}
```

You can also override these props via the CLI using the `--prop` flag:
```bash
pnpm textor add-section /users users/roles --prop title="Custom Title" --prop breadcrumbs='{[{ label: "Users" }]}'
```

Properties that start and end with curly braces `{}` are passed as JavaScript expressions, others as strings.

---

## 📝 Template Overrides

You can customize the code generated by Textor by providing your own templates. Textor looks for override files in the `.textor/templates/` directory at your project root.

### How to use Template Overrides

1.  Create the `.textor/templates/` directory if it doesn't exist.
2.  Create a file named according to the table below (e.g., `feature.astro` or `component.tsx`).
3.  Use `{{variable}}` or `__variable__` placeholders in your template. Textor will automatically replace them when generating files. Using `__variable__` (e.g., `__componentName__`) is recommended for TypeScript/JavaScript templates as it is a valid identifier and avoids "broken code" warnings in your IDE.

### Supported Templates

| Template Name | File to create in `.textor/templates/` | Available Variables |
| :--- | :--- | :--- |
| **Route** | `route.astro` | `{{layoutName}}`, `{{layoutImportPath}}`, `{{featureImportPath}}`, `{{featureComponentName}}`, plus any `layoutProps` |
| **Feature** | `feature.astro` or `feature.tsx` | `{{componentName}}`, `{{scriptImportPath}}` |
| **Component** | `component.astro` or `component.tsx` | `{{componentName}}` |
| **Hook** | `hook.ts` | `{{componentName}}`, `{{hookName}}` |
| **Context** | `context.tsx` | `{{componentName}}` |
| **Test** | `test.tsx` | `{{componentName}}`, `{{componentPath}}` |
| **Index** | `index.ts` | `{{componentName}}`, `{{componentExtension}}` |
| **Types** | `types.ts` | `{{componentName}}` |
| **API** | `api.ts` | `{{componentName}}` |
| **Endpoint** | `endpoint.ts` | `{{componentName}}` |
| **Service** | `service.ts` | `{{componentName}}` |
| **Schema** | `schema.ts` | `{{componentName}}` |
| **Readme** | `readme.md` | `{{componentName}}` |
| **Stories** | `stories.tsx` | `{{componentName}}`, `{{componentPath}}` |
| **Config** | `config.ts` | `{{componentName}}` |
| **Constants** | `constants.ts` | `{{componentName}}` |
| **Scripts Index**| `scripts-index.ts` | (none) |

> **Note:** For `feature` and `component` templates, use the extension that matches your configured framework (`.astro` for Astro, `.tsx` for React). Other templates have fixed extensions for the override file, regardless of your project's configuration.

### Variables Description

- `{{componentName}}`: The PascalCase name of the feature or component (e.g., `UserCatalog`).
- `{{componentNameCamel}}`: camelCase version of the name (e.g., `userCatalog`).
- `{{componentNameKebab}}`: kebab-case version of the name (e.g., `user-catalog`).
- `{{componentNameSnake}}`: snake_case version of the name (e.g., `user_catalog`).
- `{{componentNameUpper}}`: SCREAMING_SNAKE_CASE version of the name (e.g., `USER_CATALOG`).
- `{{hookName}}`: The camelCase name of the generated hook (e.g., `useUserCatalog`).
- `{{componentPath}}`: Relative path to the component file (useful for imports in tests or stories).
- `{{featureComponentName}}`: The name of the feature component as imported in a route.
- `{{featureComponentNameCamel}}`, `{{featureComponentNameKebab}}`, `{{featureComponentNameSnake}}`, `{{featureComponentNameUpper}}`, `{{featureComponentNamePascal}}`: Case variations for the feature component name.
- `{{layoutName}}`: The name of the layout component being used.
- `{{layoutNameCamel}}`, `{{layoutNameKebab}}`, `{{layoutNameSnake}}`, `{{layoutNameUpper}}`, `{{layoutNamePascal}}`: Case variations for the layout name.
- `{{layoutImportPath}}`: The import path for the layout component.
- `{{scriptImportPath}}`: Relative path to the client-side script entry point.
- `{{componentExtension}}`: The file extension of the component (e.g., `.astro` or `.tsx`).

### Example: Custom Route Template (`.textor/templates/route.ts`)

If you are using a custom routing library and want your templates to be valid TypeScript:

```typescript
/**
 * @generated by Textor
 * Route: {{featureComponentName}}
 */
import { defineRoute } from "my-router";
import __featureComponentName__ from "{{featureImportPath}}";

export const __featureComponentName__Route = defineRoute({
  path: "/__featureComponentNameKebab__",
  component: __featureComponentName__
});
```

### Example: Custom Feature Template (`.textor/templates/feature.astro`)

```astro
---
/**
 * @generated by Textor
 * Feature: {{componentName}}
 */

interface Props {
  title?: string;
}

const { title = "{{componentName}}" } = Astro.props;
---

<section class="feature-{{componentName}}">
  <h2>{title}</h2>
  <slot />
</section>

<script src="{{scriptImportPath}}"></script>

<style>
  .feature-{{componentName}} {
    padding: 2rem;
  }
</style>
```

---

Textor is designed to be a tool you trust to refactor a 3-year-old production codebase without fear.