@tapestry-mud/cli 0.9.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +42 -0
- package/bin/tapestry.js +18 -5
- package/package.json +6 -3
- package/specs/README.md +59 -0
- package/specs/changes/2026-06-20-pack-esm-build.md +28 -0
- package/specs/engine-management.md +140 -0
- package/specs/harvest.md +169 -0
- package/specs/lint.config.json +1 -0
- package/specs/pack-lifecycle.md +206 -0
- package/specs/registry-auth.md +127 -0
- package/specs/validate.md +70 -0
- package/src/commands/harvest.js +12 -2
- package/src/commands/pack.js +3 -0
- package/src/commands/publish.js +2 -0
- package/src/commands/types.js +15 -0
- package/src/lib/pack-manifest.js +4 -0
- package/src/lib/registry-sink.js +117 -0
- package/src/lib/render-core.js +67 -0
- package/src/lib/ts-build.js +22 -0
- package/types/tapestry-engine.d.ts +630 -0
- package/validation-ledger.md +29 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`@tapestry-mud/cli` is a Node.js CLI that manages Tapestry MUD game projects: scaffolding, pack management, registry operations, and engine lifecycle. Entry point: `bin/tapestry.js`. Commands are wired via `commander`.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm ci # install deps
|
|
13
|
+
npm test # run all tests (jest)
|
|
14
|
+
npx jest test/path/to/file.test.js # run a single test file
|
|
15
|
+
npx jest --testNamePattern "name" # run tests matching a name
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
No build step. No lint config in this repo.
|
|
19
|
+
|
|
20
|
+
## Running the CLI locally
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
node bin/tapestry.js <command>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Layout
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
bin/ entry point (tapestry.js)
|
|
30
|
+
src/
|
|
31
|
+
commands/ one file per CLI command (~30 commands)
|
|
32
|
+
lib/ shared logic (auth, registry client, pack ops, etc.)
|
|
33
|
+
schema/ zod schemas
|
|
34
|
+
scaffold/ template files for `tapestry init`
|
|
35
|
+
util/ small helpers
|
|
36
|
+
specs/ capability specs -- source of truth for system behavior
|
|
37
|
+
test/ jest tests, mirrors src/ structure
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## System behavior
|
|
41
|
+
|
|
42
|
+
All behavior is documented in `specs/`. Read the relevant spec before changing any command logic. `specs/README.md` lists the index and explains the format contract (anchored claims, change records, lint rules).
|
package/bin/tapestry.js
CHANGED
|
@@ -33,6 +33,7 @@ const { trustAdd, trustList, trustRm } = require('../src/commands/trust');
|
|
|
33
33
|
const { syncArea } = require('../src/commands/sync-area');
|
|
34
34
|
const { harvest } = require('../src/commands/harvest');
|
|
35
35
|
const { status } = require('../src/commands/status');
|
|
36
|
+
const { types } = require('../src/commands/types');
|
|
36
37
|
|
|
37
38
|
const program = new Command();
|
|
38
39
|
|
|
@@ -62,7 +63,7 @@ program.configureHelp({
|
|
|
62
63
|
},
|
|
63
64
|
{
|
|
64
65
|
title: 'Pack Authoring',
|
|
65
|
-
commands: ['create', 'validate', 'pack', 'publish', 'unpublish', 'harvest', 'status'],
|
|
66
|
+
commands: ['create', 'validate', 'pack', 'publish', 'unpublish', 'harvest', 'status', 'types'],
|
|
66
67
|
},
|
|
67
68
|
{
|
|
68
69
|
title: 'Trusted Publishing',
|
|
@@ -376,6 +377,18 @@ program
|
|
|
376
377
|
}
|
|
377
378
|
});
|
|
378
379
|
|
|
380
|
+
program
|
|
381
|
+
.command('types')
|
|
382
|
+
.description('Write the @tapestry/engine type declarations into types/ for ESM pack authoring')
|
|
383
|
+
.action(() => {
|
|
384
|
+
try {
|
|
385
|
+
types({});
|
|
386
|
+
} catch (e) {
|
|
387
|
+
console.error(`error: ${e.message}`);
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
379
392
|
async function runHarvest(areaRef, opts) {
|
|
380
393
|
try {
|
|
381
394
|
await harvest(areaRef, {
|
|
@@ -398,15 +411,15 @@ async function runHarvest(areaRef, opts) {
|
|
|
398
411
|
program
|
|
399
412
|
.command('harvest <areaRef>')
|
|
400
413
|
.description('Harvest an authored area into a portable pack (areaRef = namespace:area-id)')
|
|
401
|
-
.option('--sink <sink>', 'Output sink: file | git (auto-detected by default)')
|
|
414
|
+
.option('--sink <sink>', 'Output sink: file | git | registry (auto-detected by default)')
|
|
402
415
|
.option('--out <path>', '(file sink) where the .tgz lands')
|
|
403
|
-
.option('--name <name>', '(file sink) override the synthesized pack name (@scope/pack)')
|
|
416
|
+
.option('--name <name>', '(file/registry sink) override the synthesized pack name (@scope/pack)')
|
|
404
417
|
.option('--pack <dir>', 'Target pack directory (auto-detected from linked packs by default)')
|
|
405
418
|
.option('--game-root <path>', 'Game root containing data/ (default: current dir)')
|
|
406
419
|
.option('--keep-sidecars', 'Copy instead of move (leave the game-root side-cars in place)')
|
|
407
420
|
.option('--force', 'Overwrite pack files that diverge from the side-car')
|
|
408
|
-
.option('--minor', 'Bump the pack minor version (git sink only; default: patch)')
|
|
409
|
-
.option('--major', 'Bump the pack major version (git sink only; default: patch)')
|
|
421
|
+
.option('--minor', 'Bump the pack minor version (git/registry sink, owned pack only; default: patch)')
|
|
422
|
+
.option('--major', 'Bump the pack major version (git/registry sink, owned pack only; default: patch)')
|
|
410
423
|
.action(runHarvest);
|
|
411
424
|
|
|
412
425
|
// Deprecated: sync-area is now harvest --sink git.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tapestry-mud/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "CLI for the Tapestry MUD engine",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,22 +10,25 @@
|
|
|
10
10
|
"tapestry": "./bin/tapestry.js"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "jest"
|
|
13
|
+
"test": "jest",
|
|
14
|
+
"spec-lint": "spec-lint specs/"
|
|
14
15
|
},
|
|
15
16
|
"publishConfig": {
|
|
16
17
|
"access": "public"
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|
|
19
20
|
"commander": "11.1.0",
|
|
20
|
-
"form-data": "4.0.
|
|
21
|
+
"form-data": "4.0.6",
|
|
21
22
|
"inquirer": "8.2.7",
|
|
22
23
|
"js-yaml": "4.1.1",
|
|
23
24
|
"node-fetch": "2.7.0",
|
|
24
25
|
"semver": "7.6.2",
|
|
25
26
|
"tar": "7.5.15",
|
|
27
|
+
"typescript": "5.7.3",
|
|
26
28
|
"zod": "3.22.4"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
31
|
+
"@tapestry-mud/spec-lint": "0.2.1",
|
|
29
32
|
"jest": "29.7.0"
|
|
30
33
|
}
|
|
31
34
|
}
|
package/specs/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Tapestry CLI -- specs
|
|
2
|
+
|
|
3
|
+
Capability specs for the Tapestry CLI. Each file describes one system's current behavior,
|
|
4
|
+
known constraints, and change history. This directory is the canonical, public source of
|
|
5
|
+
truth for how each system behaves now -- a fresh agent or contributor answers "how does X
|
|
6
|
+
behave?" from the relevant file alone.
|
|
7
|
+
|
|
8
|
+
## Index
|
|
9
|
+
|
|
10
|
+
| Capability | File | Last Updated |
|
|
11
|
+
|------------|------|--------------|
|
|
12
|
+
| pack-lifecycle | [pack-lifecycle.md](pack-lifecycle.md) | 2026-06-20 |
|
|
13
|
+
| validate | [validate.md](validate.md) | 2026-06-13 |
|
|
14
|
+
| harvest | [harvest.md](harvest.md) | 2026-06-13 |
|
|
15
|
+
| registry-auth | [registry-auth.md](registry-auth.md) | 2026-06-13 |
|
|
16
|
+
| engine-management | [engine-management.md](engine-management.md) | 2026-06-13 |
|
|
17
|
+
|
|
18
|
+
## Contract summary
|
|
19
|
+
|
|
20
|
+
Each capability spec has four required sections: Overview, Behavior, Rejected and Reverted,
|
|
21
|
+
Change Log. Change records live in `specs/changes/` and use the frontmatter fields `release:`
|
|
22
|
+
(the version that shipped it) and `specs:` (capability files touched).
|
|
23
|
+
|
|
24
|
+
Hotfixes, regressions, and dependency bumps owe no change record. Tombstones on any reversal
|
|
25
|
+
of shipped behavior are mandatory.
|
|
26
|
+
|
|
27
|
+
A capability spec is current if its Change Log references the latest shipped change record
|
|
28
|
+
that names it in `specs:`.
|
|
29
|
+
|
|
30
|
+
## Format rules (mechanically linted)
|
|
31
|
+
|
|
32
|
+
- Behavior claims carry inline anchors in exactly one form: `(repo-relative/path/File.ext:123)`,
|
|
33
|
+
where the line part may be a single line `:123` or a range `:123-145`, and may be omitted only
|
|
34
|
+
for whole-file claims. Several anchors may share one set of parentheses, joined by `; `. A test
|
|
35
|
+
name in the same parentheses also counts. Lint pattern (the gate IS this regex, keep them in
|
|
36
|
+
sync): `\([@\w./\\-]+\.(js|mjs|cjs)(:\d+(-\d+)?)?[^)]*\)`. A file with no matches in its Behavior
|
|
37
|
+
section fails validation outright.
|
|
38
|
+
- An empty Rejected and Reverted section contains the single line `- None on record.` under the
|
|
39
|
+
heading (the heading itself is always present).
|
|
40
|
+
- Change Log is a one-line-per-record list, newest first: `- YYYY-MM-DD [slug](changes/...)`.
|
|
41
|
+
Not a table.
|
|
42
|
+
|
|
43
|
+
<!-- spec-lint:start -->
|
|
44
|
+
Mode: strict
|
|
45
|
+
|
|
46
|
+
Required sections: Overview, Behavior, Rejected and Reverted, Change Log
|
|
47
|
+
|
|
48
|
+
Anchor regex (Behavior): \([@\w./\\-]+\.(cs|js|ts|json|ya?ml|md)(:\d+(-\d+)?)?[^)]*\)
|
|
49
|
+
|
|
50
|
+
Empty-reversal sentinel: - None on record.
|
|
51
|
+
|
|
52
|
+
Change Log: list, newest-first by date, not a table. Empty is valid for unmodified capabilities.
|
|
53
|
+
|
|
54
|
+
Index sync: every capability .md on disk appears in README index; every indexed file exists on disk; index date matches file last-updated.
|
|
55
|
+
|
|
56
|
+
Currency: for each change record naming a capability, the top Change Log entry references that record and last-updated >= record date. A capability named by zero records may have an empty Change Log.
|
|
57
|
+
|
|
58
|
+
Tombstone: a change record with status:reverted requires a tombstone entry in the capability Rejected and Reverted (not the empty sentinel).
|
|
59
|
+
<!-- spec-lint:end -->
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
release: 0.10.0
|
|
3
|
+
specs: [pack-lifecycle.md]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Pack ESM Build
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
Packs are authored in TypeScript and ship native ES modules, but the CLI had no
|
|
11
|
+
build step: `pack` and `publish` archived whatever was on disk. An ESM pack
|
|
12
|
+
declares `content.scripts_format: esm` and points `content.scripts` at compiled
|
|
13
|
+
output under `dist/scripts/`, which is gitignored - so without a compile the
|
|
14
|
+
tarball shipped no runnable scripts. Pack authors also had no typed surface for
|
|
15
|
+
the engine API they call, so editors could not check `tapestry.*` usage.
|
|
16
|
+
|
|
17
|
+
## What
|
|
18
|
+
|
|
19
|
+
- `pack` and `publish` compile an ESM pack's `scripts/**/*.ts` to
|
|
20
|
+
`dist/scripts/**/*.js` with the bundled `tsc` and the pack's `tsconfig.json`
|
|
21
|
+
before building the tarball. The compile runs before any network or auth work
|
|
22
|
+
in `publish`. Legacy packs (no `scripts_format: esm`) are a no-op, so the step
|
|
23
|
+
is backward compatible.
|
|
24
|
+
- New `tapestry types` command vendors the `@tapestry/engine` type definitions
|
|
25
|
+
into a project's `types/` so `tsc` and editors can type-check pack scripts
|
|
26
|
+
against the engine API surface the pack calls.
|
|
27
|
+
- `typescript` is pinned exactly as a CLI dependency (no caret) so the compile is
|
|
28
|
+
reproducible.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
capability: engine-management
|
|
3
|
+
last-updated: 2026-06-13
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# engine-management
|
|
7
|
+
|
|
8
|
+
Capability spec for acquiring, configuring, running, and stopping the Tapestry engine.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
engine-management covers the lifecycle of the Tapestry engine process from the CLI's
|
|
13
|
+
perspective: reading the engine configuration from `tapestry.yaml`, fetching or building the
|
|
14
|
+
engine artifact, starting it, and stopping it. Three launch modes are supported: `docker`
|
|
15
|
+
(recommended; pulls and runs a container image), `binary` (downloads a pre-built platform
|
|
16
|
+
binary from GitHub Releases), and `source` (git-clones the engine and runs it via
|
|
17
|
+
`dotnet run`). Named channels (`stable`, `nightly`) resolve to concrete versions via the
|
|
18
|
+
registry for the docker mode.
|
|
19
|
+
|
|
20
|
+
Commands: `engine install`, `engine update`, `engine info`, `engine versions`, `start`, `stop`.
|
|
21
|
+
|
|
22
|
+
## Behavior
|
|
23
|
+
|
|
24
|
+
### Configuration
|
|
25
|
+
|
|
26
|
+
Engine configuration is read from the `engine` key in `tapestry.yaml`. The key must be an
|
|
27
|
+
object (not a plain string, which is the pack manifest format). Required fields are
|
|
28
|
+
`engine.version` and `engine.mode`; `mode` must be one of `docker`, `binary`, or `source`.
|
|
29
|
+
(src/lib/engine-manager.js:249-278)
|
|
30
|
+
|
|
31
|
+
Derived values from `readEngineConfig`: (src/lib/engine-manager.js:248-278)
|
|
32
|
+
|
|
33
|
+
- `image` defaults to `ghcr.io/tapestry-mud/tapestry` if not specified.
|
|
34
|
+
- `network` is null if not specified.
|
|
35
|
+
- `envFile` is null if not specified (`engine.env_file`).
|
|
36
|
+
- `installDir` is always `<cwd>/.tapestry-engine`.
|
|
37
|
+
- `projectName` is the manifest `name` lowercased and with non-alphanumeric/hyphen characters
|
|
38
|
+
replaced by `-`.
|
|
39
|
+
|
|
40
|
+
### Channel resolution (docker mode only)
|
|
41
|
+
|
|
42
|
+
Channel names `stable` and `nightly` are the two recognized named channels.
|
|
43
|
+
(src/lib/engine-manager.js:6)
|
|
44
|
+
|
|
45
|
+
When the configured version is a named channel, `resolveDockerTag` fetches
|
|
46
|
+
`<registry>/v1/engine-channels/<channel>` and returns the `docker_tag` field.
|
|
47
|
+
(src/lib/engine-manager.js:14-35)
|
|
48
|
+
|
|
49
|
+
- If the registry is unreachable, the channel name is used as the tag directly with a warning.
|
|
50
|
+
(src/lib/engine-manager.js:19-22)
|
|
51
|
+
- If the registry returns 404, throws with a hint to run `engine versions`.
|
|
52
|
+
(src/lib/engine-manager.js:23-27)
|
|
53
|
+
- If the version string is not a named channel, it is used as the Docker tag directly.
|
|
54
|
+
(src/lib/engine-manager.js:13-14)
|
|
55
|
+
|
|
56
|
+
### engine install
|
|
57
|
+
|
|
58
|
+
- **docker mode**: Calls `docker pull <image>:<version>`. If the tagged pull fails, falls back
|
|
59
|
+
to `docker pull <image>:latest` and re-tags to the requested version.
|
|
60
|
+
(src/lib/engine-manager.js:44-59)
|
|
61
|
+
- **binary mode**: Determines the platform (`linux`, `osx`, or `windows`), downloads the
|
|
62
|
+
engine tarball from
|
|
63
|
+
`https://github.com/tapestry-mud/tapestry/releases/download/v<version>/tapestry-<platform>.tar.gz`
|
|
64
|
+
using `curl`, extracts it to `.tapestry-engine/binary/<version>/`, then deletes the
|
|
65
|
+
downloaded tarball. (src/lib/engine-manager.js:124-151)
|
|
66
|
+
- **source mode**: `git clone`s `https://github.com/tapestry-mud/tapestry.git` into
|
|
67
|
+
`.tapestry-engine/source/`. Throws if the source directory already exists (use
|
|
68
|
+
`engine update` instead). (src/lib/engine-manager.js:180-194)
|
|
69
|
+
|
|
70
|
+
### engine update
|
|
71
|
+
|
|
72
|
+
- **docker mode**: Same pull logic as install (re-pulls the tag).
|
|
73
|
+
(src/lib/engine-manager.js:294-298)
|
|
74
|
+
- **binary mode**: Re-runs `binaryInstall` for the configured version.
|
|
75
|
+
(src/lib/engine-manager.js:299-300)
|
|
76
|
+
- **source mode**: Runs `git pull` inside `.tapestry-engine/source/`; throws if the source
|
|
77
|
+
directory is absent. (src/lib/engine-manager.js:195-207)
|
|
78
|
+
|
|
79
|
+
### engine info
|
|
80
|
+
|
|
81
|
+
Prints `Mode`, `Version`, and either `Image` (docker) or `Path` and `Status` (binary/source).
|
|
82
|
+
(src/commands/engine.js:13-23)
|
|
83
|
+
|
|
84
|
+
For binary and source modes, `installed` is true if the expected directory exists on disk.
|
|
85
|
+
(src/lib/engine-manager.js:174) (src/lib/engine-manager.js:226)
|
|
86
|
+
|
|
87
|
+
### engine versions
|
|
88
|
+
|
|
89
|
+
Fetches `/v1/engine-channels` from the registry and prints a table of Channel, Version, and
|
|
90
|
+
Updated columns. Prints "No engine channels registered." if the list is empty.
|
|
91
|
+
(src/commands/engine-versions.js:7-31)
|
|
92
|
+
|
|
93
|
+
### start
|
|
94
|
+
|
|
95
|
+
- Requires `packs/` directory and `server.yaml` in the project root; throws otherwise.
|
|
96
|
+
(src/lib/engine-manager.js:321-327)
|
|
97
|
+
- Creates `data/` directory if absent. (src/lib/engine-manager.js:328)
|
|
98
|
+
- **docker mode**: Calls `docker rm -f <containerName>` to clear any stale container, then
|
|
99
|
+
`docker run --detach` with port mappings `-p 4000:4000 -p 4001:4001`, volume mounts for
|
|
100
|
+
`packs/`, `server.yaml`, and `data/`, optional `--network` and `--env-file`, and any link
|
|
101
|
+
mounts from `dockerLinkMounts`. (src/lib/engine-manager.js:69-99)
|
|
102
|
+
- **binary mode**: Materializes links into `packs/`, spawns the engine binary detached, and
|
|
103
|
+
writes the PID to `.tapestry.pid`. (src/lib/engine-manager.js:154-171)
|
|
104
|
+
(src/lib/process-tracker.js:8-10)
|
|
105
|
+
- **source mode**: Materializes links, runs `dotnet run` in `.tapestry-engine/source/` with
|
|
106
|
+
`--packs` and `--config` arguments, detached, and writes the PID to `.tapestry.pid`.
|
|
107
|
+
(src/lib/engine-manager.js:208-222) (src/lib/process-tracker.js:8-10)
|
|
108
|
+
- **docker mode with `engine.env_file`**: Resolves the path relative to the project root and
|
|
109
|
+
throws if the file does not exist before starting. (src/lib/engine-manager.js:330-338)
|
|
110
|
+
- Container name for docker mode is `tapestry-<projectName>`. (src/lib/engine-manager.js:70)
|
|
111
|
+
- Prints "Engine started. Container: <name>" (docker) or "Engine started (PID <pid>)"
|
|
112
|
+
(binary/source) and the telnet/websocket addresses. (src/lib/engine-manager.js:96-99)
|
|
113
|
+
(src/lib/engine-manager.js:168-171)
|
|
114
|
+
|
|
115
|
+
### stop
|
|
116
|
+
|
|
117
|
+
- **docker mode**: Runs `docker stop <containerName>` then `docker rm <containerName>`.
|
|
118
|
+
Throws if either command fails. (src/lib/engine-manager.js:101-112)
|
|
119
|
+
- **binary/source mode**: Reads the PID from `.tapestry.pid`, sends `SIGTERM`, and clears the
|
|
120
|
+
file. If the process is already gone when `SIGTERM` is sent, the error is swallowed and the
|
|
121
|
+
PID file is still cleared. Throws if no PID file is found.
|
|
122
|
+
(src/lib/engine-manager.js:232-243) (src/lib/process-tracker.js:14-20)
|
|
123
|
+
(src/lib/process-tracker.js:22-27)
|
|
124
|
+
|
|
125
|
+
### Process tracker
|
|
126
|
+
|
|
127
|
+
- `writePid` writes the PID as a string to `<cwd>/.tapestry.pid`.
|
|
128
|
+
(src/lib/process-tracker.js:8-10)
|
|
129
|
+
- `readPid` reads and parses the PID; returns null if the file is absent or the value is not a
|
|
130
|
+
positive integer. (src/lib/process-tracker.js:13-20)
|
|
131
|
+
- `clearPid` deletes the file if present; no-op otherwise.
|
|
132
|
+
(src/lib/process-tracker.js:22-27)
|
|
133
|
+
|
|
134
|
+
## Rejected and Reverted
|
|
135
|
+
|
|
136
|
+
- None on record.
|
|
137
|
+
|
|
138
|
+
## Change Log
|
|
139
|
+
|
|
140
|
+
- None on record.
|
package/specs/harvest.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
capability: harvest
|
|
3
|
+
last-updated: 2026-06-13
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# harvest
|
|
7
|
+
|
|
8
|
+
Capability spec for world-state classification and the authoring-to-pack pipeline.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
This capability covers two closely related concerns:
|
|
13
|
+
|
|
14
|
+
1. **World-state** -- classifying each authored area in the game root's `data/areas/` tree
|
|
15
|
+
as Clean, Edited, Fork, or WIP by reading only on-disk side-car facts. The `tapestry status`
|
|
16
|
+
command is the user-facing view of this classification.
|
|
17
|
+
|
|
18
|
+
2. **Harvest** -- promoting authored area content from the game-root side-car tree into a
|
|
19
|
+
pack. Two sinks exist: the `git` sink renders the content into a linked pack repository,
|
|
20
|
+
bumps the pack version, and commits; the `file` sink renders into a temp build directory
|
|
21
|
+
and produces a portable `.tgz` at the current version without bumping.
|
|
22
|
+
|
|
23
|
+
The `sync-area` command is a deprecated alias for `harvest --sink git`. The `export-area`
|
|
24
|
+
command is a hidden deprecated alias for the same.
|
|
25
|
+
|
|
26
|
+
The area reference format is `namespace:area-id` (e.g. `example-pack:village-green`).
|
|
27
|
+
(src/lib/pack-resolve.js:8-13)
|
|
28
|
+
|
|
29
|
+
## Behavior
|
|
30
|
+
|
|
31
|
+
### World-state classification
|
|
32
|
+
|
|
33
|
+
The state vocabulary is defined in `src/lib/world-state.js` and maps to four values:
|
|
34
|
+
(src/lib/world-state.js:11)
|
|
35
|
+
|
|
36
|
+
- **WIP** -- `area.yaml` exists and its `area.flags` array contains the string `wip`.
|
|
37
|
+
Checked before all other states; a WIP area is not visible to players.
|
|
38
|
+
(src/lib/world-state.js:14-22) (src/lib/world-state.js:51)
|
|
39
|
+
- **Clean** -- no room files exist under `data/areas/<area>/rooms/*.yaml`, OR all existing
|
|
40
|
+
room files are byte-for-byte equal (via JSON round-trip of YAML) to their counterparts in
|
|
41
|
+
the linked pack directory. (src/lib/world-state.js:57-59) (src/lib/world-state.js:64-70)
|
|
42
|
+
- **Edited** -- at least one room file under the side-car tree either has no counterpart in
|
|
43
|
+
the pack or differs from it. The pack must be owned (namespace resolves to a linked pack).
|
|
44
|
+
(src/lib/world-state.js:62-70)
|
|
45
|
+
- **Fork** -- the area's namespace does not resolve to any linked pack the user owns (the
|
|
46
|
+
area belongs to an official or foreign pack). (src/lib/world-state.js:63-65)
|
|
47
|
+
|
|
48
|
+
Namespace is inferred from the `id` field of the first room file (`ns:key` format).
|
|
49
|
+
(src/lib/world-state.js:44-49)
|
|
50
|
+
|
|
51
|
+
`computeAreaStates` iterates `data/areas/` directories alphabetically. It never queries the
|
|
52
|
+
registry and never reads the engine's runtime state. (src/lib/world-state.js:26-77)
|
|
53
|
+
|
|
54
|
+
#### status command
|
|
55
|
+
|
|
56
|
+
Prints one line per area: `<State padded to 7> <area-dir> (<namespace>) rooms:<count> [WIP]`.
|
|
57
|
+
(src/commands/status.js:13-15)
|
|
58
|
+
|
|
59
|
+
Prints a legend line: "Clean = shipped; Edited = ready to harvest; Fork = owned by another
|
|
60
|
+
pack; WIP = hidden from players." (src/commands/status.js:18)
|
|
61
|
+
|
|
62
|
+
Prints "Strict boot remains the authority; this is a lighter on-host view."
|
|
63
|
+
(src/commands/status.js:19)
|
|
64
|
+
|
|
65
|
+
With `--game-root <path>`, reads `data/areas/` from that path instead of the current
|
|
66
|
+
directory. (src/commands/status.js:5)
|
|
67
|
+
|
|
68
|
+
### Harvest -- sink selection
|
|
69
|
+
|
|
70
|
+
`harvest` auto-detects the sink when `--sink` is not given: it calls `resolvePackDirOrNull`
|
|
71
|
+
for the area's namespace; if exactly one linked pack matches AND that directory is a git repo,
|
|
72
|
+
the git sink is used; otherwise the file sink is used.
|
|
73
|
+
(src/commands/harvest.js:15-19)
|
|
74
|
+
|
|
75
|
+
With `--sink git` or `--sink file`, the choice is explicit; any other value throws.
|
|
76
|
+
(src/commands/harvest.js:20-33)
|
|
77
|
+
|
|
78
|
+
### Harvest -- git sink (`sync-area`)
|
|
79
|
+
|
|
80
|
+
The git sink delegates entirely to `syncArea`.
|
|
81
|
+
(src/commands/harvest.js:21-23)
|
|
82
|
+
|
|
83
|
+
- Requires a linked pack for the namespace; throws if none or multiple match.
|
|
84
|
+
(src/lib/pack-resolve.js:44-55)
|
|
85
|
+
- Requires `pack.yaml` in the target pack directory. (src/commands/sync-area.js:22-25)
|
|
86
|
+
- Asserts that the pack's namespace (derived from `manifest.name` by replacing `@scope/`
|
|
87
|
+
separator with `-`) matches the area namespace. Throws on mismatch.
|
|
88
|
+
(src/lib/render-core.js:91-101) (src/lib/pack-resolve.js:17-20)
|
|
89
|
+
- Calls `renderArea` to fold the side-car rooms into the pack (see below).
|
|
90
|
+
(src/commands/sync-area.js:29)
|
|
91
|
+
- Bumps `pack.yaml` version using `semver.inc` at the requested level (default: patch).
|
|
92
|
+
(src/lib/pack-manifest.js:71-85) (src/commands/sync-area.js:31)
|
|
93
|
+
- If the pack directory is a git repo, commits all changes with the message
|
|
94
|
+
`content(<area>): sync authored edits, bump <old> -> <new>`.
|
|
95
|
+
(src/commands/sync-area.js:33-38)
|
|
96
|
+
- If the pack directory is not a git repo, bumps but does not commit; warns.
|
|
97
|
+
(src/commands/sync-area.js:39-41)
|
|
98
|
+
- `--minor` and `--major` flags set the bump level; default is patch.
|
|
99
|
+
(bin/tapestry.js:406-408)
|
|
100
|
+
- Prints "To publish + deploy, push the pack repo" with the pack directory path.
|
|
101
|
+
(src/commands/sync-area.js:46-48)
|
|
102
|
+
|
|
103
|
+
### Harvest -- file sink
|
|
104
|
+
|
|
105
|
+
The file sink never bumps the version; it captures an exact snapshot at the manifest's
|
|
106
|
+
current version. (src/lib/file-sink.js:19-21)
|
|
107
|
+
|
|
108
|
+
- If a linked pack exists for the namespace, copies the entire pack directory (excluding
|
|
109
|
+
`.git`, `node_modules`, `.DS_Store`, and `.tgz` files) into a temp build directory.
|
|
110
|
+
The rendered area content is then folded into that copy.
|
|
111
|
+
(src/lib/file-sink.js:30-39) (src/lib/tarball-builder.js:7)
|
|
112
|
+
- If no linked pack exists (hobbyist / no owned repo), synthesizes a minimal `pack.yaml`
|
|
113
|
+
using the namespace to derive `@scope/name`. (src/lib/file-sink.js:36-38)
|
|
114
|
+
(src/lib/pack-manifest.js:53-68)
|
|
115
|
+
- Calls `renderArea` to fold the side-car rooms into the temp build directory.
|
|
116
|
+
(src/lib/file-sink.js:39)
|
|
117
|
+
- Builds a `.tgz` from the temp dir. Output path defaults to `<shortName>-<version>.tgz`
|
|
118
|
+
in the current directory; overridden with `--out <path>`. (src/lib/file-sink.js:43-49)
|
|
119
|
+
- Cleans up the temp directory in a `finally` block. (src/lib/file-sink.js:56-58)
|
|
120
|
+
- Prints "This .tgz is a portable, installable pack" after completion.
|
|
121
|
+
(src/lib/file-sink.js:55-56)
|
|
122
|
+
- `--name <@scope/pack>` overrides the synthesized pack name (file sink only; only
|
|
123
|
+
meaningful when no linked pack is found). (src/lib/file-sink.js:36)
|
|
124
|
+
|
|
125
|
+
### renderArea (shared by both sinks)
|
|
126
|
+
|
|
127
|
+
- Reads room YAML files from `<gameRoot>/data/areas/<area>/rooms/*.yaml`.
|
|
128
|
+
Throws if the directory is absent or empty. (src/lib/render-core.js:13-20)
|
|
129
|
+
- Copies `area.yaml` from the game-root side-car if present; creates a minimal one if absent
|
|
130
|
+
and the target does not already have one. (src/lib/render-core.js:26-35)
|
|
131
|
+
- Writes each room file to `<targetDir>/areas/<area>/rooms/<file>`, creating directories as
|
|
132
|
+
needed. (src/lib/render-core.js:37-50)
|
|
133
|
+
- Without `--force`: throws if a room file in the target differs from the incoming side-car.
|
|
134
|
+
(src/lib/render-core.js:41-45)
|
|
135
|
+
- With `--force`: overwrites divergent room files silently. (src/lib/render-core.js:40-49)
|
|
136
|
+
- After writing rooms, copies oracle side-car files: `places-oracle.yaml` at the area root and
|
|
137
|
+
any `*-oracle-table.yaml` files anywhere under the area tree (including inside `mobs/` and
|
|
138
|
+
`items/` subdirectories). The same divergence guard (throw without `--force`, overwrite with
|
|
139
|
+
`--force`) applies. (src/lib/render-core.js) (src/lib/pack-manifest.js)
|
|
140
|
+
- After copying oracle side-cars, copies all `*.yaml` files from `mobs/` and `items/` under the
|
|
141
|
+
area side-car tree into the corresponding `areas/<area>/mobs/` and `areas/<area>/items/`
|
|
142
|
+
directories in the target pack, including any `*-oracle-table.yaml` files in those directories.
|
|
143
|
+
The same divergence guard applies. (src/lib/render-core.js)
|
|
144
|
+
- After writing, calls `ensureContentGlobs` to additively add `area_definitions`, `rooms`,
|
|
145
|
+
`oracle_tables`, `places_oracle`, `mobs`, and `items` glob entries to `pack.yaml` if not
|
|
146
|
+
already present. (src/lib/render-core.js) (src/lib/pack-manifest.js)
|
|
147
|
+
- `reconcileDependencies` is a deliberate no-op: rooms are self-contained; the seam is wired
|
|
148
|
+
for a future slice that will scan for cross-pack references in mobs/items/quests.
|
|
149
|
+
(src/lib/render-core.js)
|
|
150
|
+
|
|
151
|
+
### Side-car removal (default behavior)
|
|
152
|
+
|
|
153
|
+
After rendering, unless `--keep-sidecars` is given, `removeSideCars` deletes the room files
|
|
154
|
+
from the game-root side-car tree and prunes the `rooms/` and area directories if they become
|
|
155
|
+
empty. `area.yaml` in the game root is also deleted. (src/lib/render-core.js:69-88)
|
|
156
|
+
|
|
157
|
+
## Rejected and Reverted
|
|
158
|
+
|
|
159
|
+
- `sync-area` was the original name for `harvest --sink git`. It prints a deprecation warning
|
|
160
|
+
and delegates to `harvest` with `sink: 'git'` forced.
|
|
161
|
+
(bin/tapestry.js:412-425)
|
|
162
|
+
- `export-area` is a hidden alias with the same deprecation path.
|
|
163
|
+
(bin/tapestry.js:428-439)
|
|
164
|
+
|
|
165
|
+
## Change Log
|
|
166
|
+
|
|
167
|
+
- 2026-06-23 (0.11.0): renderArea carries oracle table side-cars (places-oracle.yaml, *-oracle-table.yaml)
|
|
168
|
+
and mints mob/item instance files (mobs/*.yaml, items/*.yaml) into the target pack alongside
|
|
169
|
+
area.yaml and rooms/. CONTENT_GLOBS gains oracle_tables, places_oracle, mobs, items.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|