npm package: @hwy-fm/cli
Description: Build toolchain and CLI for `@hwy-fm` applications — project scaffolding, dev server with HMR, production builds, and interactive REPL.
Author: hwyn
Latest version: 0.1.3

# @hwy-fm/cli

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-Strict-blue.svg)](https://www.typescriptlang.org/)

Build toolchain and CLI for `@hwy-fm` applications — project scaffolding, dev server with HMR, production builds, and interactive REPL.

## Quick Start

```bash
npm install -g @hwy-fm/cli
hwy create my-app
cd my-app && npm install && hwy start
```

## Commands

| Command | Description |
|---------|-------------|
| `hwy create <name> --template <url>` | Scaffold a new project from a git template |
| `hwy start` | Start dev server with hot module replacement |
| `hwy build` | Production build + generate deployment `package.json` |
| `hwy bundle` | Production bundle only (no deployment `package.json`) |
| `hwy clean` | Remove build output directories |
| `hwy repl` | Interactive REPL for script-mode commands |
| `hwy version` | Print CLI version |
| `hwy help` | Print usage and available commands |

### Global Options

| Option | Description |
|--------|-------------|
| `--project <path>` | Project root directory (default: cwd) |
| `--env <name>` | Target environment (e.g. `production`, `staging`) |
| `--verbose` | Enable detailed logging |
| `--debug` | Enable pipeline tracing and topology output |
| `--bundler <name>` | Bundler to use (default: `webpack`) |
| `--no_clean` | Skip output directory cleanup before build/bundle |

## Project Configuration

Create a `project.config.ts` (or `.js`, `.json`) at the project root:

```typescript
import { defineConfig } from '@hwy-fm/cli';

export default defineConfig({
  root: '.',
  source: 'src',
  output: 'dist',

  client: {
    entry: 'app/main.ts',
    index: 'index.html',
    port: 4200,
    proxy: [
      { host: 'http://localhost:3000', proxyApi: '/api' },
    ],
  },

  server: {
    entry: 'server/main.ts',
  },

  env: {
    production: {
      sourceMap: false,
      client: { publicPath: '/cdn/' },
    },
  },
});
```

### Shared Options

| Option | Default | Description |
|--------|---------|-------------|
| `root` | `'.'` | Project root directory (relative to cwd) |
| `source` | `'src'` | Source directory relative to root |
| `output` | `'dist'` | Output directory relative to root |
| `nodeModules` | `'node_modules'` | node_modules path relative to root |
| `tsConfig` | `'tsconfig.json'` | TypeScript config path |
| `sourceMap` | `false` | Enable source maps |
| `resolveAlias` | — | Module resolve aliases, e.g. `{ '@app': 'src/app' }` |
| `define` | — | Compile-time constants injected via DefinePlugin |
| `minimize` | `true` | Minify production output |

### Platforms

The CLI builds up to four platforms in a single project. Set any platform to `false` to disable it.

| Platform | Config Key | Description |
|----------|------------|-------------|
| Client | `client` | Browser bundle — entry, HTML template, styles, assets |
| Server | `server` | Node.js SSR bundle — supports hot reload |
| Server Entry | `serverEntry` | SSR entry for client-side hydration |
| DLL | `dll` | Vendor pre-bundle — shared dependencies |

#### Client

```typescript
client: {
  entry: 'app/main.ts',
  index: 'index.html',
  publicPath: '/',
  port: 3000,
  assets: ['assets/'],
  styles: ['styles/global.scss'],
  themeVariable: 'styles/variables.scss',
  browserTarget: '> 1%, last 4 versions',
  proxy: { '/api': 'http://localhost:4000' },
  templateContent: (params) => `<!DOCTYPE html>...`,
  styleLoaderOptions: { attributes: { nonce: 'abc' } },
}
```

#### Server

```typescript
server: {
  entry: 'server/main.ts',
  outputFormat: 'cjs',           // 'cjs' | 'esm'
  nodeExternals: true,           // keep node_modules out of bundle
  nodeExternalsAllowlist: [],    // packages to include despite nodeExternals
  hotContext: 'server.context',  // hot-reload context module
  watchFile: ['config/'],        // extra paths to watch for restart
  buildAssets(platforms, readSource) {
    const client = platforms['client'];
    return {
      scripts: client.entrypoints.main?.js || [],
      inlineStyles: (client.entrypoints.main?.css || []).map(readSource),
    };
  },
}
```

#### DLL

```typescript
dll: {
  entry: {
    vendor: ['react', 'react-dom'],
    charts: ['echarts'],           // built after vendor
    shared: ['./src/shared/index.ts'],
  },
}
```

### Environment Overrides

```typescript
env: {
  production: {
    sourceMap: false,
    minimize: true,
    client: { publicPath: '/cdn/' },
    configureWebpack: (config, { platform, mode }) => { /* ... */ },
  },
  staging: {
    client: { publicPath: '/staging/' },
  },
}
```

Merge strategy: `extends → base → env.development (if start) → env[targetEnv]`

### Config Extension

```typescript
// project.config.ts
export default defineConfig({
  extends: './base.config.ts',
  client: { port: 4200 },
});
```

### configureWebpack

Modify the final webpack configuration for every platform:

```typescript
export default defineConfig({
  configureWebpack(config, { platform, mode }) {
    if (platform === 'client') {
      config.devtool = 'eval-source-map';
    }
    // return void to mutate in-place, or return a new object to replace
  },
});
```

## Plugin System

### Custom Commands

Register additional CLI commands via plugins:

```typescript
// plugins/info.ts
import { defineCmd } from '@hwy-fm/cli';

defineCmd({
  name: 'info',
  description: 'Print project information',
  options: [
    { flag: '--all, -a', desc: 'Show all details' },
  ],
  execute(ctx) {
    console.log('Project info...');
    ctx.result = { exitCode: 0 };
  },
});
```

Load plugins in your config:

```typescript
export default defineConfig({
  plugins: [
    './plugins/info.ts',
    '@my-scope/cli-plugin-lint',
  ],
});
```

### Custom Bundler

Replace or add a bundler strategy:

```typescript
import { defineBundlerStrategy } from '@hwy-fm/cli';
import { KernelModel } from '@hwy-fm/kernel';

const MyBundlerModel = KernelModel();

// Register seeds and instructions on MyBundlerModel for 'build' and 'start'...

defineBundlerStrategy('my-bundler', MyBundlerModel);
```

Then use it: `hwy build --bundler my-bundler`

### Custom Webpack Rules

Extend webpack's module rule pipeline:

```typescript
import { defineRuleStrategy } from '@hwy-fm/cli/webpack';

defineRuleStrategy((context) => ({
  test: /\.svg$/,
  type: 'asset/resource',
}));
```

Return a `{ rules, plugins }` object when the strategy also needs plugins:

```typescript
import { defineRuleStrategy } from '@hwy-fm/cli/webpack';
import { VueLoaderPlugin } from 'vue-loader';

defineRuleStrategy((context) => ({
  rules: { test: /\.vue$/, loader: 'vue-loader' },
  plugins: new VueLoaderPlugin(),
}));
```

## REPL

Two ways to enter interactive mode:

### Direct REPL

```bash
hwy repl
```

Enters REPL mode — dispatches input to user-defined seeds (not build/start/bundle/clean):

```
hwy> exit
```

### Script REPL

```bash
hwy my-app.ts
```

When the first argument is a `.ts`, `.js`, or `.mjs` file, the CLI:
1. Loads the file (side-effect imports register seeds/instructions into the kernel)
2. Automatically enters REPL in **script mode**, dispatching commands to the script's kernel

This lets you write a standalone executable file:

```typescript
// my-app.ts
import { Kernel } from '@hwy-fm/kernel';
import '@hwy-fm/std';

@Kernel.Process.seed('hello')
class HelloSeed {
  execute(ctx) {
    console.log('Hello from script!');
  }
}

@Kernel.Process.seed('greet')
class GreetSeed {
  execute(ctx) {
    const name = ctx.input.args?.[0] || 'World';
    console.log(`Hi, ${name}!`);
  }
}

@Kernel.bootstrap()
class App {}
```

Run it:

```bash
hwy my-app.ts
```

```
🧩 REPL [script] — type "exit" to quit
hwy> hello
Hello from script!
hwy> greet Alice
Hi, Alice!
hwy> exit
```

### REPL Features

| Feature | Example |
|---------|---------|
| Chain commands | `hello && greet Alice` |
| Pipe input | `echo "hello" \| hwy repl` |
| Clear screen | `cls` or `clear` |
| Exit | `exit` or `quit` |

Piped input mode (non-TTY) runs commands line by line and exits automatically.

## Dev Server

`hwy start` compiles your code, starts a dev server, and hot-reloads on file changes.

### Modes

| Mode | Activation | How it works |
|------|-----------|--------------|
| **VM** (default) | — | Runs compiled server code in `vm.runInNewContext`. Hot-reload is instant (no process restart). |
| **Nodemon** | `--nodemon` flag | Spawns a child process with `tsx`/`ts-node`. Restarts on file changes via chokidar. Supports `--inspect` debugging. |

### Server-only mode

If your project has no `client` config, `hwy start` skips the Express dev server entirely — no port 3000, no proxy. The server runs standalone, and file-watch / hot-reload still works.

### The `hotReload()` contract (VM mode)

In VM mode, the CLI injects a global `hotReload` function into your server code. Your server must call it once listening is ready:

```typescript
const server = http.createServer(app);
server.listen(3001, () => {
  hotReload({
    hotHost: 'http://localhost:3001',  // proxy target (optional)
    hotReload: () => server.close(),   // cleanup before next reload
  });
});
```

| Field | Required | Description |
|-------|----------|-------------|
| `hotHost` | No | The address to reverse-proxy from the dev server. Omit for non-HTTP protocols (WebSocket, TCP, gRPC). |
| `hotReload` | No | Cleanup callback — called before the next hot-reload cycle to release resources (close sockets, etc.). |

When `hotHost` is omitted, the dev server won't set up a reverse proxy — useful for protocols that don't need one.

### Custom server context

Create a `server.context.ts` (or configure `server.hotContext`) to inject variables into the VM sandbox or replace the default execution entirely:

```typescript
// server.context.ts
export default (config, fullConfig) => ({
  // Extra globals available in VM sandbox
  MY_CONFIG: { port: 3001 },

  // Or replace VM execution with a custom handler:
  // hotHandler: (config) => { ... return { hotHost, hotReload } }
});
```

## Architecture

The CLI is built on `@hwy-fm/kernel` + `@hwy-fm/std` — it uses the same pipeline architecture it helps you build:

```
argv
  │
  ▼
Catch      [ timing — measures build/bundle/clean/create time ]
  │
  ▼
Guard      [ load project config + plugins ]
  │
  ▼
Check      [ validate configs — platforms, entries, paths ]
  │
  ▼
Process    [ command seed — create / start / build / ... ]
  │
  ▼
Deliver    [ set process.exitCode ]
  │
  ▼
Trace      [ debug output — topology, traces ]
```

Build commands (`start`, `build`, `bundle`, `clean`) dispatch into a **separate bundler sub-kernel** (e.g. webpack), maintaining physical isolation between CLI orchestration and bundler logic.

## Public API

```typescript
import {
  defineConfig,          // Type-safe project config helper
  defineCmd,             // Register a custom CLI command
  CliKernel,             // The CLI Kernel model instance
  CLI_EXT_COMMANDS,      // DI token for plugin commands
  BUNDLER_STRATEGY,      // DI token for bundler strategies
  defineBundlerStrategy, // Register a custom bundler
} from '@hwy-fm/cli';

import {
  defineRuleStrategy,    // Register a custom webpack rule
  RULE_STRATEGY,         // DI token for webpack rules
} from '@hwy-fm/cli/webpack';
```

## Related Packages

| Package | Description |
|---------|-------------|
| [`@hwy-fm/kernel`](https://www.npmjs.com/package/@hwy-fm/kernel) | The pipeline engine powering the CLI |
| [`@hwy-fm/std`](https://www.npmjs.com/package/@hwy-fm/std) | Pre-configured Slots + Gateway |
| [`@hwy-fm/di`](https://www.npmjs.com/package/@hwy-fm/di) | Dependency injection |

## License

MIT © [hwyn](https://github.com/hwyn)