@tapestry-mud/cli 0.10.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/bin/tapestry.js +4 -4
- package/package.json +2 -2
- package/specs/README.md +1 -1
- package/specs/changes/2026-06-20-pack-esm-build.md +28 -0
- package/specs/harvest.md +15 -5
- package/specs/pack-lifecycle.md +2 -2
- package/src/commands/harvest.js +12 -2
- 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/bin/tapestry.js
CHANGED
|
@@ -411,15 +411,15 @@ async function runHarvest(areaRef, opts) {
|
|
|
411
411
|
program
|
|
412
412
|
.command('harvest <areaRef>')
|
|
413
413
|
.description('Harvest an authored area into a portable pack (areaRef = namespace:area-id)')
|
|
414
|
-
.option('--sink <sink>', 'Output sink: file | git (auto-detected by default)')
|
|
414
|
+
.option('--sink <sink>', 'Output sink: file | git | registry (auto-detected by default)')
|
|
415
415
|
.option('--out <path>', '(file sink) where the .tgz lands')
|
|
416
|
-
.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)')
|
|
417
417
|
.option('--pack <dir>', 'Target pack directory (auto-detected from linked packs by default)')
|
|
418
418
|
.option('--game-root <path>', 'Game root containing data/ (default: current dir)')
|
|
419
419
|
.option('--keep-sidecars', 'Copy instead of move (leave the game-root side-cars in place)')
|
|
420
420
|
.option('--force', 'Overwrite pack files that diverge from the side-car')
|
|
421
|
-
.option('--minor', 'Bump the pack minor version (git sink only; default: patch)')
|
|
422
|
-
.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)')
|
|
423
423
|
.action(runHarvest);
|
|
424
424
|
|
|
425
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",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"commander": "11.1.0",
|
|
21
|
-
"form-data": "4.0.
|
|
21
|
+
"form-data": "4.0.6",
|
|
22
22
|
"inquirer": "8.2.7",
|
|
23
23
|
"js-yaml": "4.1.1",
|
|
24
24
|
"node-fetch": "2.7.0",
|
package/specs/README.md
CHANGED
|
@@ -9,7 +9,7 @@ behave?" from the relevant file alone.
|
|
|
9
9
|
|
|
10
10
|
| Capability | File | Last Updated |
|
|
11
11
|
|------------|------|--------------|
|
|
12
|
-
| pack-lifecycle | [pack-lifecycle.md](pack-lifecycle.md) | 2026-06-
|
|
12
|
+
| pack-lifecycle | [pack-lifecycle.md](pack-lifecycle.md) | 2026-06-20 |
|
|
13
13
|
| validate | [validate.md](validate.md) | 2026-06-13 |
|
|
14
14
|
| harvest | [harvest.md](harvest.md) | 2026-06-13 |
|
|
15
15
|
| registry-auth | [registry-auth.md](registry-auth.md) | 2026-06-13 |
|
|
@@ -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.
|
package/specs/harvest.md
CHANGED
|
@@ -133,12 +133,20 @@ current version. (src/lib/file-sink.js:19-21)
|
|
|
133
133
|
- Without `--force`: throws if a room file in the target differs from the incoming side-car.
|
|
134
134
|
(src/lib/render-core.js:41-45)
|
|
135
135
|
- With `--force`: overwrites divergent room files silently. (src/lib/render-core.js:40-49)
|
|
136
|
-
- After writing,
|
|
137
|
-
|
|
138
|
-
(
|
|
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)
|
|
139
147
|
- `reconcileDependencies` is a deliberate no-op: rooms are self-contained; the seam is wired
|
|
140
148
|
for a future slice that will scan for cross-pack references in mobs/items/quests.
|
|
141
|
-
(src/lib/render-core.js
|
|
149
|
+
(src/lib/render-core.js)
|
|
142
150
|
|
|
143
151
|
### Side-car removal (default behavior)
|
|
144
152
|
|
|
@@ -156,4 +164,6 @@ empty. `area.yaml` in the game root is also deleted. (src/lib/render-core.js:69-
|
|
|
156
164
|
|
|
157
165
|
## Change Log
|
|
158
166
|
|
|
159
|
-
-
|
|
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.
|
package/specs/pack-lifecycle.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
capability: pack-lifecycle
|
|
3
|
-
last-updated: 2026-06-
|
|
3
|
+
last-updated: 2026-06-20
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# pack-lifecycle
|
|
@@ -203,4 +203,4 @@ order file is `tapestry-boot.yaml`; links are tracked in `tapestry-links.yaml`.
|
|
|
203
203
|
|
|
204
204
|
## Change Log
|
|
205
205
|
|
|
206
|
-
-
|
|
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.
|
package/src/commands/harvest.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 };
|
package/src/lib/pack-manifest.js
CHANGED
|
@@ -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 };
|
package/src/lib/render-core.js
CHANGED
|
@@ -6,6 +6,44 @@ const { readYaml, writeYaml } = require('../util/yaml');
|
|
|
6
6
|
const { ensureContentGlobs } = require('./pack-manifest');
|
|
7
7
|
const { packNamespace } = require('./pack-resolve');
|
|
8
8
|
|
|
9
|
+
// Recursively collect files matching a name predicate under a directory.
|
|
10
|
+
// Returns absolute paths.
|
|
11
|
+
function collectFiles(dir, predicate, results = []) {
|
|
12
|
+
if (!fs.existsSync(dir)) {
|
|
13
|
+
return results;
|
|
14
|
+
}
|
|
15
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
collectFiles(path.join(dir, entry.name), predicate, results);
|
|
18
|
+
} else if (predicate(entry.name)) {
|
|
19
|
+
results.push(path.join(dir, entry.name));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Copy oracle side-car yaml files from the area source root into the area dest root,
|
|
26
|
+
// preserving relative paths and applying the divergence guard.
|
|
27
|
+
function copyOracleSideCars(areaSrcRoot, areaDestRoot, force) {
|
|
28
|
+
const oracleFiles = collectFiles(areaSrcRoot, (name) => {
|
|
29
|
+
return name === 'places-oracle.yaml' || name.endsWith('-oracle-table.yaml');
|
|
30
|
+
});
|
|
31
|
+
for (const src of oracleFiles) {
|
|
32
|
+
const rel = path.relative(areaSrcRoot, src);
|
|
33
|
+
const dest = path.join(areaDestRoot, rel);
|
|
34
|
+
const incoming = readYaml(src);
|
|
35
|
+
if (fs.existsSync(dest) && !force) {
|
|
36
|
+
const existing = readYaml(dest);
|
|
37
|
+
if (JSON.stringify(existing) !== JSON.stringify(incoming)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Pack file ${dest} diverges from the side-car. Review the diff and re-run with --force to overwrite.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
43
|
+
writeYaml(dest, incoming);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
9
47
|
// Fold one area's authored side-cars into a target pack directory. Setup-independent:
|
|
10
48
|
// it does not know whether targetDir is a real repo or a temp build dir.
|
|
11
49
|
// Returns { written, files }. Throws if there is nothing to render or a pack file diverges.
|
|
@@ -49,6 +87,35 @@ function renderArea(targetDir, { gameRoot, area, force = false }) {
|
|
|
49
87
|
written++;
|
|
50
88
|
}
|
|
51
89
|
|
|
90
|
+
// Copy oracle table side-cars: places-oracle.yaml at the area root,
|
|
91
|
+
// and any *-oracle-table.yaml files in subdirectories (mobs/, items/, etc.).
|
|
92
|
+
const areaSrcRoot = path.join(gameRoot, 'data', 'areas', area);
|
|
93
|
+
const areaDestRoot = path.join(targetDir, 'areas', area);
|
|
94
|
+
copyOracleSideCars(areaSrcRoot, areaDestRoot, force);
|
|
95
|
+
|
|
96
|
+
// Copy minted mob and item instance files (including their *-oracle-table.yaml).
|
|
97
|
+
for (const sub of ['mobs', 'items']) {
|
|
98
|
+
const srcDir = path.join(gameRoot, 'data', 'areas', area, sub);
|
|
99
|
+
if (fs.existsSync(srcDir)) {
|
|
100
|
+
const subFiles = fs.readdirSync(srcDir).filter((f) => f.endsWith('.yaml'));
|
|
101
|
+
const destDir = path.join(targetDir, 'areas', area, sub);
|
|
102
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
103
|
+
for (const file of subFiles) {
|
|
104
|
+
const src = path.join(srcDir, file);
|
|
105
|
+
const dest = path.join(destDir, file);
|
|
106
|
+
const incoming = readYaml(src);
|
|
107
|
+
if (fs.existsSync(dest) && !force) {
|
|
108
|
+
const existing = readYaml(dest);
|
|
109
|
+
if (JSON.stringify(existing) !== JSON.stringify(incoming)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Pack file ${dest} diverges from the side-car. Review the diff and re-run with --force to overwrite.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
writeYaml(dest, incoming);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
52
119
|
ensureContentGlobs(targetDir);
|
|
53
120
|
reconcileDependencies(targetDir, area);
|
|
54
121
|
|