# Projects and workspaces



A project is **not** the consuming app — it's the *generator
configuration* the consuming app pulls code from. One repository can
contain multiple SKMTC projects, each generating into a different
part of the consuming app (or into different apps).

## The one-paragraph essence [#the-one-paragraph-essence]

A SKMTC root is a directory containing a `.skmtc/` folder. Inside
`.skmtc/` is one directory per project. Each project directory holds
the project's `deno.json` (which generators are installed), its
`.settings/client.json` (which schema, which output path, which
enrichments), its `worker.ts` and `bundle.js` (derived from
`deno.json`, the things the Worker actually loads), and optionally
local generator source directories for any cloned generators.
Generated output lands wherever `client.json#settings.basePath`
points.

## The SKMTC root [#the-skmtc-root]

The SKMTC root is the nearest ancestor directory containing a
`.skmtc/` folder. The CLI discovers it by walking up from the current
working directory.

```
my-app/                          ← SKMTC root (nearest .skmtc/ ancestor)
├── .skmtc/                      ← root marker
│   ├── my-api/                  ← project "my-api"
│   ├── my-graphql/              ← project "my-graphql"
│   └── ...
├── src/                         ← consuming app source
└── package.json                 ← consuming app
```

Created by `skmtc init`. After init, all `skmtc` commands run from
anywhere inside the root will find it.

## Project directory layout [#project-directory-layout]

```
.skmtc/<project>/
├── deno.json                    ← user-edited (or via CLI)
├── deno.lock                    ← auto-managed by Deno
├── worker.ts                    ← derived from deno.json
├── bundle.js                    ← derived from worker.ts (present for every project)
├── .settings/
│   ├── client.json              ← user-edited (schema, basePath, enrichments)
│   ├── manifest.json            ← derived; overwritten every generate run
│   ├── logs.txt                 ← derived; bundle/generate stdout
│   └── error-logs.txt           ← derived; bundle/generate stderr
└── <gen-name>/                  ← only if cloned generators exist
    ├── deno.json
    ├── mod.ts
    └── src/...
```

Each file's role:

### `deno.json` [#denojson]

The project's dependency manifest. JSR imports of installed
generators plus core peer dependencies:

```json
{
  "imports": {
    "@skmtc/core": "jsr:@skmtc/core@^0.3.7",
    "@skmtc/worker": "jsr:@skmtc/worker@^0.2.0",
    "@skmtc/gen-zod": "jsr:@skmtc/gen-zod@^0.0.55",
    "@skmtc/gen-typescript": "jsr:@skmtc/gen-typescript@^0.0.55"
  }
}
```

User-edited via `skmtc install`, `skmtc clone`, or `skmtc remove`.
Hand-editing is supported but the CLI commands also do validation
(peer-pin checks) that hand-editing skips.

### `deno.lock` [#denolock]

Auto-managed by Deno. Pins exact versions and content hashes. Ensures
reproducible installs across machines.

### `worker.ts` [#workerts]

A **derived file** — the CLI templates it from `deno.json#imports`:

```ts
import toWorker from '@skmtc/worker'
import gen1 from '@skmtc/gen-zod'
import gen2 from '@skmtc/gen-typescript'
// ... one import per generator

export default toWorker(() =>
  Object.fromEntries([gen1, gen2].map(g => [g.id, g]))
)
```

Regenerated by `skmtc bundle` and `skmtc dev`. &#x2A;*Never hand-edited.**
Hand-edits are lost on next bundle.

### `bundle.js` [#bundlejs]

The compiled output of `deno bundle worker.ts -o bundle.js`. This is
what the SKMTC Worker actually loads at generate time.

**Present for every project**, including remote-only ones (all
generators installed from JSR). `skmtc generate` loads the
project-local `bundle.js` regardless of where the generator source
comes from, so a matching `worker.ts`/`bundle.js` must exist.

Bundle freshness — i.e., whether `bundle.js` matches the current
`deno.json` — is checked by `skmtc doctor`. The `project-bundle/`
check surfaces stale bundles.

