@looop-games/cli 0.1.33 → 0.1.35
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/CHANGELOG.md +13 -0
- package/lib/agent-surface.mjs +6 -2
- package/lib/dev.mjs +1 -0
- package/lib/room-server.mjs +13 -1
- package/lib/static-server.mjs +6 -0
- package/lib/test-cmd.mjs +22 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,19 @@ Versions: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
14
14
|
|
|
15
15
|
## [Unreleased]
|
|
16
16
|
|
|
17
|
+
## [0.1.35] - 2026-09-02
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Reports landing in `.looop/agent-inbox/` are stamped with `engine`, the engine version `looop dev` was serving when the report was made — so a report read later, after an update, still says which engine produced the numbers.
|
|
22
|
+
|
|
23
|
+
## [0.1.34] - 2026-09-02
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `looop test` on Windows no longer discovers and runs the engine's own scaffold smokes from `node_modules` (or anything under `overrides/` and dot-folders). The gate could never go green there because those smokes always failed; it now runs only your game's tests, as on macOS and Linux. A test filter typed as `sub/depth` also matches on Windows now.
|
|
28
|
+
- `looop dev`, `looop test` and `looop publish` on Windows no longer fail with `Could not resolve "./entitiessaucer.js"` when a game has entity kinds: the generated room module now writes import paths with forward slashes whichever engine version produced them.
|
|
29
|
+
|
|
17
30
|
## [0.1.33] - 2026-08-27
|
|
18
31
|
|
|
19
32
|
### Added
|
package/lib/agent-surface.mjs
CHANGED
|
@@ -69,9 +69,13 @@ const sha = (buf) => createHash('sha256').update(buf).digest('hex');
|
|
|
69
69
|
|
|
70
70
|
// Where each artifact path lands in the game repo. `skills/**` is the whole
|
|
71
71
|
// surface today; AGENTS.md is spliced rather than written whole.
|
|
72
|
+
// Targets are manifest keys, not host paths: forward slashes on every platform,
|
|
73
|
+
// because they are recorded in .looop/agent-surface.json, compared with
|
|
74
|
+
// `startsWith(`${dest}/`)`, and reported to the creator. `join(projectDir, dest)`
|
|
75
|
+
// turns one into a host path wherever the filesystem is touched.
|
|
72
76
|
function target(rel) {
|
|
73
77
|
if (rel === 'AGENTS.md') return 'AGENTS.md';
|
|
74
|
-
if (rel.startsWith('skills/')) return
|
|
78
|
+
if (rel.startsWith('skills/')) return `.claude/${rel}`;
|
|
75
79
|
return null; // unknown surface entry from a newer engine — ignore, don't guess
|
|
76
80
|
}
|
|
77
81
|
|
|
@@ -128,7 +132,7 @@ function contentMatchesRecord(abs, dest, placed) {
|
|
|
128
132
|
if (!entries.length) return true; // an empty folder holds nothing to lose
|
|
129
133
|
return entries.every((e) => {
|
|
130
134
|
const file = join(e.parentPath, e.name);
|
|
131
|
-
const rel =
|
|
135
|
+
const rel = `${dest}/${relative(abs, file).split('\\').join('/')}`;
|
|
132
136
|
return placed[rel] !== undefined && sha(readFileSync(file)) === placed[rel];
|
|
133
137
|
});
|
|
134
138
|
}
|
package/lib/dev.mjs
CHANGED
|
@@ -146,6 +146,7 @@ export async function dev({ cwd = process.cwd(), port, noMp = process.env.NO_MP
|
|
|
146
146
|
slug: project.slug,
|
|
147
147
|
projectDir: project.dir, // answers /__looop/whoami — see ports.mjs
|
|
148
148
|
log, // agent-inbox report lines ride the dev logger
|
|
149
|
+
engineVersion: engine.version ?? null, // stamped on agent-inbox reports
|
|
149
150
|
mounts: [
|
|
150
151
|
{ url: `/games/${project.slug}/`, dir: project.dir },
|
|
151
152
|
{ url: '/shared/', dir: generatedOverridesDir },
|
package/lib/room-server.mjs
CHANGED
|
@@ -155,7 +155,19 @@ export function renderEntityComponents(components) {
|
|
|
155
155
|
// generated entry to wrap. Kept as one function so the kind-aliasing, the
|
|
156
156
|
// `./world.js` root convention, and the assemble→lower call can never drift
|
|
157
157
|
// between the dev and publish builds.
|
|
158
|
-
|
|
158
|
+
// A relative path with forward slashes whichever host produced it.
|
|
159
|
+
const posixPath = (p) => p.split('\\').join('/');
|
|
160
|
+
|
|
161
|
+
function renderV2Preamble(raw) {
|
|
162
|
+
// Kind modules are game-relative paths the engine relativized on the
|
|
163
|
+
// creator's machine. An engine whose framework build predates the
|
|
164
|
+
// forward-slash fix emits them with the host separator, so on Windows they
|
|
165
|
+
// arrive as `entities\saucer.js`. Dropped raw into a string literal, `\s` is
|
|
166
|
+
// an escape sequence and esbuild looks for `./entitiessaucer.js`. The CLI and
|
|
167
|
+
// the engine ship separately, so a fixed CLI on an unfixed engine is a real
|
|
168
|
+
// pairing; import specifiers and the URLs the client's kind loader builds
|
|
169
|
+
// from the embedded skeleton are always forward-slash, so normalize both.
|
|
170
|
+
const skeleton = { ...raw, kinds: raw.kinds.map((k) => ({ ...k, module: posixPath(k.module) })) };
|
|
159
171
|
const imports = skeleton.kinds.map(
|
|
160
172
|
(k, i) => `import { ${k.name} as K${i} } from './${k.module}';`,
|
|
161
173
|
);
|
package/lib/static-server.mjs
CHANGED
|
@@ -113,6 +113,11 @@ export function createStaticServer({
|
|
|
113
113
|
// import map + globalThis.__LOOOP_SKELETON__ so startGame() can boot. Null for
|
|
114
114
|
// a v1 game (no bare `looop` import, no skeleton).
|
|
115
115
|
skeleton = null,
|
|
116
|
+
// The engine version this server serves, stamped onto every agent-inbox
|
|
117
|
+
// file as `engine`. A report is read off disk later against whatever the
|
|
118
|
+
// game is pinned to THEN; the server is the only side that knows what it
|
|
119
|
+
// was when the report was made (the page has no version of its own).
|
|
120
|
+
engineVersion = null,
|
|
116
121
|
}) {
|
|
117
122
|
// The injected skeleton is LIVE, not frozen at startup: a v2 hot-reload
|
|
118
123
|
// rebuilds the room with a fresh skeleton, and `setSkeleton` below pushes that
|
|
@@ -304,6 +309,7 @@ export function createStaticServer({
|
|
|
304
309
|
mkdirSync(inboxDir, { recursive: true });
|
|
305
310
|
const selfIgnore = join(inboxDir, '.gitignore');
|
|
306
311
|
if (!existsSync(selfIgnore)) writeFileSync(selfIgnore, '*\n');
|
|
312
|
+
if (engineVersion) envelope.engine = engineVersion;
|
|
307
313
|
const d = new Date();
|
|
308
314
|
const p = (n, w = 2) => String(n).padStart(w, '0');
|
|
309
315
|
const stamp = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}-${p(d.getHours())}-${p(d.getMinutes())}-${p(d.getSeconds())}-${p(d.getMilliseconds(), 3)}`;
|
package/lib/test-cmd.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { spawn } from 'node:child_process';
|
|
11
11
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
|
-
import { join, relative, dirname } from 'node:path';
|
|
13
|
+
import nodePath, { join, relative, dirname } from 'node:path';
|
|
14
14
|
import { pathToFileURL } from 'node:url';
|
|
15
15
|
import { findProject, resolveEngine } from './project.mjs';
|
|
16
16
|
import { dev } from './dev.mjs';
|
|
@@ -19,16 +19,31 @@ import { portsFor, portInUse } from './ports.mjs';
|
|
|
19
19
|
|
|
20
20
|
const SKIP_DIRS = new Set(['node_modules', 'overrides']);
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Whether a game-relative path lies under a directory discovery must ignore:
|
|
23
|
+
// dependencies (the engine ships its own scaffold smokes under node_modules),
|
|
24
|
+
// engine-override copies, and dot-directories. `path.relative()` returns
|
|
25
|
+
// backslash-separated paths on Windows, so the split accepts both separators —
|
|
26
|
+
// splitting on '/' alone yields one giant segment there and skips nothing.
|
|
27
|
+
export function isSkippedPath(rel) {
|
|
28
|
+
return rel.split(/[\\/]/).some((p) => SKIP_DIRS.has(p) || p.startsWith('.'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The game-relative path with forward slashes on every platform, so a filter
|
|
32
|
+
// the creator types as `sub/depth` matches on Windows too. The path module is
|
|
33
|
+
// injectable so the Windows form can be produced on any host.
|
|
34
|
+
export const posixRelative = (dir, file, p = nodePath) => p.relative(dir, file).split('\\').join('/');
|
|
35
|
+
|
|
36
|
+
// The path module is injectable for the same reason: with `path.win32` the
|
|
37
|
+
// real readdir → join → relative → skip chain runs with Windows semantics
|
|
38
|
+
// against real files, on any host.
|
|
39
|
+
export function discoverTestFiles(dir, { path: p = nodePath } = {}) {
|
|
23
40
|
const unit = [];
|
|
24
41
|
const smokes = [];
|
|
25
42
|
const entries = readdirSync(dir, { withFileTypes: true, recursive: true });
|
|
26
43
|
for (const entry of entries) {
|
|
27
44
|
if (!entry.isFile()) continue;
|
|
28
|
-
const path = join(entry.parentPath, entry.name);
|
|
29
|
-
|
|
30
|
-
const parts = rel.split('/');
|
|
31
|
-
if (parts.some((p) => SKIP_DIRS.has(p) || p.startsWith('.'))) continue;
|
|
45
|
+
const path = p.join(entry.parentPath, entry.name);
|
|
46
|
+
if (isSkippedPath(p.relative(dir, path))) continue;
|
|
32
47
|
if (entry.name.endsWith('.test.mjs')) unit.push(path);
|
|
33
48
|
else if (entry.name.endsWith('.smoke.mjs')) smokes.push(path);
|
|
34
49
|
}
|
|
@@ -98,7 +113,7 @@ export async function testCmd({ cwd = process.cwd(), log = console.log, devFn =
|
|
|
98
113
|
// milestone close and before publish (qa.md T3). With no pattern, everything
|
|
99
114
|
// runs, exactly as before.
|
|
100
115
|
const scoped = patterns.length > 0;
|
|
101
|
-
const matches = (f) => patterns.some((p) =>
|
|
116
|
+
const matches = (f) => patterns.some((p) => posixRelative(project.dir, f).includes(p));
|
|
102
117
|
const unit = scoped ? all.unit.filter(matches) : all.unit;
|
|
103
118
|
const smokes = scoped ? all.smokes.filter(matches) : all.smokes;
|
|
104
119
|
|