@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.
@@ -0,0 +1,206 @@
1
+ ---
2
+ capability: pack-lifecycle
3
+ last-updated: 2026-06-20
4
+ ---
5
+
6
+ # pack-lifecycle
7
+
8
+ Capability spec for project scaffolding, pack installation and removal, boot-order management,
9
+ local development linking, tarball building, and publishing to the registry.
10
+
11
+ ## Overview
12
+
13
+ pack-lifecycle covers the full lifecycle a user exercises to create a game project and manage
14
+ its installed packs: scaffolding a new project (`tapestry init`), installing and removing
15
+ packs from the registry, updating to newer compatible versions, toggling packs in or out of
16
+ the engine boot order, attaching a local working copy for development (`link`), building a
17
+ pack tarball for inspection, and publishing or unpublishing a pack on the registry.
18
+
19
+ Supporting commands `create pack`, `list`, and `outdated` are also in this boundary.
20
+
21
+ The project manifest is `tapestry.yaml`; the lock file is `tapestry-lock.yaml`; the boot
22
+ order file is `tapestry-boot.yaml`; links are tracked in `tapestry-links.yaml`.
23
+
24
+ ## Behavior
25
+
26
+ ### init
27
+
28
+ - Aborts if `tapestry.yaml` already exists in the target directory.
29
+ (src/commands/init.js:127-129)
30
+ - Fetches the preset list from the registry (`/v1/presets`). If the list endpoint returns 404,
31
+ falls back to fetching the `starter` preset directly. If exactly one preset is available it
32
+ is selected automatically; if multiple exist, the user is prompted to choose.
33
+ (src/commands/init.js:133-151)
34
+ - With `--yes`, skips all interactive prompts and uses directory name as game name, handle
35
+ `admin`, email `admin@localhost`, password `changeme`, telemetry off; prints a warning about
36
+ default credentials. (src/commands/init.js:166-175)
37
+ - Writes `tapestry.yaml` with the preset's engine version and channel, and dependency ranges
38
+ pinned with a `^` prefix from the preset's pack versions. (src/commands/init.js:157-161)
39
+ (src/commands/init.js:226-227)
40
+ - Writes `server.yaml` with server name, telnet port 4000, websocket port 4001, admin handle
41
+ and email, and telemetry block (commented out unless telemetry was selected).
42
+ (src/commands/init.js:228-229)
43
+ - Creates a `packs/` directory and a `.gitignore` excluding `packs/`, `.tapestry-engine/`,
44
+ `tapestry-links.yaml`, and `data/`. (src/commands/init.js:238-241)
45
+ - Warns if no `.git` directory is found. (src/commands/init.js:263-266)
46
+ - `buildManifest` and `buildServerYaml` are exported for test injection.
47
+ (src/commands/init.js:269)
48
+
49
+ ### install
50
+
51
+ - Requires `tapestry.yaml`; throws if absent. (src/commands/install.js:83-86)
52
+ - With no argument, skips all linked packs, then checks whether the lock file is current
53
+ (deps_hash matches a SHA-256 of the sorted `name@range` dependency entries). If current,
54
+ installs from the lock; otherwise resolves freshly via the registry.
55
+ (src/commands/install.js:107-122) (src/lib/lock-file.js:10-13)
56
+ - With a package argument, adds or updates the entry in `tapestry.yaml` dependencies, then
57
+ resolves and installs. A bare `@scope/name` (no range) is resolved and the entry is pinned
58
+ at `^<resolvedVersion>`; an explicit `@scope/name@range` writes the supplied range as-is and
59
+ is not re-pinned to the resolved version. (src/commands/install.js:92-105)
60
+ (src/commands/install.js:101-103)
61
+ - Skips a package entirely if it is already linked (local working copy takes precedence).
62
+ (src/commands/install.js:37-41)
63
+ - Skips a package if the installed version already matches the resolved version.
64
+ (src/commands/install.js:50-54)
65
+ - Downloads each tarball to a temp file, verifies its `sha256-` integrity hash, extracts into
66
+ `packs/<scope>/<name>/`, then removes the temp file. (src/commands/install.js:64-75)
67
+ - After installing, calls `addPackageToBoot` to register the pack (and any module entry)
68
+ in `tapestry-boot.yaml`. (src/commands/install.js:77-78)
69
+ - Writes the lock file with `lockfile_version: 1`, `deps_hash`, and the full resolved map.
70
+ (src/commands/install.js:128)
71
+
72
+ ### uninstall
73
+
74
+ - Requires `tapestry.yaml` and that the package appears in `dependencies`; throws otherwise.
75
+ (src/commands/uninstall.js:14-22)
76
+ - Deletes `packs/<scope>/<name>/` if present. (src/commands/uninstall.js:24-28)
77
+ - Removes the entry from `tapestry.yaml` and from `lock.resolved`, then writes both files.
78
+ (src/commands/uninstall.js:29-39)
79
+ - Removes the pack (and its module entry) from `tapestry-boot.yaml`.
80
+ (src/commands/uninstall.js:40)
81
+ - Transitive dependencies are NOT automatically removed; a note to run `tapestry install` is
82
+ printed. (src/commands/uninstall.js:43)
83
+
84
+ ### update
85
+
86
+ - Resolves fresh versions for all dependencies (or one if a package name is given), bypassing
87
+ the lock file. Merges with the existing lock so packages not being updated keep their pinned
88
+ entries. (src/commands/update.js:36-43)
89
+ - Does NOT pass an auth token to the resolver (update.js:39 calls `resolve(deps, url)` with
90
+ no token, unlike install which passes `loadAccess()`). UNVERIFIED: whether this prevents
91
+ updating private packs; the install path always loads the token.
92
+ - Deletes the old install directory before downloading the new version.
93
+ (src/commands/update.js:54-62)
94
+ - Updates `tapestry-boot.yaml` for the new version via `addPackageToBoot`.
95
+ (src/commands/update.js:80-81)
96
+ - Reports "up to date" and skips download when the resolved version matches what the lock
97
+ already has. (src/commands/update.js:46-49)
98
+
99
+ ### enable / disable
100
+
101
+ - Both require `tapestry.yaml`. (src/commands/enable.js:4-9) (src/commands/disable.js:1-16)
102
+ - Delegate to `enablePackage` / `disablePackage` in boot.js, which set `enabled: true/false`
103
+ on the pack entry and on every module entry for that package in `tapestry-boot.yaml`.
104
+ (src/lib/boot.js:51-76)
105
+ - Throw if the package is not in `tapestry-boot.yaml`. (src/lib/boot.js:56) (src/lib/boot.js:68)
106
+
107
+ ### link / unlink
108
+
109
+ - `link` requires `tapestry.yaml` and an existing path. (src/commands/link.js:33-38)
110
+ - Reads the target's `pack.yaml`, records the name-to-absolute-path mapping in
111
+ `tapestry-links.yaml`, and adds the pack to `tapestry-boot.yaml`. (src/commands/link.js:45-47)
112
+ - Appends `tapestry-links.yaml` to `.gitignore` if not already present.
113
+ (src/commands/link.js:22-30)
114
+ - Warns if `active: false` is set in the linked pack's manifest. (src/commands/link.js:49-52)
115
+ - With `--skip-install`, skips dependency resolution and prints warnings for any missing deps
116
+ instead. (src/commands/link.js:53-58)
117
+ - Without `--skip-install`, resolves and installs only the deps of the linked pack that are not
118
+ already on disk. Rolls back all changes (link record, boot entry, any newly installed deps)
119
+ on resolution failure. (src/commands/link.js:62-109)
120
+ - `unlink` removes the link record from `tapestry-links.yaml`, deletes any materialized copy
121
+ from `packs/`, and removes the pack and its module from `tapestry-boot.yaml`.
122
+ (src/commands/link.js:112-121)
123
+ - `link --list` (or `link` with no path) prints each linked name and absolute path, flagging
124
+ missing paths with `(MISSING)`. (src/commands/link.js:123-133)
125
+
126
+ ### Boot-order management
127
+
128
+ - `tapestry-boot.yaml` has two keys: `modules` (list of module class entries) and `packs`
129
+ (map of pack name to `{ enabled }` flag). (src/lib/boot.js:9-14)
130
+ - `addPackageToBoot` writes the pack entry (always `enabled: true`) and, if the manifest has a
131
+ `module.class`, appends a module entry then topologically sorts all modules.
132
+ (src/lib/boot.js:22-41)
133
+ - `removePackageFromBoot` deletes the pack entry and all module entries for that package.
134
+ (src/lib/boot.js:44-49)
135
+ - Topological sort honors `module.after` to order .NET classes that depend on each other.
136
+ Cycles throw. (src/lib/boot.js:79-110)
137
+
138
+ ### Dependency resolution
139
+
140
+ - Resolution is a breadth-first queue over `name@range` pairs, fetching package metadata from
141
+ `/v1/packages/<name>`. (src/lib/semver-resolver.js:21-79)
142
+ - Dist-tag names (all lowercase letters) are resolved to a version range via `meta.dist_tags`
143
+ before semver selection. (src/lib/semver-resolver.js:39-44)
144
+ - Picks the maximum semver version satisfying the range. Conflicts (same package, incompatible
145
+ ranges from different requirers) throw with a descriptive message.
146
+ (src/lib/semver-resolver.js:24-32) (src/lib/semver-resolver.js:46-51)
147
+ - Transitive dependencies are queued from the resolved manifest's `dependencies` field; peer
148
+ dependencies emit a warning if not present but do not block resolution.
149
+ (src/lib/semver-resolver.js:69-79)
150
+ - Package names must match `@scope/name` format; path traversal (`..`, `//`) is rejected at
151
+ the HTTP layer. (src/lib/registry-client.js:7-16)
152
+
153
+ ### pack (build tarball)
154
+
155
+ - Runs `validate` first; aborts on validation failure. (src/commands/pack.js:7)
156
+ - For ESM packs (`scripts_format: esm`), compiles `scripts/**/*.ts` to `dist/scripts/**/*.js`
157
+ using the bundled `tsc` and the pack's `tsconfig.json`; legacy packs are a no-op.
158
+ (src/commands/pack.js:14) (src/lib/ts-build.js:8-20)
159
+ - Builds a `.tgz` under a `package/` prefix in the current directory named
160
+ `<shortName>-<version>.tgz`. (src/commands/pack.js:16) (src/lib/tarball-builder.js:16-26)
161
+ - Excludes `.git`, `node_modules`, `.DS_Store`, and `.tgz` files from the archive.
162
+ (src/lib/tarball-builder.js:7-12)
163
+ - Prints the `sha256-<base64>` integrity hash of the output file.
164
+ (src/commands/pack.js:22) (src/lib/tarball-builder.js:29-33)
165
+
166
+ ### publish
167
+
168
+ - Runs `validate` first; aborts on validation failure. (src/commands/publish.js:18)
169
+ - For ESM packs (`scripts_format: esm`), compiles `scripts/**/*.ts` to `dist/scripts/**/*.js`
170
+ using the bundled `tsc` and the pack's `tsconfig.json` before any network or auth work;
171
+ legacy packs are a no-op. (src/commands/publish.js:21) (src/lib/ts-build.js:8-20)
172
+ - In a GitHub Actions OIDC environment (both `ACTIONS_ID_TOKEN_REQUEST_URL` and
173
+ `ACTIONS_ID_TOKEN_REQUEST_TOKEN` set), fetches a GitHub id-token and exchanges it for a
174
+ scoped access token; also sets a `tag: stable` field on the form.
175
+ (src/commands/publish.js:26-30)
176
+ - Otherwise, requires an existing authenticated session. (src/commands/publish.js:31)
177
+ - Builds the tarball to a temp file, computes integrity, POSTs a multipart form with the
178
+ tarball and JSON metadata (manifest fields plus integrity) to `/v1/publish`.
179
+ (src/commands/publish.js:40-65)
180
+ - Deletes the temp file in a `finally` block. (src/commands/publish.js:71-73)
181
+
182
+ ### create pack (scaffold)
183
+
184
+ - Accepts `@scope/name` (used as-is) or a plain name (scope defaults to `@todo`).
185
+ (src/commands/create-pack.js:8-18)
186
+ - Creates a subdirectory named after the short name in the current directory; throws if it
187
+ already exists. (src/commands/create-pack.js:41-46)
188
+ - Writes the scaffold files from `generatePackFiles` and prints each one.
189
+ (src/commands/create-pack.js:48-57) (src/scaffold/templates.js)
190
+
191
+ ### list
192
+
193
+ - Reads the lock file and boot file; prints package name, resolved version, type (from the
194
+ installed `pack.yaml`), and enabled/disabled status.
195
+ (src/commands/list.js:17-48)
196
+ - If no packages are installed, prints "No packages installed." (src/commands/list.js:46-48)
197
+ - Also prints linked packs below the installed list, flagging missing paths.
198
+ (src/commands/list.js:50-58)
199
+
200
+ ## Rejected and Reverted
201
+
202
+ - None on record.
203
+
204
+ ## Change Log
205
+
206
+ - 2026-06-20 (0.10.0): `pack` and `publish` compile ESM packs (`scripts_format: esm`) via `tsc` before archiving; new `tapestry types` command vendors the engine `.d.ts`. See changes/2026-06-20-pack-esm-build.md.
@@ -0,0 +1,127 @@
1
+ ---
2
+ capability: registry-auth
3
+ last-updated: 2026-06-13
4
+ ---
5
+
6
+ # registry-auth
7
+
8
+ Capability spec for user identity, session management, and OIDC trusted publishing.
9
+
10
+ ## Overview
11
+
12
+ registry-auth covers everything the CLI does on behalf of a user's identity: password-based
13
+ login that exchanges credentials for an access/refresh token pair; silent proactive token
14
+ refresh before expiry; logout with best-effort server-side revocation; account registration
15
+ and password change; and the OIDC trusted-publishing path that lets a GitHub Actions workflow
16
+ publish a pack without stored credentials.
17
+
18
+ Trust binding management (`tapestry trust add/list/rm`) is in this boundary because it
19
+ configures the OIDC flow on the registry side.
20
+
21
+ ## Behavior
22
+
23
+ ### Session storage
24
+
25
+ - The session file lives at `~/.tapestryrc` (resolved via `os.homedir()`).
26
+ (src/lib/auth.js:9)
27
+ - Written with mode `0o600` (user-read/write only). (src/lib/auth.js:28)
28
+ - Contains four YAML keys: `registry` (the registry URL), `access` (the access JWT),
29
+ `access_exp` (the JWT `exp` claim decoded as a Unix timestamp), and `refresh` (the refresh
30
+ token string). (src/lib/auth.js:24-29)
31
+ - `decodeExp` extracts `exp` from the JWT payload by base64url-decoding the second segment.
32
+ Returns null on any parse error. (src/lib/auth.js:38-45)
33
+ - `readSession` returns null if the file is absent or unparseable.
34
+ (src/lib/auth.js:13-20)
35
+ - `clearSession` deletes the file if it exists; does nothing otherwise.
36
+ (src/lib/auth.js:32-36)
37
+
38
+ ### Token refresh
39
+
40
+ - `loadAccess` returns the stored access token if `access_exp` is present and the token will
41
+ not expire within the next 60 seconds (`REFRESH_SKEW_SECONDS`). (src/lib/auth.js:52-54)
42
+ - If the access token is expired or near expiry, `loadAccess` silently POSTs the refresh
43
+ token to `<registry>/v1/auth/refresh` and saves the new access and refresh tokens.
44
+ (src/lib/auth.js:58-68)
45
+ - If the refresh request fails, the session is cleared and `loadAccess` returns null (treated
46
+ as "not logged in" by callers). (src/lib/auth.js:63-65)
47
+ - `requireAccess` wraps `loadAccess` and throws "Not logged in. Run: tapestry login" if the
48
+ result is null. (src/lib/auth.js:72-77)
49
+ - Sessions that have an `access` token but no `refresh` token (legacy format) are treated as
50
+ absent by `loadAccess`. (src/lib/auth.js:48-50)
51
+
52
+ ### login
53
+
54
+ - Prompts for email and password interactively unless both are passed programmatically.
55
+ (src/commands/login.js:8-16) (src/commands/login.js:19-21)
56
+ - POSTs `{ email, password }` to `<registry>/v1/auth/login`. (src/commands/login.js:23-28)
57
+ - On success, saves the returned `access_token`, its decoded `exp`, and `refresh_token` to
58
+ `~/.tapestryrc`. Prints "Logged in." (src/commands/login.js:33-34)
59
+ - The registry URL defaults to `TAPESTRY_REGISTRY` env var or
60
+ `https://registry.tapestryengine.com`. (src/lib/registry-client.js:5)
61
+
62
+ ### logout
63
+
64
+ - Reads the current session. If a refresh token is present, attempts a server-side revocation
65
+ by POSTing the refresh token to `<registry>/v1/auth/logout`. Server errors are swallowed;
66
+ the local session is always cleared regardless. (src/commands/logout.js:6-17)
67
+ - Deletes `~/.tapestryrc`. Prints "Logged out." (src/commands/logout.js:14-16)
68
+
69
+ ### register
70
+
71
+ - Prompts for handle, email, and password unless all three are passed programmatically.
72
+ (src/commands/register.js:8-18)
73
+ - POSTs `{ handle, email, password }` to `<registry>/v1/auth/register`. On success, saves
74
+ the returned token pair and prints "Registered as <handle>. Logged in."
75
+ (src/commands/register.js:24-36)
76
+
77
+ ### change-password
78
+
79
+ - Requires an active session (`requireAccess`); throws if not logged in.
80
+ (src/commands/change-password.js:9)
81
+ - Prompts for current password, new password, and confirmation; throws if the two new-password
82
+ entries do not match. (src/commands/change-password.js:10-17)
83
+ - POSTs `{ currentPassword, newPassword }` to `<registry>/v1/auth/change-password` with a
84
+ Bearer token. Prints "Password changed." on success. (src/commands/change-password.js:18-24)
85
+
86
+ ### OIDC trusted publishing
87
+
88
+ - CI mode is detected when both `ACTIONS_ID_TOKEN_REQUEST_URL` and
89
+ `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variables are set. (src/lib/oidc.js:7-9)
90
+ - `fetchGitHubIdToken` fetches the GitHub OIDC id-token by GETting
91
+ `$ACTIONS_ID_TOKEN_REQUEST_URL&audience=<audience>` with the request token in the
92
+ Authorization header. The audience constant is `https://registry.tapestryengine.com`.
93
+ (src/lib/oidc.js:5) (src/lib/oidc.js:11-24)
94
+ - `exchangeOIDCForAccess` POSTs the id-token (as a Bearer token) and the target scope to
95
+ `<registry>/v1/token`, returning the `access_token` from the response.
96
+ (src/lib/registry-client.js:169-178)
97
+ - `publish` uses OIDC in CI mode and also appends `tag: stable` to the publish form;
98
+ in interactive mode it uses the standard session access token.
99
+ (src/commands/publish.js:22-30)
100
+
101
+ ### Trust binding management
102
+
103
+ Trust bindings authorize a GitHub repository to publish packs to a scope via OIDC without
104
+ stored credentials. All three sub-commands require an active session.
105
+
106
+ - `trust add <scope> <repo>` POSTs `{ scope, repo }` (plus optional `ref` and `environment`)
107
+ to `<registry>/v1/trusted-publishers`. Prints the assigned id on success.
108
+ (src/lib/registry-client.js:141-149) (src/commands/trust.js:13-19)
109
+ - `trust list` GETs `/v1/trusted-publishers` (optionally filtered with `?scope=`).
110
+ Prints each binding as `#<id> @<scope> <- <repo> (ref=... env=...)`.
111
+ (src/lib/registry-client.js:151-158) (src/commands/trust.js:22-33)
112
+ - `trust rm <id>` DELETEs `/v1/trusted-publishers/<id>`.
113
+ (src/lib/registry-client.js:160-167) (src/commands/trust.js:36-40)
114
+
115
+ ### Rate limiting
116
+
117
+ - HTTP 429 responses are detected before any other error handling. If the response includes a
118
+ `retry-after` header (in seconds), the error message includes a "Try again in N min." hint.
119
+ (src/lib/registry-client.js:20-24)
120
+
121
+ ## Rejected and Reverted
122
+
123
+ - None on record.
124
+
125
+ ## Change Log
126
+
127
+ - None on record.
@@ -0,0 +1,70 @@
1
+ ---
2
+ capability: validate
3
+ last-updated: 2026-06-13
4
+ ---
5
+
6
+ # validate
7
+
8
+ Capability spec for offline pack manifest validation.
9
+
10
+ ## Overview
11
+
12
+ `tapestry validate` reads `pack.yaml` from the current directory and checks it against the
13
+ Zod schema that defines a valid pack manifest. No network calls are made; no registry is
14
+ consulted. The command is also called internally by `pack` and `publish` before they proceed.
15
+
16
+ ## Behavior
17
+
18
+ - Looks for `pack.yaml` (the constant `PACK_MANIFEST`) in the current working directory.
19
+ (src/lib/manifest.js:3) (src/commands/validate.js:10-11)
20
+ - If `pack.yaml` is absent but `tapestry.yaml` is present, throws a specific error explaining
21
+ that the file found is a server manifest, not a pack manifest.
22
+ (src/commands/validate.js:13-20)
23
+ - If neither file is present, throws "No pack.yaml found in current directory".
24
+ (src/commands/validate.js:21)
25
+ - Runs `validatePackageManifest` which calls Zod's `safeParse` on the YAML-parsed data.
26
+ (src/commands/validate.js:25-26) (src/schema/manifest.js:58-60)
27
+ - On success, prints "OK <name> v<version>" with two leading spaces. (src/commands/validate.js:38-39)
28
+ - On failure, iterates Zod issues and prints each as " error: <fieldPath> - <message>".
29
+ The field path is the dot-joined `issue.path`, or "root" if the path is empty.
30
+ (src/commands/validate.js:29-36)
31
+ - For an `engine` field that is an object (the server manifest format) in a document being
32
+ validated as a pack manifest, appends a hint that `engine` must be a version constraint
33
+ string in pack manifests. (src/commands/validate.js:31-35)
34
+ - Throws with a count of validation errors so callers (`pack`, `publish`) can abort.
35
+ (src/commands/validate.js:37)
36
+
37
+ ### Pack manifest schema
38
+
39
+ Required fields (src/schema/manifest.js:7-39):
40
+
41
+ - `name` -- string matching `@scope/package-name` (lowercase letters, digits, hyphens only).
42
+ - `version` -- non-empty string (no semver enforcement at this layer).
43
+ - `type` -- one of `core`, `module`, `world`.
44
+ - `display_name` -- non-empty string.
45
+ - `description` -- non-empty string.
46
+ - `author` -- non-empty string.
47
+ - `license` -- non-empty string.
48
+ - `engine` -- non-empty string (a version constraint, e.g. `>=0.0.1`).
49
+ - `validation` -- one of `strict`, `lenient`.
50
+
51
+ Optional fields (src/schema/manifest.js:17-38):
52
+
53
+ - `dependencies` -- record of package name to version range string.
54
+ - `peerDependencies` -- record of package name to version range string.
55
+ - `provides` -- array of strings.
56
+ - `tags` -- string.
57
+ - `module` -- object with required `assembly`, `class`, `implements`; optional `after`.
58
+ - `content` -- record of glob key to glob pattern string.
59
+ - `client` -- object with `manifest`, `assets`, `min_client_version`.
60
+ - `meta` -- object with optional `commands` (string array), `properties` (number), `keywords`
61
+ (string array).
62
+ - `private` -- boolean.
63
+
64
+ ## Rejected and Reverted
65
+
66
+ - None on record.
67
+
68
+ ## Change Log
69
+
70
+ - None on record.
@@ -4,6 +4,7 @@ const { parseAreaRef, resolvePackDirOrNull } = require('../lib/pack-resolve');
4
4
  const { isRepo } = require('../lib/git');
