@looop-games/cli 0.1.4 → 0.1.5
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/looop.mjs +1 -1
- package/lib/bundle-primitives.mjs +2 -2
- package/lib/create.mjs +1 -1
- package/lib/dev.mjs +3 -5
- package/lib/feedback.mjs +1 -1
- package/lib/inject.mjs +1 -2
- package/lib/llm-shim.mjs +1 -1
- package/lib/login.mjs +1 -2
- package/lib/ports.mjs +1 -1
- package/lib/publish.mjs +6 -7
- package/lib/static-server.mjs +2 -3
- package/lib/test-cmd.mjs +3 -4
- package/lib/update.mjs +2 -2
- package/package.json +3 -2
- package/lib/bundle-primitives.test.mjs +0 -95
- package/lib/config.test.mjs +0 -37
- package/lib/create.test.mjs +0 -433
- package/lib/dev.test.mjs +0 -52
- package/lib/engine.test.mjs +0 -176
- package/lib/feedback.test.mjs +0 -144
- package/lib/inject.test.mjs +0 -87
- package/lib/llm-shim.test.mjs +0 -90
- package/lib/login.test.mjs +0 -89
- package/lib/npm.test.mjs +0 -61
- package/lib/project.test.mjs +0 -88
- package/lib/publish.test.mjs +0 -148
- package/lib/static-server.test.mjs +0 -161
- package/lib/test-cmd.test.mjs +0 -118
- package/lib/update.test.mjs +0 -98
package/bin/looop.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// looop — the Looop game-development CLI
|
|
2
|
+
// looop — the Looop game-development CLI.
|
|
3
3
|
//
|
|
4
4
|
// Runs inside a standalone game folder (its own repo). The agent is the
|
|
5
5
|
// primary user: keep output plain, actionable, and machine-legible.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Bundle a server-backed game's primitives on the CREATOR's machine, with the
|
|
2
|
+
// engine marked external.
|
|
3
3
|
//
|
|
4
4
|
// The publish endpoint is a Pages Function — a Worker with no filesystem and no
|
|
5
5
|
// npm — so it cannot run a bundler. But real server primitives import npm
|
package/lib/create.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `looop create <name>` — bootstrap a standalone Looop game
|
|
1
|
+
// `looop create <name>` — bootstrap a standalone Looop game.
|
|
2
2
|
//
|
|
3
3
|
// The create-next-app gesture: one command → a complete folder that is its
|
|
4
4
|
// own repo, where `looop dev` immediately serves a WORKING multiplayer game
|
package/lib/dev.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// `looop dev` — the standalone three-service dev stack
|
|
2
|
-
// cuqfzo), same shape the monorepo's dev.sh proved:
|
|
1
|
+
// `looop dev` — the standalone three-service dev stack:
|
|
3
2
|
//
|
|
4
3
|
// static :8000 game folder at /games/<slug>/, /shared/ → engine bundle,
|
|
5
4
|
// head injection + auto-reload
|
|
@@ -37,12 +36,11 @@ export function partykitBin() {
|
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
// Per-game server parity
|
|
39
|
+
// Per-game server parity: a game shipping its own
|
|
41
40
|
// `partykit.json` runs its OWN server in dev — partykit gets the game folder
|
|
42
41
|
// as cwd, resolving the game's `main` entry (which imports the room server
|
|
43
42
|
// from the installed engine). Without one, the bundle's shared room server
|
|
44
|
-
// runs, exactly as before.
|
|
45
|
-
// vendor-flipped game behaves identically standalone. A config whose `main`
|
|
43
|
+
// runs, exactly as before. A config whose `main`
|
|
46
44
|
// doesn't exist is ignored LOUDLY — a dead file must not take dev's
|
|
47
45
|
// multiplayer down with it.
|
|
48
46
|
export function partykitCwdFor(project, engine, warn = console.warn) {
|
package/lib/feedback.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `looop feedback` — send the reports under notes/feedback/ to the Looop team
|
|
2
|
-
// (
|
|
2
|
+
// (the transport behind the template's /feedback skill).
|
|
3
3
|
//
|
|
4
4
|
// Target: /api/creator/feedback — the token-authenticated intake beside
|
|
5
5
|
// creator/publish. Each unsent notes/feedback/*.md ships VERBATIM as the
|
package/lib/inject.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// HTML/JS dev-serving transforms
|
|
2
|
-
// looop-core tools/game/dev_server.py, kept behaviour-identical:
|
|
1
|
+
// HTML/JS dev-serving transforms:
|
|
3
2
|
//
|
|
4
3
|
// * Head injection mirrors the production /g/<slug> serve path
|
|
5
4
|
// (builder/functions/g/[[path]].ts): window.GAME_SLUG + window.LOOOP_IDENTITY
|
package/lib/llm-shim.mjs
CHANGED
package/lib/login.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// `looop login` — device-flow authentication (
|
|
2
|
-
// Q2/Q6, the `gh auth login` shape):
|
|
1
|
+
// `looop login` — device-flow authentication (the `gh auth login` shape):
|
|
3
2
|
//
|
|
4
3
|
// 1. ask the platform for a pairing (device_code for us, user_code for the
|
|
5
4
|
// human),
|
package/lib/ports.mjs
CHANGED
package/lib/publish.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// `looop publish` — ship the game to play.looop.games
|
|
1
|
+
// `looop publish` — ship the game to play.looop.games.
|
|
2
2
|
//
|
|
3
3
|
// Server-resolved: we send ONLY the game's own files plus the engine version
|
|
4
4
|
// the installed bundle declares; the platform resolves /shared/... from its
|
|
5
|
-
// release registry.
|
|
6
|
-
//
|
|
5
|
+
// release registry. The client never stages the shared library itself — that
|
|
6
|
+
// trust hole is what this design replaces.
|
|
7
7
|
//
|
|
8
8
|
// Target: /api/creator/publish — the token-authenticated creator lane, the
|
|
9
9
|
// one publish route reachable on the public play host (the legacy
|
|
@@ -21,9 +21,8 @@ import { bundlePrimitives, PRIMITIVES_ENTRY } from './bundle-primitives.mjs';
|
|
|
21
21
|
export const PLAY_BASE = 'https://play.looop.games';
|
|
22
22
|
|
|
23
23
|
// Never shipped: tooling, VCS, agent workspace, notes + handbook + agent
|
|
24
|
-
// instructions (repo knowledge travels with the repo, not the catalog —
|
|
25
|
-
//
|
|
26
|
-
// public URL), tests/smokes.
|
|
24
|
+
// instructions (repo knowledge travels with the repo, not the catalog — a
|
|
25
|
+
// published game must not expose it on a public URL), tests/smokes.
|
|
27
26
|
const SKIP_DIRS = new Set(['node_modules', 'notes', 'handbook', '.git', '.looop', '.claude', '__pycache__']);
|
|
28
27
|
const SKIP_FILES = [
|
|
29
28
|
/^package(-lock)?\.json$/,
|
|
@@ -69,7 +68,7 @@ export async function publish({
|
|
|
69
68
|
throw new Error('this game has no index.html at its root — publish needs an entry page.');
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
// Server-backed game
|
|
71
|
+
// Server-backed game: its primitives can
|
|
73
72
|
// import npm packages + WASM the platform's bundler-less endpoint can't
|
|
74
73
|
// resolve, so we bundle them HERE (engine marked external) and send the
|
|
75
74
|
// single self-contained module in place of the raw barrel. The endpoint
|
package/lib/static-server.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// The standalone dev static server —
|
|
2
|
-
//
|
|
3
|
-
// bundle behind the same URL shape production uses:
|
|
1
|
+
// The standalone dev static server — serves a single game + the installed
|
|
2
|
+
// engine bundle behind the same URL shape production uses:
|
|
4
3
|
//
|
|
5
4
|
// /games/<slug>/… → the game folder
|
|
6
5
|
// /shared/… → the engine bundle's shared/ tree
|
package/lib/test-cmd.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// `looop test` — run the game's own verification gate
|
|
2
|
-
// Slice 1; the successor to the monorepo's `gamedev <name> test`).
|
|
1
|
+
// `looop test` — run the game's own verification gate.
|
|
3
2
|
//
|
|
4
3
|
// Discovers the game's test files — `*.test.mjs` (unit, run under
|
|
5
4
|
// `node --test`) and `*.smoke.mjs` (integration, run against a REAL dev
|
|
@@ -115,8 +114,8 @@ export async function testCmd({ cwd = process.cwd(), log = console.log, devFn =
|
|
|
115
114
|
env: {
|
|
116
115
|
...env,
|
|
117
116
|
LOOOP_TEST_GAME_URL: handle.url,
|
|
118
|
-
//
|
|
119
|
-
//
|
|
117
|
+
// A legacy alias for the same URL, exported so smokes written
|
|
118
|
+
// against the older convention keep running unedited.
|
|
120
119
|
GAMEDEV_TEST_GAME_URL: handle.url,
|
|
121
120
|
URL: handle.url,
|
|
122
121
|
},
|
package/lib/update.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// `looop update` — move this game to the latest engine release
|
|
2
|
-
//
|
|
1
|
+
// `looop update` — move this game to the latest engine release.
|
|
2
|
+
// Asks the platform registry
|
|
3
3
|
// for the latest downloadable release, rewrites the `looop.engine` pin, and
|
|
4
4
|
// lets ensureEngine do what it already does on every dev/publish: download
|
|
5
5
|
// (cached), install, re-pin. Slice 2 extends this with the skills/agent-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@looop-games/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Looop game development CLI — dev server, login, and publishing for standalone Looop games.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
12
12
|
"lib",
|
|
13
|
-
"template"
|
|
13
|
+
"template",
|
|
14
|
+
"!lib/**/*.test.mjs"
|
|
14
15
|
],
|
|
15
16
|
"engines": {
|
|
16
17
|
"node": ">=20.11"
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
// cuqfzo Phase 2 / Option B — Slice 2: the CLI "bundle primitives, engine
|
|
2
|
-
// external" step. A creator's server primitives can import npm packages, WASM,
|
|
3
|
-
// and sibling game files; the publish endpoint (a Pages Function) has no
|
|
4
|
-
// bundler, so the CLI bundles them into ONE self-contained module here. The
|
|
5
|
-
// engine surface a primitive imports (`/shared/ui/room/...`, or the engine
|
|
6
|
-
// package) is marked EXTERNAL and rewritten to `./rooms-runtime.js` — the
|
|
7
|
-
// pre-bundled engine artifact the endpoint uploads alongside; bundling it in
|
|
8
|
-
// would duplicate the room runtime and bypass the cost boundary.
|
|
9
|
-
import { test } from 'node:test';
|
|
10
|
-
import assert from 'node:assert/strict';
|
|
11
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
12
|
-
import { tmpdir } from 'node:os';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
-
import { bundlePrimitives } from './bundle-primitives.mjs';
|
|
15
|
-
|
|
16
|
-
// A fixture game whose primitives exercise all three seams: an npm dep, a WASM
|
|
17
|
-
// module, and an engine import. Returns the project dir (caller cleans up).
|
|
18
|
-
function fixture() {
|
|
19
|
-
const dir = mkdtempSync(join(tmpdir(), 'looop-bundle-'));
|
|
20
|
-
// A fake installed npm package (proves node_modules resolution + inlining).
|
|
21
|
-
mkdirSync(join(dir, 'node_modules/tiny-dep'), { recursive: true });
|
|
22
|
-
writeFileSync(join(dir, 'node_modules/tiny-dep/package.json'), JSON.stringify({ name: 'tiny-dep', version: '1.0.0', main: 'index.js', type: 'module' }));
|
|
23
|
-
writeFileSync(join(dir, 'node_modules/tiny-dep/index.js'), 'export const DEP_MARKER = 41;\n');
|
|
24
|
-
// A tiny WASM blob (bytes only — the bundler embeds it; validity is the
|
|
25
|
-
// smoke's job). The magic header makes it a recognizable, real .wasm file.
|
|
26
|
-
mkdirSync(join(dir, '_local/primitives'), { recursive: true });
|
|
27
|
-
writeFileSync(join(dir, '_local/primitives/mod.wasm'), Buffer.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]));
|
|
28
|
-
// The primitives barrel: engine import (external), npm import (inline),
|
|
29
|
-
// sibling wasm import (inline).
|
|
30
|
-
writeFileSync(
|
|
31
|
-
join(dir, '_local/primitives/index.js'),
|
|
32
|
-
[
|
|
33
|
-
"import { runEffect } from '/shared/ui/room/primitives/effects.js';",
|
|
34
|
-
"import { DEP_MARKER } from 'tiny-dep';",
|
|
35
|
-
"import wasmBytes from './mod.wasm';",
|
|
36
|
-
'export const extraPrimitives = {',
|
|
37
|
-
" demo: class { constructor() { this.v = DEP_MARKER; this.w = wasmBytes; this.e = runEffect; } },",
|
|
38
|
-
'};',
|
|
39
|
-
].join('\n'),
|
|
40
|
-
);
|
|
41
|
-
return dir;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
test('bundles npm deps and WASM INLINE, and leaves the engine import external as ./rooms-runtime.js', async () => {
|
|
45
|
-
const dir = fixture();
|
|
46
|
-
try {
|
|
47
|
-
const { source } = await bundlePrimitives(dir);
|
|
48
|
-
assert.equal(typeof source, 'string');
|
|
49
|
-
|
|
50
|
-
// npm dep inlined — its value is present, its bare specifier is gone.
|
|
51
|
-
assert.match(source, /41/, 'the npm dep value should be inlined');
|
|
52
|
-
assert.doesNotMatch(source, /from\s*["']tiny-dep["']/, 'the npm dep must not remain an external import');
|
|
53
|
-
|
|
54
|
-
// WASM inlined — no leftover ".wasm" import; the magic bytes appear (base64
|
|
55
|
-
// "AGFzbQ" is the standard encoding of \0asm\1\0\0\0).
|
|
56
|
-
assert.doesNotMatch(source, /\.wasm["']/, 'the wasm import must be inlined, not external');
|
|
57
|
-
assert.match(source, /AGFzbQ/, 'the wasm bytes should be embedded (base64)');
|
|
58
|
-
|
|
59
|
-
// Engine import is EXTERNAL and rewritten to the artifact module.
|
|
60
|
-
assert.match(source, /from\s*["']\.\/rooms-runtime\.js["']/, 'engine imports must be external → ./rooms-runtime.js');
|
|
61
|
-
assert.match(source, /runEffect/, 'the imported engine symbol is preserved');
|
|
62
|
-
// The original /shared/... specifier is gone (rewritten).
|
|
63
|
-
assert.doesNotMatch(source, /\/shared\/ui\/room/, 'the /shared/... specifier must be rewritten away');
|
|
64
|
-
} finally {
|
|
65
|
-
rmSync(dir, { recursive: true, force: true });
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('the ONLY external import in the bundle is the engine artifact (everything else is self-contained)', async () => {
|
|
70
|
-
const dir = fixture();
|
|
71
|
-
try {
|
|
72
|
-
const { source, externals } = await bundlePrimitives(dir);
|
|
73
|
-
// Every `from "..."` in the output must point at the artifact (or a
|
|
74
|
-
// cloudflare: builtin) — nothing else may be left unresolved.
|
|
75
|
-
const froms = [...source.matchAll(/\bfrom\s*["']([^"']+)["']/g)].map((m) => m[1]);
|
|
76
|
-
for (const spec of froms) {
|
|
77
|
-
assert.ok(
|
|
78
|
-
spec === './rooms-runtime.js' || spec.startsWith('cloudflare:'),
|
|
79
|
-
`unexpected external in the primitives bundle: ${spec}`,
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
assert.deepEqual([...new Set(externals)].sort(), ['/shared/ui/room/primitives/effects.js']);
|
|
83
|
-
} finally {
|
|
84
|
-
rmSync(dir, { recursive: true, force: true });
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('throws a clear error when the primitives barrel is missing (not a server-backed game)', async () => {
|
|
89
|
-
const dir = mkdtempSync(join(tmpdir(), 'looop-bundle-empty-'));
|
|
90
|
-
try {
|
|
91
|
-
await assert.rejects(() => bundlePrimitives(dir), /primitives/i);
|
|
92
|
-
} finally {
|
|
93
|
-
rmSync(dir, { recursive: true, force: true });
|
|
94
|
-
}
|
|
95
|
-
});
|
package/lib/config.test.mjs
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// CLI config lives in ~/.looop/config.json (LOOOP_HOME overrides for tests).
|
|
2
|
-
// It stores the creator token minted by `looop login` — the secret never
|
|
3
|
-
// passes through the human's hands (device flow, decision Q2/Q6 in cuqfzo).
|
|
4
|
-
import { test } from 'node:test';
|
|
5
|
-
import assert from 'node:assert/strict';
|
|
6
|
-
import { mkdtempSync, rmSync, readFileSync, statSync } from 'node:fs';
|
|
7
|
-
import { tmpdir } from 'node:os';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { readConfig, writeConfig, getToken, setToken, clearToken, configPath } from './config.mjs';
|
|
10
|
-
|
|
11
|
-
const home = mkdtempSync(join(tmpdir(), 'looop-home-'));
|
|
12
|
-
process.env.LOOOP_HOME = home;
|
|
13
|
-
|
|
14
|
-
test('empty config reads as {}', () => {
|
|
15
|
-
assert.deepEqual(readConfig(), {});
|
|
16
|
-
assert.equal(getToken(), null);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('setToken persists and getToken reads it back', () => {
|
|
20
|
-
setToken('looop_abc123', { apiBase: 'https://play.looop.games' });
|
|
21
|
-
assert.equal(getToken(), 'looop_abc123');
|
|
22
|
-
const onDisk = JSON.parse(readFileSync(configPath(), 'utf8'));
|
|
23
|
-
assert.equal(onDisk.token, 'looop_abc123');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('token file is not world-readable', () => {
|
|
27
|
-
const mode = statSync(configPath()).mode & 0o777;
|
|
28
|
-
assert.equal(mode & 0o077, 0, `mode ${mode.toString(8)} leaks to group/other`);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('clearToken removes the token but keeps other config', () => {
|
|
32
|
-
writeConfig({ ...readConfig(), custom: 'x' });
|
|
33
|
-
clearToken();
|
|
34
|
-
assert.equal(getToken(), null);
|
|
35
|
-
assert.equal(readConfig().custom, 'x');
|
|
36
|
-
rmSync(home, { recursive: true, force: true });
|
|
37
|
-
});
|