### `.settings/client.json` [#settingsclientjson]

The user-facing configuration. Schema source, output path, generator
filters, enrichments:

```json
{
  "source": "./openapi.json",
  "settings": {
    "basePath": "src/generated",
    "skip": [],
    "include": [],
    "enrichments": { /* per-generator */ }
  }
}
```

User-edited. The `source`, `basePath`, and `enrichments` are the
three knobs most users adjust.

### `.settings/manifest.json` [#settingsmanifestjson]

A **derived file** — overwritten every `skmtc generate` run. Contains
the canonical record of the last run: parse issues, per-(generator,
operation) results, file metadata, timing.

See [error handling philosophy](/docs/concepts/error-handling-philosophy#the-manifest-as-canonical-run-record)
for the structure and `manifest-format.md` reference for the full
schema.

### `.settings/logs.txt` and `.settings/error-logs.txt` [#settingslogstxt-and-settingserror-logstxt]

stdout and stderr captures from `skmtc bundle` runs. Useful for
debugging bundle failures.

### `<gen-name>/` [#gen-name]

Present only for cloned generators. Contains the generator's source
code — `deno.json`, `mod.ts`, `src/`. The directory is what
`skmtc clone` writes to.

After cloning, the `deno.json#imports` entry points to this local
directory (e.g., `"@skmtc/gen-zod": "./gen-zod/mod.ts"`) instead of a
JSR specifier.

## basePath alignment with the consuming app [#basepath-alignment-with-the-consuming-app]

The `basePath` field in `client.json` is **load-bearing**: it
controls both where on disk generated files land AND the `@` alias
in the consuming app's bundler resolver.

Example: a Vite app with `vite.config.ts` setting `@` → `./src` would
configure `basePath: "src/generated"` (relative to the SKMTC root).
Generators produce paths like `@/forms/CreateUserForm.generated.tsx`,
which:

* On disk lands at `<root>/src/generated/forms/CreateUserForm.generated.tsx`
* In the consuming app's TS imports resolves to `<root>/src/forms/...` via the bundler

These two paths must match. If they diverge, generated imports break
at runtime.

For Vite/Next/Webpack/Rollup, the `@` alias is typically configured
in their respective config files. SKMTC doesn't manage the bundler
config — you align them manually.

## Global state at \~/.skmtc/ [#global-state-at-skmtc]

Not all project state is per-project. SKMTC also maintains a global
state directory:

```
~/.skmtc/
├── auth-sb-api-auth-token.txt   ← Supabase auth for the sandbox API
├── <project-name>/              ← shadow project state (per-project)
│   ├── schema.json              ← cached schema
│   └── manifest.json            ← shadow manifest
└── ...
```

The global state is mostly used by the hosted sandbox API path. For
local-only setups, you can mostly ignore it. When debugging "why
isn't this working," **check both** `<root>/.skmtc/<project>/` and
`~/.skmtc/<project>/`.

## Multiple projects in one repo [#multiple-projects-in-one-repo]

A repo can have many projects:

```
my-app/
├── .skmtc/
│   ├── api-v1/         ← old API generator config
│   ├── api-v2/         ← new API generator config
│   ├── graphql/        ← GraphQL operations
│   └── mocks/          ← MSW handlers
├── src/
│   ├── generated/      ← api-v2 output
│   ├── generated-mocks/← mocks output (different basePath)
│   └── ...
```

Each project has its own `deno.json`, `client.json`, generators, and
output location. The CLI subcommands take the project name as an
argument:

```bash
skmtc generate api-v2
skmtc generate mocks
```

This is the right pattern when you want different generator sets,
different schemas, or different output locations. There's no
performance benefit to combining multiple "logical" projects into
one — generators run independently per project anyway.

## Initialization [#initialization]

`skmtc init <projectName> <basePath>`:

1. Creates `.skmtc/<projectName>/`
2. Writes `deno.json` with the core peer-dep pin and an empty imports list
3. Writes `.settings/client.json` with `basePath` and no source
4. Returns `{ type: 'created', projectName, basePath }`

If the project already exists, `init` returns `{ type: 'existed' }`
and exits cleanly — it's idempotent.

The `basePath` argument:

* **Must be relative** to the SKMTC root. Absolute paths are rejected
  with exit code 2 and a recipe error.
* **Must match the consumer app's bundler alias**. The CLI doesn't
  verify this — the user does — but `skmtc doctor`'s
  `project-base-path/<project>` check surfaces obvious mistakes
  (missing, absolute, etc.).

## Common questions [#common-questions]

### Can multiple projects share generators? [#can-multiple-projects-share-generators]

The JSR imports in each project's `deno.json` are independent. If
both projects install `@skmtc/gen-zod`, each pins its own version.
Deno's content-addressed cache means the package only downloads once
per version, so the disk cost is low even with duplication.

If you clone a generator: the clone is project-local. It exists in
one project's directory only. To share a clone, you'd need to
publish your customized version to JSR and `install` it in both
projects.

### How do I add or remove generators? [#how-do-i-add-or-remove-generators]

Use `skmtc install <generator-id> <project>` and
`skmtc remove <project> <generator-id>`. Hand-editing `deno.json`
works but skips the CLI's pre-flight checks (peer-pin compatibility,
auto-rebundle). Recommended: use the CLI for state changes; only
hand-edit for advanced pinning needs.