5
5
  const { syncArea } = require('./sync-area');
6
6
  const { fileSink } = require('../lib/file-sink');
7
+ const { registrySink } = require('../lib/registry-sink');
7
8
 
8
9
  // Umbrella harvest verb. Auto-detects the sink (owned linked pack that is a git repo -> git;
9
10
  // else file) unless --sink is explicit. The render core is shared by every sink.
@@ -23,14 +24,23 @@ async function harvest(areaRef, options = {}) {
23
24
  return syncArea(areaRef, options);
24
25
  }
25
26
  if (sink === 'file') {
26
- // The file sink snapshots at the current version -- it ignores --minor/--major (git-sink only).
27
+ // The file sink snapshots at the current version -- it ignores --minor/--major.
27
28
  return fileSink(areaRef, {
28
29
  cwd, gameRoot, namespace, area,
29
30
  force: options.force, keepSidecars: options.keepSidecars,
30
31
  out: options.out, name: options.name, pack: options.pack,
31
32
  });
32
33
  }
33
- throw new Error(`Unknown sink '${sink}'. Use 'file' or 'git'.`);
34
+ if (sink === 'registry') {
35
+ return registrySink(areaRef, {
36
+ cwd, gameRoot, namespace, area,
37
+ force: options.force, keepSidecars: options.keepSidecars,
38
+ pack: options.pack, name: options.name,
39
+ bump: options.bump,
40
+ registryUrl: options.registryUrl,
41
+ });
42
+ }
43
+ throw new Error(`Unknown sink '${sink}'. Use 'file', 'git', or 'registry'.`);
34
44
  }
