@looop-games/cli 0.1.12 → 0.1.13

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 CHANGED
@@ -14,6 +14,24 @@ Versions: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.1.13] - 2026-07-12
18
+
19
+ ### Fixed
20
+ - **`looop update` deleted your engine.** The engine is not an npm package — it
21
+ installs without a record in `package.json`, so *any* `npm install` in your game
22
+ treats it as junk and removes it. `looop update` then updated the `looop` command
23
+ itself with an npm install — **after** putting the engine in place — and wiped it
24
+ out every time. The update said it succeeded; `node_modules/@looop-games/engine`
25
+ was gone. Everything that reads the engine then failed, and re-running
26
+ `looop update` cheerfully said "already up to date" and fixed nothing.
27
+
28
+ The engine is now the last thing installed, and an update that finds it missing
29
+ puts it back (from the local cache — instant, no download). If you hit this, one
30
+ `npx looop update` repairs it.
31
+ - `looop lint` said "this engine ships no rules yet" when the real problem was that
32
+ the engine was not installed at all — sending you to `looop update`, the one
33
+ command that could not help. It now tells you which of the two it is.
34
+
17
35
  ## [0.1.12] - 2026-07-12
18
36
 
19
37
  ### Added
package/lib/lint.mjs CHANGED
@@ -38,17 +38,23 @@ async function loadFromProject(projectDir) {
38
38
  }
39
39
 
40
40
  // The shareable config, out of the installed engine artifact.
41
+ //
42
+ // The two ways this can come up empty are DIFFERENT problems with different
43
+ // fixes, and conflating them sends people in a circle: the first version of this
44
+ // said "engine ships no rules — run looop update" when the engine was not
45
+ // installed at all, so `looop update` was the one command that could never help.
46
+ // Say which it is.
41
47
  async function loadRulesFromEngine(projectDir) {
42
48
  let engineDir;
43
49
  try {
44
50
  ({ dir: engineDir } = resolveEngine(projectDir));
45
51
  } catch {
46
- return null; // no engine installed yet — `looop dev` installs it
52
+ return { configs: null, why: 'no-engine' };
47
53
  }
48
54
  const entry = join(engineDir, 'eslint', 'index.js');
49
- if (!existsSync(entry)) return null; // engine predates the rules
55
+ if (!existsSync(entry)) return { configs: null, why: 'engine-too-old' };
50
56
  const mod = await import(pathToFileURL(entry).href);
51
- return mod.configs ?? mod.default?.configs ?? null;
57
+ return { configs: mod.configs ?? mod.default?.configs ?? null, why: 'ok' };
52
58
  }
53
59
 
