refactor: unwrap BashArity namespace to flat exports + self-reexport (#22874)

This commit is contained in:
Kit Langton
2026-04-16 15:24:24 -04:00
committed by GitHub
parent 7c1b30291c
commit 219b473e66
2 changed files with 309 additions and 596 deletions

View File

@@ -1,499 +1,212 @@
# Namespace → flat export migration
# Namespace → self-reexport migration
Migrate `export namespace` to the `export * as` / flat-export pattern used by
effect-smol. Primary goal: tree-shakeability. Secondary: consistency with Effect
conventions, LLM-friendliness for future migrations.
## What changes and what doesn't
The **consumer API stays the same**. You still write `Provider.ModelNotFoundError`,
`Config.JsonError`, `Bus.publish`, etc. The namespace ergonomics are preserved.
What changes is **how** the namespace is constructed — the TypeScript
`export namespace` keyword is replaced by `export * as` in a barrel file. This
is a mechanical change: unwrap the namespace body into flat exports, add a
one-line barrel. Consumers that import `{ Provider }` don't notice.
Import paths actually get **nicer**. Today most consumers import from the
explicit file (`"../provider/provider"`). After the migration, each module has a
barrel `index.ts`, so imports become `"../provider"` or `"@/provider"`:
Migrate every `export namespace Foo { ... }` to flat top-level exports plus a
single self-reexport line at the bottom of the same file:
```ts
// BEFORE — points at the file directly
import { Provider } from "../provider/provider"
// AFTER — resolves to provider/index.ts, same Provider namespace
import { Provider } from "../provider"
export * as Foo from "./foo"
```
## Why this matters right now
No barrel `index.ts` files. No cross-directory indirection. Consumers keep the
exact same `import { Foo } from "../foo/foo"` ergonomics.
The CLI binary startup time (TOI) is too slow. Profiling shows we're loading
massive dependency graphs that are never actually used at runtime — because
bundlers cannot tree-shake TypeScript `export namespace` bodies.
## Why this pattern
### The problem in one sentence
`cli/error.ts` needs 6 lightweight `.isInstance()` checks on error classes, but
importing `{ Provider }` from `provider.ts` forces the bundler to include **all
20+ `@ai-sdk/*` packages**, `@aws-sdk/credential-providers`,
`google-auth-library`, and every other top-level import in that 1709-line file.
### Why `export namespace` defeats tree-shaking
TypeScript compiles `export namespace Foo { ... }` to an IIFE:
```js
// TypeScript output
export var Provider;
(function (Provider) {
Provider.ModelNotFoundError = NamedError.create(...)
// ... 1600 more lines of assignments ...
})(Provider || (Provider = {}))
```
This is **opaque to static analysis**. The bundler sees one big function call
whose return value populates an object. It cannot determine which properties are
used downstream, so it keeps everything. Every `import` statement at the top of
`provider.ts` executes unconditionally — that's 20+ AI SDK packages loaded into
memory just so the CLI can check `Provider.ModelNotFoundError.isInstance(x)`.
### What `export * as` does differently
`export * as Provider from "./provider"` compiles to a static re-export. The
bundler knows the exact shape of `Provider` at compile time — it's the named
export list of `./provider.ts`. When it sees `Provider.ModelNotFoundError` used
but `Provider.layer` unused, it can trace that `ModelNotFoundError` doesn't
reference `createAnthropic` or any AI SDK import, and drop them. The namespace
object still exists at runtime — same API — but the bundler can see inside it.
### Concrete impact
The worst import chain in the codebase:
We tested three options against Bun, esbuild, Rollup (what Vite uses under the
hood), Bun's runtime, and Node's native TypeScript runner.
```
src/index.ts (entry point)
heavy.ts loaded?
A. namespace B. barrel C. self-reexport
Bun bundler YES YES no
esbuild YES YES no
Rollup (Vite) YES YES no
Bun runtime YES YES no
Node --experimental-strip-types SYNTAX ERROR YES no
```
- **`export namespace`** compiles to an IIFE. Bundlers see one opaque function
call and can't analyze what's used. Node's native TS runner rejects the
syntax outright: `SyntaxError: TypeScript namespace declaration is not
supported in strip-only mode`.
- **Barrel `index.ts`** files (`export * as Foo from "./foo"` in a separate
file) force every re-exported sibling to evaluate when you import one name.
Siblings with side effects (top-level imports of SDKs, etc.) always load.
- **Self-reexport** keeps the file as plain ESM. Bundlers see static named
exports. The module is only pulled in when something actually imports from
it. There is no barrel hop, so no sibling contamination and no circular
import hazard.
Bundle overhead for the self-reexport wrapper is roughly 240 bytes per module
(`Object.defineProperty` namespace proxy). At ~100 modules that's ~24KB —
negligible for a CLI binary.
## The pattern
### Before
```ts
// src/permission/arity.ts
export namespace BashArity {
export function prefix(tokens: string[]) { ... }
}
```
### After
```ts
// src/permission/arity.ts
export function prefix(tokens: string[]) { ... }
export * as BashArity from "./arity"
```
Consumers don't change at all:
```ts
import { BashArity } from "@/permission/arity"
BashArity.prefix(...) // still works
```
Editors still auto-import `BashArity` like any named export, because the file
does have a named `BashArity` export at the module top level.
### Odd but harmless
`BashArity.BashArity.BashArity.prefix(...)` compiles and runs because the
namespace contains a re-export of itself. Nobody would write that. Not a
problem.
## Why this is different from what we tried first
An earlier pass used sibling barrel files (`index.ts` with `export * as ...`).
That turned out to be wrong for our constraints:
1. The barrel file always loads all its sibling modules when you import
through it, even if you only need one. For our CLI this is exactly the
cost we're trying to avoid.
2. Barrel + sibling imports made it very easy to accidentally create circular
imports that only surface as `ReferenceError` at runtime, not at
typecheck.
The self-reexport has none of those issues. There is no indirection. The
file and the namespace are the same unit.
## Why this matters for startup
The worst import chain in the codebase looks like:
```
src/index.ts
└── FormatError from src/cli/error.ts
├── { Provider } from provider/provider.ts (1709 lines)
├── { Provider } from provider/provider.ts (~1700 lines)
│ ├── 20+ @ai-sdk/* packages
│ ├── @aws-sdk/credential-providers
│ ├── google-auth-library
── gitlab-ai-provider, venice-ai-sdk-provider
│ └── fuzzysort, remeda, etc.
── { Config } from config/config.ts (1663 lines)
│ ├── jsonc-parser
│ ├── LSPServer (all server definitions)
│ └── Plugin, Auth, Env, Account, etc.
└── { MCP } from mcp/index.ts (930 lines)
├── @modelcontextprotocol/sdk (3 transports)
└── open (browser launcher)
── more
├── { Config } from config/config.ts (~1600 lines)
── { MCP } from mcp/mcp.ts (~900 lines)
```
All of this gets pulled in to check `.isInstance()` on 6 error classes — code
that needs maybe 200 bytes total. This inflates the binary, increases startup
memory, and slows down initial module evaluation.
### Why this also hurts memory
Every module-level import is eagerly evaluated. Even with Bun's fast module
loader, evaluating 20+ AI SDK factory functions, the AWS credential chain, and
Google's auth library allocates objects, closures, and prototype chains that
persist for the lifetime of the process. Most CLI commands never use a provider
at all.
## What effect-smol does
effect-smol achieves tree-shakeable namespaced APIs via three structural choices.
### 1. Each module is a separate file with flat named exports
```ts
// Effect.ts — no namespace wrapper, just flat exports
export const gen: { ... } = internal.gen
export const fail: <E>(error: E) => Effect<never, E> = internal.fail
export const succeed: <A>(value: A) => Effect<A> = internal.succeed
// ... 230+ individual named exports
```
### 2. Barrel file uses `export * as` (not `export namespace`)
```ts
// index.ts
export * as Effect from "./Effect.ts"
export * as Schema from "./Schema.ts"
export * as Stream from "./Stream.ts"
// ~134 modules
```
This creates a namespace-like API (`Effect.gen`, `Schema.parse`) but the
bundler knows the **exact shape** at compile time — it's the static export list
of that file. It can trace property accesses (`Effect.gen` → keep `gen`,
drop `timeout` if unused). With `export namespace`, the IIFE is opaque and
nothing can be dropped.
### 3. `sideEffects: []` and deep imports
```jsonc
// package.json
{ "sideEffects": [] }
```
Plus `"./*": "./src/*.ts"` in the exports map, enabling
`import * as Effect from "effect/Effect"` to bypass the barrel entirely.
### 4. Errors as flat exports, not class declarations
```ts
// Cause.ts
export const NoSuchElementErrorTypeId = core.NoSuchElementErrorTypeId
export interface NoSuchElementError extends YieldableError { ... }
export const NoSuchElementError: new(msg?: string) => NoSuchElementError = core.NoSuchElementError
export const isNoSuchElementError: (u: unknown) => u is NoSuchElementError = core.isNoSuchElementError
```
Each error is 4 independent exports: TypeId, interface, constructor (as const),
type guard. All individually shakeable.
## The plan
The core migration is **Phase 1** — convert `export namespace` to
`export * as`. Once that's done, the bundler can tree-shake individual exports
within each module. You do NOT need to break things into subfiles for
tree-shaking to work — the bundler traces which exports you actually access on
the namespace object and drops the rest, including their transitive imports.
Splitting errors/schemas into separate files (Phase 0) is optional — it's a
lower-risk warmup step that can be done before or after the main conversion, and
it provides extra resilience against bundler edge cases. But the big win comes
from Phase 1.
### Phase 0 (optional): Pre-split errors into subfiles
This is a low-risk warmup that provides immediate benefit even before the full
`export * as` conversion. It's optional because Phase 1 alone is sufficient for
tree-shaking. But it's a good starting point if you want incremental progress:
**For each namespace that defines errors** (15 files, ~30 error classes total):
1. Create a sibling `errors.ts` file (e.g. `provider/errors.ts`) with the error
definitions as top-level named exports:
```ts
// provider/errors.ts
import z from "zod"
import { NamedError } from "@opencode-ai/shared/util/error"
import { ProviderID, ModelID } from "./schema"
export const ModelNotFoundError = NamedError.create(
"ProviderModelNotFoundError",
z.object({
providerID: ProviderID.zod,
modelID: ModelID.zod,
suggestions: z.array(z.string()).optional(),
}),
)
export const InitError = NamedError.create("ProviderInitError", z.object({ providerID: ProviderID.zod }))
```
2. In the namespace file, re-export from the errors file to maintain backward
compatibility:
```ts
// provider/provider.ts — inside the namespace
export { ModelNotFoundError, InitError } from "./errors"
```
3. Update `cli/error.ts` (and any other light consumers) to import directly:
```ts
// BEFORE
import { Provider } from "../provider/provider"
Provider.ModelNotFoundError.isInstance(input)
// AFTER
import { ModelNotFoundError as ProviderModelNotFoundError } from "../provider/errors"
ProviderModelNotFoundError.isInstance(input)
```
**Files to split (Phase 0):**
| Current file | New errors file | Errors to extract |
| ----------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `provider/provider.ts` | `provider/errors.ts` | ModelNotFoundError, InitError |
| `provider/auth.ts` | `provider/auth-errors.ts` | OauthMissing, OauthCodeMissing, OauthCallbackFailed, ValidationFailed |
| `config/config.ts` | (already has `config/paths.ts`) | ConfigDirectoryTypoError → move to paths.ts |
| `config/markdown.ts` | `config/markdown-errors.ts` | FrontmatterError |
| `mcp/index.ts` | `mcp/errors.ts` | Failed |
| `session/message-v2.ts` | `session/message-errors.ts` | OutputLengthError, AbortedError, StructuredOutputError, AuthError, APIError, ContextOverflowError |
| `session/message.ts` | (shares with message-v2) | OutputLengthError, AuthError |
| `cli/ui.ts` | `cli/ui-errors.ts` | CancelledError |
| `skill/index.ts` | `skill/errors.ts` | InvalidError, NameMismatchError |
| `worktree/index.ts` | `worktree/errors.ts` | NotGitError, NameGenerationFailedError, CreateFailedError, StartCommandFailedError, RemoveFailedError, ResetFailedError |
| `storage/storage.ts` | `storage/errors.ts` | NotFoundError |
| `npm/index.ts` | `npm/errors.ts` | InstallFailedError |
| `ide/index.ts` | `ide/errors.ts` | AlreadyInstalledError, InstallFailedError |
| `lsp/client.ts` | `lsp/errors.ts` | InitializeError |
### Phase 1: The real migration — `export namespace` → `export * as`
This is the phase that actually fixes tree-shaking. For each module:
1. **Unwrap** the `export namespace Foo { ... }` — remove the namespace wrapper,
keep all the members as top-level `export const` / `export function` / etc.
2. **Rename** the file if it's currently `index.ts` (e.g. `bus/index.ts` →
`bus/bus.ts`), so the barrel can take `index.ts`.
3. **Create the barrel** `index.ts` with one line: `export * as Foo from "./foo"`
The file structure change for a module that's currently a single file:
```
# BEFORE
provider/
provider.ts ← 1709-line file with `export namespace Provider { ... }`
# AFTER
provider/
index.ts ← NEW: `export * as Provider from "./provider"`
provider.ts ← SAME file, same name, just unwrap the namespace
```
And the code change is purely removing the wrapper:
```ts
// BEFORE: provider/provider.ts
export namespace Provider {
export class Service extends Context.Service<...>()("@opencode/Provider") {}
export const layer = Layer.effect(Service, ...)
export const ModelNotFoundError = NamedError.create(...)
export function parseModel(model: string) { ... }
}
// AFTER: provider/provider.ts — identical exports, no namespace keyword
export class Service extends Context.Service<...>()("@opencode/Provider") {}
export const layer = Layer.effect(Service, ...)
export const ModelNotFoundError = NamedError.create(...)
export function parseModel(model: string) { ... }
```
```ts
// NEW: provider/index.ts
export * as Provider from "./provider"
```
Consumer code barely changes — import path gets shorter:
```ts
// BEFORE
import { Provider } from "../provider/provider"
// AFTER — resolves to provider/index.ts, same Provider object
import { Provider } from "../provider"
```
All access like `Provider.ModelNotFoundError`, `Provider.Service`,
`Provider.layer` works exactly as before. The difference is invisible to
consumers but lets the bundler see inside the namespace.
**Once this is done, you don't need to break anything into subfiles for
tree-shaking.** The bundler traces that `Provider.ModelNotFoundError` only
depends on `NamedError` + `zod` + the schema file, and drops
`Provider.layer` + all 20 AI SDK imports when they're unused. This works because
`export * as` gives the bundler a static export list it can do inner-graph
analysis on — it knows which exports reference which imports.
**Order of conversion** (by risk / size, do small modules first):
1. Tiny utilities: `Archive`, `Color`, `Token`, `Rpc`, `LocalContext` (~7-66 lines each)
2. Small services: `Auth`, `Env`, `BusEvent`, `SessionStatus`, `SessionRunState`, `Editor`, `Selection` (~25-91 lines)
3. Medium services: `Bus`, `Format`, `FileTime`, `FileWatcher`, `Command`, `Question`, `Permission`, `Vcs`, `Project`
4. Large services: `Config`, `Provider`, `MCP`, `Session`, `SessionProcessor`, `SessionPrompt`, `ACP`
### Phase 2: Build configuration
After the module structure supports tree-shaking:
1. Add `"sideEffects": []` to `packages/opencode/package.json` (or
`"sideEffects": false`) — this is safe because our services use explicit
layer composition, not import-time side effects.
2. Verify Bun's bundler respects the new structure. If Bun's tree-shaking is
insufficient, evaluate whether the compiled binary path needs an esbuild
pre-pass.
3. Consider adding `/*#__PURE__*/` annotations to `NamedError.create(...)` calls
— these are factory functions that return classes, and bundlers may not know
they're side-effect-free without the annotation.
All of that currently gets pulled in just to do `.isInstance()` on a handful
of error classes. The namespace IIFE shape is the main reason bundlers cannot
strip the unused parts. Self-reexport + flat ESM fixes it.
## Automation
The transformation is scripted. From `packages/opencode`:
From `packages/opencode`:
```bash
bun script/unwrap-namespace.ts <file> [--dry-run]
```
The script uses ast-grep for accurate AST-based namespace boundary detection
(no false matches from braces in strings/templates/comments), then:
The script:
1. Removes the `export namespace Foo {` line and its closing `}`
2. Dedents the body by one indent level (2 spaces)
3. If the file is `index.ts`, renames it to `<name>.ts` and creates a new
`index.ts` barrel
4. If the file is NOT `index.ts`, rewrites it in place and creates `index.ts`
5. Prints the exact commands to find and rewrite import paths
1. Uses ast-grep to locate the `export namespace Foo { ... }` block accurately.
2. Removes the `export namespace Foo {` line and the matching closing `}`.
3. Dedents the body by one indent level (2 spaces).
4. Rewrites `Foo.Bar` self-references inside the file to just `Bar`.
5. Appends `export * as Foo from "./<basename>"` at the bottom of the file.
6. Never creates a barrel `index.ts`.
### Walkthrough: converting a module
Using `Provider` as an example:
### Typical flow for one file
```bash
# 1. Preview what will change
bun script/unwrap-namespace.ts src/provider/provider.ts --dry-run
# 1. Preview
bun script/unwrap-namespace.ts src/permission/arity.ts --dry-run
# 2. Apply the transformation
bun script/unwrap-namespace.ts src/provider/provider.ts
# 2. Apply
bun script/unwrap-namespace.ts src/permission/arity.ts
# 3. Rewrite import paths (script prints the exact command)
rg -l 'from.*provider/provider' src/ | xargs sed -i '' 's|provider/provider"|provider"|g'
# 4. Verify
bun typecheck
bun run test
# 3. Verify
cd packages/opencode
bunx --bun tsgo --noEmit
bun run --conditions=browser ./src/index.ts generate
bun run test <affected test files>
```
**What changes on disk:**
### Consumer imports usually don't need to change
```
# BEFORE
provider/
provider.ts ← 1709 lines, `export namespace Provider { ... }`
# AFTER
provider/
index.ts ← NEW: `export * as Provider from "./provider"`
provider.ts ← same file, namespace unwrapped to flat exports
```
**What changes in consumer code:**
Most consumers already import straight from the file, e.g.:
```ts
// BEFORE
import { Provider } from "../provider/provider"
// AFTER — shorter path, same Provider object
import { Provider } from "../provider"
import { BashArity } from "@/permission/arity"
import { Config } from "@/config/config"
```
All property access (`Provider.Service`, `Provider.ModelNotFoundError`, etc.)
stays identical.
Because the file itself now does `export * as Foo from "./foo"`, those imports
keep working with zero edits.
### Two cases the script handles
**Case A: file is NOT `index.ts`** (e.g. `provider/provider.ts`)
- Rewrites the file in place (unwrap + dedent)
- Creates `provider/index.ts` as the barrel
- Import paths change: `"../provider/provider"` → `"../provider"`
**Case B: file IS `index.ts`** (e.g. `bus/index.ts`)
- Renames `index.ts` → `bus.ts` (kebab-case of namespace name)
- Creates new `index.ts` as the barrel
- **No import rewrites needed** — `"@/bus"` already resolves to `bus/index.ts`
## Do I need to split errors/schemas into subfiles?
**No.** Once you do the `export * as` conversion, the bundler can tree-shake
individual exports within the file. If `cli/error.ts` only accesses
`Provider.ModelNotFoundError`, the bundler traces that `ModelNotFoundError`
doesn't reference `createAnthropic` and drops the AI SDK imports.
Splitting into subfiles (errors.ts, schema.ts) is still a fine idea for **code
organization** — smaller files are easier to read and review. But it's not
required for tree-shaking. The `export * as` conversion alone is sufficient.
The one case where subfile splitting provides extra tree-shake value is if an
imported package has module-level side effects that the bundler can't prove are
unused. In practice this is rare — most npm packages are side-effect-free — and
adding `"sideEffects": []` to package.json handles the common cases.
## Scope
| Metric | Count |
| ----------------------------------------------- | --------------- |
| Files with `export namespace` | 106 |
| Total namespace declarations | 118 (12 nested) |
| Files with `NamedError.create` inside namespace | 15 |
| Total error classes to extract | ~30 |
| Files using `export * as` today | 0 |
Phase 1 (the `export * as` conversion) is the main change. It's mechanical and
LLM-friendly but touches every import site, so it should be done module by
module with type-checking between each step. Each module is an independent PR.
## Rules for new code
Going forward:
- **No new `export namespace`**. Use a file with flat named exports and
`export * as` in the barrel.
- Keep the service, layer, errors, schemas, and runtime wiring together in one
file if you want — that's fine now. The `export * as` barrel makes everything
individually shakeable regardless of file structure.
- If a file grows large enough that it's hard to navigate, split by concern
(errors.ts, schema.ts, etc.) for readability. Not for tree-shaking — the
bundler handles that.
## Circular import rules
Barrel files (`index.ts` with `export * as`) introduce circular import risks.
These cause `ReferenceError: Cannot access 'X' before initialization` at
runtime — not caught by the type checker.
### Rule 1: Sibling files never import through their own barrel
Files in the same directory must import directly from the source file, never
through `"."` or `"@/<own-dir>"`:
The only edits needed are when a consumer was importing through a previous
barrel (`"@/config"` or `"../config"` resolving to `config/index.ts`). In
that case, repoint it at the file:
```ts
// BAD — circular: index.ts re-exports both files, so A → index → B → index → A
import { Sibling } from "."
// before
import { Config } from "@/config"
// GOOD — direct, no cycle
import * as Sibling from "./sibling"
// after
import { Config } from "@/config/config"
```
### Rule 2: Cross-directory imports must not form cycles through barrels
### Dynamic imports in tests
If `src/lsp/lsp.ts` imports `Config` from `"../config"`, and
`src/config/config.ts` imports `LSPServer` from `"../lsp"`, that's a cycle:
If a test did `const { Foo } = await import("../../src/x/y")`, the destructure
still works because of the self-reexport. No change required.
```
lsp/lsp.ts → config/index.ts → config/config.ts → lsp/index.ts → lsp/lsp.ts 💥
```
## Verification checklist (per PR)
Fix by importing the specific file, breaking the cycle:
```ts
// In config/config.ts — import directly, not through the lsp barrel
import * as LSPServer from "../lsp/server"
```
### Why the type checker doesn't catch this
TypeScript resolves types lazily — it doesn't evaluate module-scope
expressions. The `ReferenceError` only happens at runtime when a module-scope
`const` or function call accesses a value from a circular dependency that
hasn't finished initializing. The SDK build step (`bun run --conditions=browser
./src/index.ts generate`) is the reliable way to catch these because it
evaluates all modules eagerly.
### How to verify
After any namespace conversion, run:
Run all of these locally before pushing:
```bash
cd packages/opencode
bunx --bun tsgo --noEmit
bun run --conditions=browser ./src/index.ts generate
bun run test <affected test files>
```
If this completes without `ReferenceError`, the module graph is safe.
Also do a quick grep in `src/`, `test/`, and `script/` to make sure no
consumer is still importing the namespace from an old barrel path that no
longer exports it.
The SDK build step (`bun run --conditions=browser ./src/index.ts generate`)
evaluates every module eagerly and is the most reliable way to catch circular
import regressions at runtime — the typechecker does not catch these.
## Rules for new code
- No new `export namespace`.
- Every module file that wants a namespace gets a self-reexport at the
bottom:
`export * as Foo from "./foo"`
- Consumers import from the file itself:
`import { Foo } from "../path/to/foo"`
- No new barrel `index.ts` files for internal code.
- If a file needs a sibling, import the sibling file directly:
`import * as Sibling from "./sibling"`, not `from "."`.
## Scope
There are still dozens of `export namespace` files left across the codebase.
Each one is its own small PR. Do them one at a time, verified locally, rather
than batching by directory.

View File

@@ -1,15 +1,14 @@
export namespace BashArity {
export function prefix(tokens: string[]) {
for (let len = tokens.length; len > 0; len--) {
const prefix = tokens.slice(0, len).join(" ")
const arity = ARITY[prefix]
if (arity !== undefined) return tokens.slice(0, arity)
}
if (tokens.length === 0) return []
return tokens.slice(0, 1)
export function prefix(tokens: string[]) {
for (let len = tokens.length; len > 0; len--) {
const prefix = tokens.slice(0, len).join(" ")
const arity = ARITY[prefix]
if (arity !== undefined) return tokens.slice(0, arity)
}
if (tokens.length === 0) return []
return tokens.slice(0, 1)
}
/* Generated with following prompt:
/* Generated with following prompt:
You are generating a dictionary of command-prefix arities for bash-style commands.
This dictionary is used to identify the "human-understandable command" from an input shell command.### **RULES (follow strictly)**1. Each entry maps a **command prefix string → number**, representing how many **tokens** define the command.
2. **Flags NEVER count as tokens**. Only subcommands count.
@@ -22,142 +21,143 @@ This dictionary is used to identify the "human-understandable command" from an i
* `npm run dev` → `npm run dev` (because `npm run` has arity 3)
* `python script.py` → `python script.py` (default: whole input, not in dictionary)### **Now generate the dictionary.**
*/
const ARITY: Record<string, number> = {
cat: 1, // cat file.txt
cd: 1, // cd /path/to/dir
chmod: 1, // chmod 755 script.sh
chown: 1, // chown user:group file.txt
cp: 1, // cp source.txt dest.txt
echo: 1, // echo "hello world"
env: 1, // env
export: 1, // export PATH=/usr/bin
grep: 1, // grep pattern file.txt
kill: 1, // kill 1234
killall: 1, // killall process
ln: 1, // ln -s source target
ls: 1, // ls -la
mkdir: 1, // mkdir new-dir
mv: 1, // mv old.txt new.txt
ps: 1, // ps aux
pwd: 1, // pwd
rm: 1, // rm file.txt
rmdir: 1, // rmdir empty-dir
sleep: 1, // sleep 5
source: 1, // source ~/.bashrc
tail: 1, // tail -f log.txt
touch: 1, // touch file.txt
unset: 1, // unset VAR
which: 1, // which node
aws: 3, // aws s3 ls
az: 3, // az storage blob list
bazel: 2, // bazel build
brew: 2, // brew install node
bun: 2, // bun install
"bun run": 3, // bun run dev
"bun x": 3, // bun x vite
cargo: 2, // cargo build
"cargo add": 3, // cargo add tokio
"cargo run": 3, // cargo run main
cdk: 2, // cdk deploy
cf: 2, // cf push app
cmake: 2, // cmake build
composer: 2, // composer require laravel
consul: 2, // consul members
"consul kv": 3, // consul kv get config/app
crictl: 2, // crictl ps
deno: 2, // deno run server.ts
"deno task": 3, // deno task dev
doctl: 3, // doctl kubernetes cluster list
docker: 2, // docker run nginx
"docker builder": 3, // docker builder prune
"docker compose": 3, // docker compose up
"docker container": 3, // docker container ls
"docker image": 3, // docker image prune
"docker network": 3, // docker network inspect
"docker volume": 3, // docker volume ls
eksctl: 2, // eksctl get clusters
"eksctl create": 3, // eksctl create cluster
firebase: 2, // firebase deploy
flyctl: 2, // flyctl deploy
gcloud: 3, // gcloud compute instances list
gh: 3, // gh pr list
git: 2, // git checkout main
"git config": 3, // git config user.name
"git remote": 3, // git remote add origin
"git stash": 3, // git stash pop
go: 2, // go build
gradle: 2, // gradle build
helm: 2, // helm install mychart
heroku: 2, // heroku logs
hugo: 2, // hugo new site blog
ip: 2, // ip link show
"ip addr": 3, // ip addr show
"ip link": 3, // ip link set eth0 up
"ip netns": 3, // ip netns exec foo bash
"ip route": 3, // ip route add default via 1.1.1.1
kind: 2, // kind delete cluster
"kind create": 3, // kind create cluster
kubectl: 2, // kubectl get pods
"kubectl kustomize": 3, // kubectl kustomize overlays/dev
"kubectl rollout": 3, // kubectl rollout restart deploy/api
kustomize: 2, // kustomize build .
make: 2, // make build
mc: 2, // mc ls myminio
"mc admin": 3, // mc admin info myminio
minikube: 2, // minikube start
mongosh: 2, // mongosh test
mysql: 2, // mysql -u root
mvn: 2, // mvn compile
ng: 2, // ng generate component home
npm: 2, // npm install
"npm exec": 3, // npm exec vite
"npm init": 3, // npm init vue
"npm run": 3, // npm run dev
"npm view": 3, // npm view react version
nvm: 2, // nvm use 18
nx: 2, // nx build
openssl: 2, // openssl genrsa 2048
"openssl req": 3, // openssl req -new -key key.pem
"openssl x509": 3, // openssl x509 -in cert.pem
pip: 2, // pip install numpy
pipenv: 2, // pipenv install flask
pnpm: 2, // pnpm install
"pnpm dlx": 3, // pnpm dlx create-next-app
"pnpm exec": 3, // pnpm exec vite
"pnpm run": 3, // pnpm run dev
poetry: 2, // poetry add requests
podman: 2, // podman run alpine
"podman container": 3, // podman container ls
"podman image": 3, // podman image prune
psql: 2, // psql -d mydb
pulumi: 2, // pulumi up
"pulumi stack": 3, // pulumi stack output
pyenv: 2, // pyenv install 3.11
python: 2, // python -m venv env
rake: 2, // rake db:migrate
rbenv: 2, // rbenv install 3.2.0
"redis-cli": 2, // redis-cli ping
rustup: 2, // rustup update
serverless: 2, // serverless invoke
sfdx: 3, // sfdx force:org:list
skaffold: 2, // skaffold dev
sls: 2, // sls deploy
sst: 2, // sst deploy
swift: 2, // swift build
systemctl: 2, // systemctl restart nginx
terraform: 2, // terraform apply
"terraform workspace": 3, // terraform workspace select prod
tmux: 2, // tmux new -s dev
turbo: 2, // turbo run build
ufw: 2, // ufw allow 22
vault: 2, // vault login
"vault auth": 3, // vault auth list
"vault kv": 3, // vault kv get secret/api
vercel: 2, // vercel deploy
volta: 2, // volta install node
wp: 2, // wp plugin install
yarn: 2, // yarn add react
"yarn dlx": 3, // yarn dlx create-react-app
"yarn run": 3, // yarn run dev
}
const ARITY: Record<string, number> = {
cat: 1, // cat file.txt
cd: 1, // cd /path/to/dir
chmod: 1, // chmod 755 script.sh
chown: 1, // chown user:group file.txt
cp: 1, // cp source.txt dest.txt
echo: 1, // echo "hello world"
env: 1, // env
export: 1, // export PATH=/usr/bin
grep: 1, // grep pattern file.txt
kill: 1, // kill 1234
killall: 1, // killall process
ln: 1, // ln -s source target
ls: 1, // ls -la
mkdir: 1, // mkdir new-dir
mv: 1, // mv old.txt new.txt
ps: 1, // ps aux
pwd: 1, // pwd
rm: 1, // rm file.txt
rmdir: 1, // rmdir empty-dir
sleep: 1, // sleep 5
source: 1, // source ~/.bashrc
tail: 1, // tail -f log.txt
touch: 1, // touch file.txt
unset: 1, // unset VAR
which: 1, // which node
aws: 3, // aws s3 ls
az: 3, // az storage blob list
bazel: 2, // bazel build
brew: 2, // brew install node
bun: 2, // bun install
"bun run": 3, // bun run dev
"bun x": 3, // bun x vite
cargo: 2, // cargo build
"cargo add": 3, // cargo add tokio
"cargo run": 3, // cargo run main
cdk: 2, // cdk deploy
cf: 2, // cf push app
cmake: 2, // cmake build
composer: 2, // composer require laravel
consul: 2, // consul members
"consul kv": 3, // consul kv get config/app
crictl: 2, // crictl ps
deno: 2, // deno run server.ts
"deno task": 3, // deno task dev
doctl: 3, // doctl kubernetes cluster list
docker: 2, // docker run nginx
"docker builder": 3, // docker builder prune
"docker compose": 3, // docker compose up
"docker container": 3, // docker container ls
"docker image": 3, // docker image prune
"docker network": 3, // docker network inspect
"docker volume": 3, // docker volume ls
eksctl: 2, // eksctl get clusters
"eksctl create": 3, // eksctl create cluster
firebase: 2, // firebase deploy
flyctl: 2, // flyctl deploy
gcloud: 3, // gcloud compute instances list
gh: 3, // gh pr list
git: 2, // git checkout main
"git config": 3, // git config user.name
"git remote": 3, // git remote add origin
"git stash": 3, // git stash pop
go: 2, // go build
gradle: 2, // gradle build
helm: 2, // helm install mychart
heroku: 2, // heroku logs
hugo: 2, // hugo new site blog
ip: 2, // ip link show
"ip addr": 3, // ip addr show
"ip link": 3, // ip link set eth0 up
"ip netns": 3, // ip netns exec foo bash
"ip route": 3, // ip route add default via 1.1.1.1
kind: 2, // kind delete cluster
"kind create": 3, // kind create cluster
kubectl: 2, // kubectl get pods
"kubectl kustomize": 3, // kubectl kustomize overlays/dev
"kubectl rollout": 3, // kubectl rollout restart deploy/api
kustomize: 2, // kustomize build .
make: 2, // make build
mc: 2, // mc ls myminio
"mc admin": 3, // mc admin info myminio
minikube: 2, // minikube start
mongosh: 2, // mongosh test
mysql: 2, // mysql -u root
mvn: 2, // mvn compile
ng: 2, // ng generate component home
npm: 2, // npm install
"npm exec": 3, // npm exec vite
"npm init": 3, // npm init vue
"npm run": 3, // npm run dev
"npm view": 3, // npm view react version
nvm: 2, // nvm use 18
nx: 2, // nx build
openssl: 2, // openssl genrsa 2048
"openssl req": 3, // openssl req -new -key key.pem
"openssl x509": 3, // openssl x509 -in cert.pem
pip: 2, // pip install numpy
pipenv: 2, // pipenv install flask
pnpm: 2, // pnpm install
"pnpm dlx": 3, // pnpm dlx create-next-app
"pnpm exec": 3, // pnpm exec vite
"pnpm run": 3, // pnpm run dev
poetry: 2, // poetry add requests
podman: 2, // podman run alpine
"podman container": 3, // podman container ls
"podman image": 3, // podman image prune
psql: 2, // psql -d mydb
pulumi: 2, // pulumi up
"pulumi stack": 3, // pulumi stack output
pyenv: 2, // pyenv install 3.11
python: 2, // python -m venv env
rake: 2, // rake db:migrate
rbenv: 2, // rbenv install 3.2.0
"redis-cli": 2, // redis-cli ping
rustup: 2, // rustup update
serverless: 2, // serverless invoke
sfdx: 3, // sfdx force:org:list
skaffold: 2, // skaffold dev
sls: 2, // sls deploy
sst: 2, // sst deploy
swift: 2, // swift build
systemctl: 2, // systemctl restart nginx
terraform: 2, // terraform apply
"terraform workspace": 3, // terraform workspace select prod
tmux: 2, // tmux new -s dev
turbo: 2, // turbo run build
ufw: 2, // ufw allow 22
vault: 2, // vault login
"vault auth": 3, // vault auth list
"vault kv": 3, // vault kv get secret/api
vercel: 2, // vercel deploy
volta: 2, // volta install node
wp: 2, // wp plugin install
yarn: 2, // yarn add react
"yarn dlx": 3, // yarn dlx create-react-app
"yarn run": 3, // yarn run dev
}
export * as BashArity from "./arity"