35
45
 
36
46
  module.exports = { harvest };
@@ -5,11 +5,14 @@ const { readYaml } = require('../util/yaml');
5
5
  const { buildTarball, computeIntegrity } = require('../lib/tarball-builder');
6
6
  const { PACK_MANIFEST } = require('../lib/manifest');
7
7
  const { validate } = require('./validate');
8
+ const { buildTypeScript } = require('../lib/ts-build');
8
9
 
9
10
  async function pack({ cwd = process.cwd() } = {}) {
10
11
  validate({ cwd });
11
12
 
12
13
  const manifest = readYaml(path.join(cwd, PACK_MANIFEST));
14
+ buildTypeScript(cwd, manifest);
15
+
13
16
  const shortName = manifest.name.split('/')[1];
14
17
  const outputPath = path.join(cwd, `${shortName}-${manifest.version}.tgz`);
15
18
 
@@ -12,11 +12,13 @@ const { PACK_MANIFEST } = require('../lib/manifest');
12
12
  const { requireAccess } = require('../lib/auth');
13
13
  const { DEFAULT_REGISTRY, throwIfError, exchangeOIDCForAccess } = require('../lib/registry-client');
14
14
  const { detectCI, fetchGitHubIdToken, AUDIENCE } = require('../lib/oidc');
15
+ const { buildTypeScript } = require('../lib/ts-build');
15
16
 
16
17
  async function publish({ cwd = process.cwd(), registryUrl = DEFAULT_REGISTRY } = {}) {
17
18
  validate({ cwd });
18
19
 
19
20
  const manifest = readYaml(path.join(cwd, PACK_MANIFEST));
21
+ buildTypeScript(cwd, manifest);
20
22
 
21
23
  const scope = manifest.name.match(/^@([^/]+)\//)[1];
22
24
  const ciMode = detectCI();
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function types({ cwd = process.cwd() } = {}) {
7
+ const src = path.join(__dirname, '..', '..', 'types', 'tapestry-engine.d.ts');
8
+ const destDir = path.join(cwd, 'types');
9
+ fs.mkdirSync(destDir, { recursive: true });
10
+ const dest = path.join(destDir, 'tapestry-engine.d.ts');
11
+ fs.copyFileSync(src, dest);
12
+ console.log(`Wrote ${path.relative(cwd, dest)}`);
13
+ }
14
+
15
+ module.exports = { types };
@@ -7,6 +7,10 @@ const { readYaml, writeYaml } = require('../util/yaml');
7
7
  const CONTENT_GLOBS = {
8
8
  area_definitions: 'areas/**/area.yaml',
9
9
  rooms: 'areas/**/rooms/*.yaml',
10
+ oracle_tables: 'areas/**/*-oracle-table.yaml',
11
+ places_oracle: 'areas/**/places-oracle.yaml',
12
+ mobs: 'areas/**/mobs/*.yaml',
13
+ items: 'areas/**/items/*.yaml',
10
14
  };
11
15
 
12
16
  // Additively ensure the pack manifest declares the given content globs.
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const fetch = require('node-fetch');
7
+ const FormData = require('form-data');
8
+ const { readYaml, writeYaml } = require('../util/yaml');
9
+ const { resolvePackDirOrNull } = require('./pack-resolve');
10
+ const { renderArea, removeSideCars, assertNamespaceMatch } = require('./render-core');
11
+ const { synthesizeManifest, bumpVersion } = require('./pack-manifest');
12
+ const { buildTarball, computeIntegrity } = require('./tarball-builder');
13
+ const { isRepo } = require('./git');
14
+ const { requireAccess } = require('./auth');
15
+ const { DEFAULT_REGISTRY, throwIfError } = require('./registry-client');
16
+
17
+ // Registry sink: render -> tar -> POST /v1/publish.
18
+ // Run where the registry token lives (operator machine or no-git server).
19
+ // Refuses registry-direct when the linked pack is repo-backed (source-of-truth trap).
20
+ //
21
+ // Owned (linked pack, not git repo): mirrors the git sink. Renders into the REAL pack dir
22
+ // so content accumulates in the operator's source of truth across repeated harvests; bumps;
23
+ // tars+POSTs. Edge: if POST fails after render+bump, the real pack holds both - re-running
24
+ // re-renders to a no-op and re-bumps (a registry version gap, accepted, source truth intact).
25
+ //
26
+ // Hobbyist (no linked pack): mirrors the file sink. Synthesizes a manifest, renders into a
27
+ // temp dir, tars+POSTs. No persistent bump. A second publish at 0.1.0 will fail at the
28
+ // registry - the intended signal to set up a real pack.
29
+ async function registrySink(areaRef, options) {
30
+ const { cwd, gameRoot, namespace, area } = options;
31
+ const force = !!options.force;
32
+ const keepSidecars = !!options.keepSidecars;
33
+ const registryUrl = options.registryUrl || DEFAULT_REGISTRY;
34
+
35
+ const packDir = resolvePackDirOrNull(cwd, namespace, options.pack);
36
+
37
+ if (packDir && isRepo(packDir)) {
38
+ throw new Error(
39
+ `Cannot publish registry-direct: '${packDir}' is a git repo.\n` +
40
+ `Harvest to file instead: tapestry harvest ${areaRef} --sink file\n` +
41
+ `Pull the .tgz to the machine that owns the repo, unpack into the repo, commit, push,\n` +
42
+ `and let CI publish.`
43
+ );
44
+ }
45
+
46
+ let files;
47
+ let manifest;
48
+ let tmpBuild = null;
49
+ let tmpTgz;
50
+
51
+ try {
52
+ if (packDir) {
53
+ const existingManifest = readYaml(path.join(packDir, 'pack.yaml')) || {};
54
+ assertNamespaceMatch(existingManifest, namespace, packDir);
55
+
56
+ // Fail loudly on EACCES before any mutation - never silently.
57
+ try {
58
+ fs.accessSync(packDir, fs.constants.W_OK);
59
+ } catch (e) {
60
+ throw new Error(
61
+ `Cannot write to pack directory ${packDir}: ${e.message}. ` +
62
+ `The user running tapestry may not own that directory.`
63
+ );
64
+ }
65
+
66
+ // Render into the REAL pack dir (content accumulates), then bump.
67
+ ({ files } = renderArea(packDir, { gameRoot, area, force }));
68
+ bumpVersion(packDir, options.bump || 'patch');
69
+ manifest = readYaml(path.join(packDir, 'pack.yaml'));
70
+ } else {
71
+ tmpBuild = fs.mkdtempSync(path.join(os.tmpdir(), 'tapestry-harvest-'));
72
+ manifest = synthesizeManifest(namespace, { name: options.name });
73
+ writeYaml(path.join(tmpBuild, 'pack.yaml'), manifest);
74
+ ({ files } = renderArea(tmpBuild, { gameRoot, area, force }));
75
+ // Re-read after render in case ensureContentGlobs updated the manifest.
76
+ manifest = readYaml(path.join(tmpBuild, 'pack.yaml'));
77
+ }
78
+
79
+ const buildDir = tmpBuild || packDir;
80
+ const shortName = manifest.name.split('/')[1];
81
+ tmpTgz = path.join(os.tmpdir(), `tapestry-publish-${shortName}-${manifest.version}.tgz`);
82
+
83
+ await buildTarball(buildDir, tmpTgz);
84
+ const integrity = computeIntegrity(tmpTgz);
85
+ const token = await requireAccess();
86
+
87
+ const form = new FormData();
88
+ form.append('tarball', fs.createReadStream(tmpTgz), {
89
+ filename: `${manifest.version}.tgz`,
90
+ contentType: 'application/gzip',
91
+ });
92
+ form.append('metadata', JSON.stringify({ ...manifest, integrity }));
93
+
94
+ const res = await fetch(`${registryUrl}/v1/publish`, {
95
+ method: 'POST',
96
+ headers: { ...form.getHeaders(), Authorization: `Bearer ${token}` },
97
+ body: form,
98
+ });
99
+ await throwIfError(res, 'Publish failed');
100
+ const result = await res.json();
101
+
102
+ if (!keepSidecars) {
103
+ removeSideCars(gameRoot, area, files);
104
+ }
105
+ console.log(`Harvested area '${area}' and published ${result.name}@${result.version}.`);
106
+ console.log('Run `tapestry update` on your game server to pull the new version.');
107
+ } finally {
108
+ if (tmpTgz && fs.existsSync(tmpTgz)) {
109
+ fs.unlinkSync(tmpTgz);
110
+ }
111
+ if (tmpBuild) {
112
+ fs.rmSync(tmpBuild, { recursive: true, force: true });
113
+ }
114
+ }
115
+ }
116
+
117
+ module.exports = { registrySink };