54
60
  export async function lintCmd({
@@ -68,10 +74,19 @@ export async function lintCmd({
68
74
  return { ok: true, skipped: true, errorCount: 0, warningCount: 0 };
69
75
  }
70
76
 
71
- const configs = await loadRules(project.dir);
77
+ const { configs, why } = await loadRules(project.dir);
72
78
  if (!configs) {
73
- log('⚠️ looop lint: this engine ships no rules yet — skipping.');
74
- log(' Get the latest engine (it carries them): looop update');
79
+ if (why === 'no-engine') {
80
+ // The engine is not on disk. Usually it was PRUNED: it installs with
81
+ // `npm install --no-save`, so npm has no record of it and any later
82
+ // `npm install` in this game deletes it. It is not lost — it is cached.
83
+ log('⚠️ looop lint: the Looop engine is not installed in this game — skipping.');
84
+ log(' (A plain `npm install` removes it; it is cached, so putting it back is instant.)');
85
+ log(' Restore it with: looop update');
86
+ } else {
87
+ log('⚠️ looop lint: your engine is older than the Looop rules — skipping.');
88
+ log(' Get an engine that carries them: looop update');
89
+ }
75
90
  return { ok: true, skipped: true, errorCount: 0, warningCount: 0 };
76
91
  }
77
92
 
package/lib/update.mjs CHANGED
@@ -97,19 +97,52 @@ export async function update({
97
97
  .sort((a, b) => compareVersions(b.version, a.version))
98
98
  : null;
99
99
 
100
+ // ── The `looop` command FIRST, before the engine lands ─────────────────────
101
+ //
102
+ // Order is load-bearing, and getting it wrong deleted the engine. The engine is
103
+ // not an npm package — it installs with `npm install --no-save`, so npm has no
104
+ // record of it and treats it as extraneous. ANY later `npm install` in the game
105
+ // prunes it. syncCli runs exactly such an install (`--save-dev` the new CLI), so
106
+ // running it AFTER the engine install wiped the engine every single time: the
107
+ // update reported success and left node_modules/@looop-games/engine gone. The
108
+ // next `looop lint` then found no engine at all (caught on a real game).
109
+ //
110
+ // So: do every npm install that this command is going to do BEFORE the engine
111
+ // is put on disk, and let the engine be the last thing to land.
112
+ //
113
+ // Wrapped: syncCli is already fail-soft, and a bug in it must not stop the
114
+ // engine update that is the point of this command.
115
+ let cli;
116
+ try {
117
+ cli = await syncCliFn({ projectDir: project.dir, log });
118
+ } catch (err) {
119
+ cli = { updated: false, error: err };
120
+ }
121
+
100
122
  let engineDir = null;
101
123
  let updated = false;
102
124
 
103
125
  if (from === latest) {
104
- log(`✅ Engine ${latest} — already up to date.`);
105
126
  // Reconcile anyway. A repo can sit on the latest engine and STILL have an
106
127
  // out-of-date surface: one scaffolded before this mechanism existed has
107
128
  // never had its skills adopted, and would otherwise wait forever for a
108
129
  // release it already has.
109
130
  try {
110
131
  engineDir = resolveEngine(project.dir).dir;
132
+ log(`✅ Engine ${latest} — already up to date.`);
111
133
  } catch {
112
- engineDir = null; // engine not installed yet the next `dev` fetches it
134
+ // Pinned to the latest, but NOT on disk. The pin is a claim about what this
135
+ // game runs; node_modules is the truth, and they disagree — because a plain
136
+ // `npm install` (the creator's own, or ours above) prunes the engine, which
137
+ // npm never recorded. "Already up to date" while the engine is missing is a
138
+ // lie that leaves every engine-reading command broken, and re-running update
139
+ // could never fix it. Put it back — from the local cache, so this is fast and
140
+ // works offline.
141
+ log(`Engine ${latest} is pinned but missing from node_modules — reinstalling it.`);
142
+ const engine = await ensure(project.dir, { apiBase, log, fetchImpl });
143
+ engineDir = engine.dir ?? null;
144
+ log('');
145
+ log(`✅ Engine ${latest} — restored.`);
113
146
  }
114
147
  } else {
115
148
  // Rewrite the pin first; ensureEngine honors it (download → install → pin).
@@ -125,21 +158,18 @@ export async function update({
125
158
  if (!surface.skipped) report(log, surface.engineVersion ?? latest, surface);
126
159
 
127
160
  // The third lane: the `looop` command itself (see self-update.mjs). The skills
128
- // we just reconciled ship WITH the engine and can name any command they like —
129
- // riven ended up on an engine whose skills say `looop changelog` while its CLI
130
- // was four versions too old to have it. So update moves this too.
161
+ // we reconciled ship WITH the engine and can name any command they like — a real
162
+ // game ended up on an engine whose skills say `looop changelog` while its CLI was
163
+ // four versions too old to have it. So update moves this too.
131
164
  //
132
- // Wrapped: syncCli is already fail-soft, but a bug in it must not undo an
133
- // engine update that has already landed on disk.
134
- let cli;
135
- try {
136
- cli = await syncCliFn({ projectDir: project.dir, log });
137
- reportCli(log, cli);
138
- } catch (err) {
139
- cli = { updated: false, error: err };
165
+ // It already RAN, at the top: its npm install has to happen before the engine
166
+ // lands or it prunes it. This is only the report.
167
+ if (cli?.error) {
140
168
  log('');
141
- log(` The looop command could not be updated (${err.message}).`);
169
+ log(` The looop command could not be updated (${cli.error.message}).`);
142
170
  log(' Your engine and skills are up to date. Retry with: npm update @looop-games/cli');
171
+ } else {
172
+ reportCli(log, cli);
143
173
  }
144
174
 
145
175
  if (updated) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@looop-games/cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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",