### What happens to a project's `bundle.js` when I `install` a new generator? [#what-happens-to-a-projects-bundlejs-when-i-install-a-new-generator]

`skmtc install` triggers a post-install rebundle unconditionally —
regardless of whether the project has any clones. The newly installed
generator is bundled into `bundle.js` so `generate` picks it up. (The
same holds for a project with only installed generators: it still has
a local `bundle.js`, and install refreshes it.)

### Where do tests for cloned generators live? [#where-do-tests-for-cloned-generators-live]

There's no convention. Some teams put them adjacent to the cloned
source (`.skmtc/<project>/<gen-name>/src/*.test.ts`); some put them
in the consuming app's test directory; some skip them. Cloned
generators are project source, so the test location is the user's
call.

### Can I version-control `.skmtc/<project>/`? [#can-i-version-control-skmtcproject]

Yes — and you should. The directory contains:

* User-edited config (`deno.json`, `client.json`) — definitely commit
* Derived files (`worker.ts`, `bundle.js`, `manifest.json`,
  `logs.txt`) — gitignore is reasonable
* Cloned generator source (`<gen-name>/`) — definitely commit (it's
  your code now)

A reasonable `.gitignore`:

```
.skmtc/**/bundle.js
.skmtc/**/worker.ts
.skmtc/**/.settings/manifest.json
.skmtc/**/.settings/*.txt
```

### Can SKMTC live outside a Node/pnpm project? [#can-skmtc-live-outside-a-nodepnpm-project]

Yes. SKMTC requires Deno; the consuming app can be anything (Vite,
Next, Bun, even a non-JS project that doesn't import the generated
code). The CLI doesn't read `package.json`.

### What if I have multiple SKMTC roots in nested directories? [#what-if-i-have-multiple-skmtc-roots-in-nested-directories]

The CLI discovers the *nearest* ancestor `.skmtc/`. If you have
nested roots, only the inner one is found. To work with the outer
root, run the CLI from a directory outside the inner root.

In practice, one SKMTC root per repository is the convention.

## Further reading [#further-reading]

* [Clone vs install](/docs/concepts/clone-vs-install) — what changes in the project directory when cloning
* [Enrichments](/docs/concepts/enrichments) — the `client.json#settings.enrichments` structure
* [The Worker runtime](/docs/concepts/the-worker-runtime) — how `worker.ts` and `bundle.js` get used
* [Settings reference: client.json schema](/docs/reference/settings/client-json-schema)
* [`skmtc init` reference](/docs/reference/cli/init)
* [`skmtc doctor` reference](/docs/reference/cli/doctor)
* `skmtc-cli` skill
