@codyswann/lisa 2.178.5 → 2.179.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/dist/configs/eslint/phaser.d.ts +2 -8
- package/dist/configs/eslint/phaser.d.ts.map +1 -1
- package/dist/configs/eslint/phaser.js +143 -17
- package/dist/configs/eslint/phaser.js.map +1 -1
- package/eslint-plugin-phaser/README.md +19 -0
- package/eslint-plugin-phaser/index.js +35 -0
- package/eslint-plugin-phaser/package.json +10 -0
- package/eslint-plugin-phaser/rules/no-allocation-in-update.js +189 -0
- package/eslint-plugin-phaser/rules/no-create-in-update.js +200 -0
- package/eslint-plugin-phaser/rules/require-shutdown-cleanup.js +191 -0
- package/package.json +5 -3
- package/phaser/package-lisa/package.lisa.json +11 -3
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/skills/analyze-claude-remote/SKILL.md +72 -2
- package/plugins/lisa/skills/generate-claude-remote-build-script/SKILL.md +30 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/analyze-claude-remote/SKILL.md +72 -2
- package/plugins/lisa-agy/skills/generate-claude-remote-build-script/SKILL.md +30 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/analyze-claude-remote/SKILL.md +72 -2
- package/plugins/lisa-copilot/skills/generate-claude-remote-build-script/SKILL.md +30 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/analyze-claude-remote/SKILL.md +72 -2
- package/plugins/lisa-cursor/skills/generate-claude-remote-build-script/SKILL.md +30 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/agents/ops-specialist.md +2 -2
- package/plugins/lisa-expo/skills/ops-check-logs/SKILL.md +8 -2
- package/plugins/lisa-expo/skills/ops-db-ops/SKILL.md +7 -3
- package/plugins/lisa-expo/skills/ops-deploy/SKILL.md +6 -2
- package/plugins/lisa-expo/skills/ops-run-local/SKILL.md +5 -2
- package/plugins/lisa-expo-agy/agents/ops-specialist.md +2 -2
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-agy/skills/ops-check-logs/SKILL.md +8 -2
- package/plugins/lisa-expo-agy/skills/ops-db-ops/SKILL.md +7 -3
- package/plugins/lisa-expo-agy/skills/ops-deploy/SKILL.md +6 -2
- package/plugins/lisa-expo-agy/skills/ops-run-local/SKILL.md +5 -2
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/agents/ops-specialist.agent.md +2 -2
- package/plugins/lisa-expo-copilot/skills/ops-check-logs/SKILL.md +8 -2
- package/plugins/lisa-expo-copilot/skills/ops-db-ops/SKILL.md +7 -3
- package/plugins/lisa-expo-copilot/skills/ops-deploy/SKILL.md +6 -2
- package/plugins/lisa-expo-copilot/skills/ops-run-local/SKILL.md +5 -2
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/agents/ops-specialist.md +2 -2
- package/plugins/lisa-expo-cursor/skills/ops-check-logs/SKILL.md +8 -2
- package/plugins/lisa-expo-cursor/skills/ops-db-ops/SKILL.md +7 -3
- package/plugins/lisa-expo-cursor/skills/ops-deploy/SKILL.md +6 -2
- package/plugins/lisa-expo-cursor/skills/ops-run-local/SKILL.md +5 -2
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/rules/phaser.md +111 -44
- package/plugins/lisa-phaser/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser/skills/phaser-accessibility/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/lisa-phaser/skills/phaser-asset-pipeline/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/lisa-phaser/skills/phaser-build-deploy/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser/skills/phaser-i18n/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser/skills/phaser-services/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser-agy/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/lisa-phaser-agy/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/lisa-phaser-agy/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser-agy/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser-agy/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/rules/phaser.md +111 -44
- package/plugins/lisa-phaser-copilot/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/rules/phaser.mdc +111 -44
- package/plugins/lisa-phaser-cursor/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/agents/ops-specialist.md +1 -1
- package/plugins/lisa-rails-agy/agents/ops-specialist.md +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/agents/ops-specialist.agent.md +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/agents/ops-specialist.md +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/analyze-claude-remote/SKILL.md +72 -2
- package/plugins/src/base/skills/generate-claude-remote-build-script/SKILL.md +30 -1
- package/plugins/src/expo/agents/ops-specialist.md +2 -2
- package/plugins/src/expo/skills/ops-check-logs/SKILL.md +8 -2
- package/plugins/src/expo/skills/ops-db-ops/SKILL.md +7 -3
- package/plugins/src/expo/skills/ops-deploy/SKILL.md +6 -2
- package/plugins/src/expo/skills/ops-run-local/SKILL.md +5 -2
- package/plugins/src/phaser/rules/phaser.md +111 -44
- package/plugins/src/phaser/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/src/phaser/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/src/phaser/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/src/phaser/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/src/phaser/skills/phaser-services/SKILL.md +208 -0
- package/plugins/src/phaser/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/src/rails/agents/ops-specialist.md +1 -1
- package/tsconfig/phaser.json +8 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: phaser-services
|
|
3
|
+
description: This skill should be used when wiring cross-cutting services and shared state in a Phaser 4 game — the typed registry wrapper for global state, a single dedicated EventsCenter bus (never game.events), the .on()/.off() listener discipline enforced in scene shutdown, SoundService (mobile audio unlock on first gesture), InputService (semantic actions instead of raw keys), and SaveService (versioned localStorage with a migration chain). Use it when adding global state, an event bus, audio/input/save plumbing, or fixing listener leaks and lost saves. Pairs with phaser-scenes, phaser-testing, and phaser-i18n.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Phaser 4 Services and Shared State
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Cross-cutting concerns — global state, app events, sound, input, save — live in
|
|
11
|
+
`src/services/**` as thin, typed singletons, **not** scattered through scenes.
|
|
12
|
+
Two rules anchor everything: state flows through a typed registry wrapper, and
|
|
13
|
+
app messaging flows through **one dedicated EventsCenter** that is never
|
|
14
|
+
`game.events`. Direct `localStorage` access outside `src/services/**` is
|
|
15
|
+
lint-banned — persistence goes through SaveService.
|
|
16
|
+
|
|
17
|
+
## Global state: a typed wrapper over the registry
|
|
18
|
+
|
|
19
|
+
`this.registry` is a game-wide key/value `DataManager` shared across scenes. Raw
|
|
20
|
+
access is stringly-typed and untestable, so wrap it once:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// src/services/state.ts
|
|
24
|
+
interface GameState { coins: number; level: number; muted: boolean; seed: number; }
|
|
25
|
+
const DEFAULTS: GameState = { coins: 0, level: 1, muted: false, seed: 0 };
|
|
26
|
+
|
|
27
|
+
export class GameStore {
|
|
28
|
+
constructor(private reg: Phaser.Data.DataManager) {
|
|
29
|
+
for (const k in DEFAULTS) if (!reg.has(k)) reg.set(k, (DEFAULTS as any)[k]);
|
|
30
|
+
}
|
|
31
|
+
get<K extends keyof GameState>(k: K): GameState[K] { return this.reg.get(k); }
|
|
32
|
+
set<K extends keyof GameState>(k: K, v: GameState[K]) { this.reg.set(k, v); }
|
|
33
|
+
onChange<K extends keyof GameState>(k: K, fn: (v: GameState[K]) => void) {
|
|
34
|
+
const h = (_p: unknown, key: string, v: GameState[K]) => { if (key === k) fn(v); };
|
|
35
|
+
this.reg.events.on(Phaser.Data.Events.CHANGE_DATA, h);
|
|
36
|
+
return () => this.reg.events.off(Phaser.Data.Events.CHANGE_DATA, h); // caller .off()s in shutdown
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Construct it once (in Boot) and read it via the registry. Keys are typed by
|
|
42
|
+
`GameState`, so a typo is a compile error. The registry holds *small* shared
|
|
43
|
+
state (settings, run seed, currency) — not GameObjects and not per-frame data.
|
|
44
|
+
|
|
45
|
+
## The EventsCenter bus (never `game.events`)
|
|
46
|
+
|
|
47
|
+
App-level messaging ("enemy-died", "score-changed", "level-cleared") uses a
|
|
48
|
+
single dedicated emitter. Reusing `game.events` (the engine's own bus) is
|
|
49
|
+
lint-banned — it mixes your events with engine lifecycle events and is
|
|
50
|
+
impossible to fully tear down.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// src/services/event-center.ts
|
|
54
|
+
export const EventCenter = new Phaser.Events.EventEmitter();
|
|
55
|
+
|
|
56
|
+
// typed key constants come from src/assets.ts (generated) — no raw strings
|
|
57
|
+
import { GameEvent } from "../assets";
|
|
58
|
+
EventCenter.emit(GameEvent.ScoreChanged, score);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Listener discipline: every `.on()` has a matching `.off()`
|
|
62
|
+
|
|
63
|
+
This is the leak rule the `require-shutdown-cleanup` lint rule and the testing
|
|
64
|
+
leak gate ([[phaser-testing]]) enforce. **Any** external listener — `EventCenter`,
|
|
65
|
+
`this.input`, `this.scale`, `this.time`, `window`, the registry — must be removed
|
|
66
|
+
in the scene's `shutdown`. Listeners on the scene's own display objects die with
|
|
67
|
+
the scene; external ones do not, and they fire again (doubled) after a restart.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
export class Game extends Phaser.Scene {
|
|
71
|
+
private cleanups: Array<() => void> = [];
|
|
72
|
+
|
|
73
|
+
create() {
|
|
74
|
+
const onResize = (s: Phaser.Structs.Size) => this.layout(s.width, s.height);
|
|
75
|
+
this.scale.on(Phaser.Scale.Events.RESIZE, onResize);
|
|
76
|
+
this.cleanups.push(() => this.scale.off(Phaser.Scale.Events.RESIZE, onResize));
|
|
77
|
+
|
|
78
|
+
const onScore = (n: number) => this.hud.setScore(n);
|
|
79
|
+
EventCenter.on(GameEvent.ScoreChanged, onScore);
|
|
80
|
+
this.cleanups.push(() => EventCenter.off(GameEvent.ScoreChanged, onScore));
|
|
81
|
+
|
|
82
|
+
this.events.once(Phaser.Scenes.Events.SHUTDOWN, this.shutdown, this);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
shutdown() { this.cleanups.forEach(fn => fn()); this.cleanups.length = 0; }
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Keeping the matching `.off()` next to each `.on()` (a `cleanups` array, or named
|
|
90
|
+
handler methods you remove in `shutdown`) is the pattern that survives the leak
|
|
91
|
+
gate. Prefer `.once()` where a listener should fire only once.
|
|
92
|
+
|
|
93
|
+
## SoundService — audio unlock on first gesture
|
|
94
|
+
|
|
95
|
+
Web Audio starts **suspended** until a user gesture; sound played before then is
|
|
96
|
+
silently dropped ("works on my machine, silent on the phone"). Centralize the
|
|
97
|
+
unlock and all playback:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// src/services/sound-service.ts
|
|
101
|
+
export class SoundService {
|
|
102
|
+
private unlocked = false;
|
|
103
|
+
constructor(private sound: Phaser.Sound.BaseSoundManager, private store: GameStore) {}
|
|
104
|
+
|
|
105
|
+
attachUnlock(scene: Phaser.Scene) {
|
|
106
|
+
if (this.unlocked) return;
|
|
107
|
+
scene.input.once(Phaser.Input.Events.POINTER_DOWN, () => this.resume());
|
|
108
|
+
// Phaser also fires its own unlock; resume() is idempotent
|
|
109
|
+
this.sound.once(Phaser.Sound.Events.UNLOCKED, () => this.resume());
|
|
110
|
+
}
|
|
111
|
+
private resume() { if ((this.sound as any).context?.state === "suspended") (this.sound as any).context.resume(); this.unlocked = true; }
|
|
112
|
+
play(key: string, cfg?: Phaser.Types.Sound.SoundConfig) { if (!this.store.get("muted")) this.sound.play(key, cfg); }
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Scenes call `soundService.play(Sfx.Jump)` — never `this.sound.play("jump")`.
|
|
117
|
+
Sound keys are generated constants ([[phaser-asset-pipeline]]).
|
|
118
|
+
|
|
119
|
+
## InputService — semantic actions, not raw keys
|
|
120
|
+
|
|
121
|
+
Scenes should ask "is *jump* down?", not "is the spacebar down?". A semantic
|
|
122
|
+
layer makes rebinding, gamepads, and touch swap in without touching game code,
|
|
123
|
+
and keeps replays deterministic (record actions, not hardware).
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// src/services/input-service.ts
|
|
127
|
+
export type Action = "left" | "right" | "jump" | "fire" | "pause";
|
|
128
|
+
|
|
129
|
+
export class InputService {
|
|
130
|
+
private down = new Set<Action>();
|
|
131
|
+
private bindings: Record<number, Action> = {};
|
|
132
|
+
constructor(kb: Phaser.Input.Keyboard.KeyboardPlugin) {
|
|
133
|
+
this.bindings[Phaser.Input.Keyboard.KeyCodes.LEFT] = "left";
|
|
134
|
+
this.bindings[Phaser.Input.Keyboard.KeyCodes.SPACE] = "jump";
|
|
135
|
+
kb.on("keydown", (e: KeyboardEvent) => { const a = this.bindings[e.keyCode]; if (a) this.down.add(a); });
|
|
136
|
+
kb.on("keyup", (e: KeyboardEvent) => { const a = this.bindings[e.keyCode]; if (a) this.down.delete(a); });
|
|
137
|
+
}
|
|
138
|
+
isDown(a: Action) { return this.down.has(a); }
|
|
139
|
+
snapshot(): readonly Action[] { return [...this.down]; } // for replay capture
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`update()` reads `input.isDown("jump")` and forwards the action set to pure logic
|
|
144
|
+
in `src/logic/**`. (Register/remove the keyboard listeners with the same on/off
|
|
145
|
+
discipline above.)
|
|
146
|
+
|
|
147
|
+
## SaveService — versioned localStorage with a migration chain
|
|
148
|
+
|
|
149
|
+
Raw `localStorage` outside `src/services/**` is lint-banned. Saves carry a schema
|
|
150
|
+
`VERSION`; on load, unknown-but-older saves run forward through a migration chain,
|
|
151
|
+
so an old player's data is never silently lost or crash-loaded.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// src/services/save-service.ts
|
|
155
|
+
const KEY = "save:v"; const VERSION = 3;
|
|
156
|
+
interface SaveV3 { version: 3; coins: number; unlocked: string[]; settings: { muted: boolean } }
|
|
157
|
+
|
|
158
|
+
const migrations: Record<number, (s: any) => any> = {
|
|
159
|
+
1: s => ({ ...s, unlocked: [], version: 2 }), // v1 -> v2
|
|
160
|
+
2: s => ({ ...s, settings: { muted: false }, version: 3 }), // v2 -> v3
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export class SaveService {
|
|
164
|
+
load(): SaveV3 {
|
|
165
|
+
const raw = localStorage.getItem(KEY); // only legal localStorage access lives here
|
|
166
|
+
if (!raw) return this.fresh();
|
|
167
|
+
try {
|
|
168
|
+
let s = JSON.parse(raw);
|
|
169
|
+
while (s.version < VERSION) s = migrations[s.version](s);
|
|
170
|
+
return s as SaveV3;
|
|
171
|
+
} catch { return this.fresh(); } // corrupt save -> fresh, never crash
|
|
172
|
+
}
|
|
173
|
+
save(s: SaveV3) { localStorage.setItem(KEY, JSON.stringify({ ...s, version: VERSION })); }
|
|
174
|
+
private fresh(): SaveV3 { return { version: VERSION, coins: 0, unlocked: [], settings: { muted: false } }; }
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Bump `VERSION` and add the `n -> n+1` migration whenever the shape changes; never
|
|
179
|
+
mutate the read path to "just handle" an old shape inline.
|
|
180
|
+
|
|
181
|
+
## Errors and telemetry
|
|
182
|
+
|
|
183
|
+
A vendor-neutral telemetry/analytics abstraction (`src/services/telemetry.ts`)
|
|
184
|
+
exposes `track(event, props)` and `captureError(err, ctx)` behind a stable
|
|
185
|
+
interface so the concrete sink (PostHog, Sentry, none) swaps without touching
|
|
186
|
+
game code. Wire Phaser's error surfaces to it — `window.onerror`, the loader's
|
|
187
|
+
`FILE_LOAD_ERROR`, and `try/catch` around scene transitions all route to
|
|
188
|
+
`captureError`. Strings shown to players come from the i18n catalog
|
|
189
|
+
([[phaser-i18n]]), never hardcoded.
|
|
190
|
+
|
|
191
|
+
## Project conventions
|
|
192
|
+
|
|
193
|
+
- Services live in `src/services/**`; that directory is the **only** place raw
|
|
194
|
+
`localStorage` is permitted.
|
|
195
|
+
- One EventsCenter instance, exported once; never `game.events` for app events.
|
|
196
|
+
- Every external `.on()` is paired with an `.off()` removed in `shutdown` (lint:
|
|
197
|
+
`require-shutdown-cleanup`; verified by the leak gate in [[phaser-testing]]).
|
|
198
|
+
- Service logic that is pure (migrations, action mapping, mute gating) belongs in
|
|
199
|
+
or beside `src/logic/**` so Vitest covers it.
|
|
200
|
+
|
|
201
|
+
## Verification
|
|
202
|
+
|
|
203
|
+
Services are verified by the runtime gates: the leak gate ([[phaser-testing]])
|
|
204
|
+
proves listeners are removed (start/stop a scene N times, counts return to
|
|
205
|
+
baseline); a SaveService unit test feeds each old version through the migration
|
|
206
|
+
chain and asserts the current shape; and on a real phone, the first tap unlocks
|
|
207
|
+
audio (sound plays) — confirm before committing.
|
|
208
|
+
</content>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: phaser-testing
|
|
3
|
-
description: This skill should be used when writing or designing tests for a Phaser 4 game — unit-testing pure game logic with Vitest, keeping logic Phaser-free so it tests without a browser, the Phaser.HEADLESS renderer for logic-only boots, asset-manifest coverage tests, and
|
|
3
|
+
description: This skill should be used when writing or designing tests for a Phaser 4 game — unit-testing pure game logic with Vitest, keeping logic Phaser-free so it tests without a browser, the Phaser.HEADLESS renderer for logic-only boots, asset-manifest coverage tests, and the CI runtime gates (boot smoke, allocation/perf budget, leak gate, determinism gate, deterministic Playwright visual regression, bundle-size budget) that verify scenes and entities that are excluded from unit coverage. Use it when adding tests, setting up CI verification, or deciding what is testable at which level. Pairs with phaser-project-structure, phaser-assets, and phaser-services.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Phaser 4 Testing
|
|
@@ -13,10 +13,17 @@ pyramid, bottom-up:
|
|
|
13
13
|
1. **Vitest unit tests** over `src/logic/**` — the bulk of coverage.
|
|
14
14
|
2. **Manifest/contract tests** — cheap structural checks (asset packs, scene
|
|
15
15
|
keys, anim definitions).
|
|
16
|
-
3. **
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
3. **Headless integration** (sparingly) — `Phaser.HEADLESS` for things that
|
|
17
|
+
genuinely need the engine loop.
|
|
18
|
+
4. **Playwright smoke** — the game boots in a real browser, renders past the
|
|
19
|
+
Preloader, no console errors.
|
|
20
|
+
5. **CI runtime gates** — scenes/entities are excluded from unit coverage and
|
|
21
|
+
instead verified by deterministic runtime gates: boot smoke, allocation/perf
|
|
22
|
+
budget, leak gate, determinism gate, visual regression, bundle-size budget.
|
|
23
|
+
|
|
24
|
+
There is no official Phaser testing harness — this layering IS the strategy. The
|
|
25
|
+
dividing line: pure logic is unit-tested; everything that needs the engine is a
|
|
26
|
+
runtime gate, never a brittle unit test that mocks the world.
|
|
20
27
|
|
|
21
28
|
## Layer 1: pure logic under Vitest (the rule that makes it possible)
|
|
22
29
|
|
|
@@ -85,15 +92,101 @@ Convention: scenes set `window.__sceneReady = key` in `create()` (dev/test
|
|
|
85
92
|
builds) so tests await real readiness instead of sleeping. Run against
|
|
86
93
|
`bun run dev` (or `vite preview` in CI).
|
|
87
94
|
|
|
95
|
+
## Layer 5: CI runtime gates (how scenes get verified)
|
|
96
|
+
|
|
97
|
+
Scenes and entities are deliberately **excluded from unit coverage** — mocking
|
|
98
|
+
the engine to "unit test" a scene tests the mock. Instead, CI runs a set of
|
|
99
|
+
deterministic gates against a real (or headless) game. Each gate fails the build
|
|
100
|
+
on regression; together they are the contract that the engine-coupled layer
|
|
101
|
+
keeps working.
|
|
102
|
+
|
|
103
|
+
**Boot smoke** — the Playwright test above, hardened: page error listener, wait
|
|
104
|
+
for `__sceneReady`, assert zero console errors. This is the floor.
|
|
105
|
+
|
|
106
|
+
**Allocation / perf budget** — run the game for N frames and assert the frame
|
|
107
|
+
budget and heap growth. Per-frame allocation is what the `no-allocation-in-update`
|
|
108
|
+
and `no-create-in-update` lint rules forbid statically; this gate catches what
|
|
109
|
+
slips through dynamically.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
test("steady-state frames stay within budget", async ({ page }) => {
|
|
113
|
+
await page.goto("/"); await bootTo(page, "Game");
|
|
114
|
+
const stats = await page.evaluate(async () => {
|
|
115
|
+
const g = (window as any).__game as Phaser.Game;
|
|
116
|
+
const frames: number[] = []; let last = performance.now();
|
|
117
|
+
for (let i = 0; i < 600; i++) { await new Promise(r => g.events.once("postrender", r)); const n = performance.now(); frames.push(n - last); last = n; }
|
|
118
|
+
return { p95: frames.sort((a,b)=>a-b)[Math.floor(frames.length*0.95)], heap: (performance as any).memory?.usedJSHeapSize };
|
|
119
|
+
});
|
|
120
|
+
expect(stats.p95).toBeLessThan(20); // ~50fps floor on CI hardware
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Leak gate** — start and stop a scene N times and assert that texture count,
|
|
125
|
+
event-listener count, active tweens, and timers all return to baseline. This is
|
|
126
|
+
the runtime enforcement of the `require-shutdown-cleanup` rule and the
|
|
127
|
+
on/off-discipline in [[phaser-services]].
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
test("scene start/stop leaves no leaks", async ({ page }) => {
|
|
131
|
+
await page.goto("/"); await bootTo(page, "MainMenu");
|
|
132
|
+
const before = await page.evaluate(() => (window as any).__game.textures.getTextureKeys().length);
|
|
133
|
+
for (let i = 0; i < 20; i++) await page.evaluate(async () => {
|
|
134
|
+
const g = (window as any).__game as Phaser.Game; g.scene.start("Game");
|
|
135
|
+
await new Promise(r => setTimeout(r, 50)); g.scene.stop("Game");
|
|
136
|
+
await new Promise(r => setTimeout(r, 50));
|
|
137
|
+
});
|
|
138
|
+
const after = await page.evaluate(() => (window as any).__game.textures.getTextureKeys().length);
|
|
139
|
+
expect(after).toBeLessThanOrEqual(before); // no per-cycle texture growth
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Determinism gate** — boot twice with the same seed, drive the same inputs, and
|
|
144
|
+
assert an identical state hash. Logic exposes a serializable snapshot; the gate
|
|
145
|
+
hashes it. Same seed → same hash, always. This is the runtime backstop for the
|
|
146
|
+
no-`Math.random()`/`Date.now()`/`performance.now()` rules ([[phaser-services]]).
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const run = (seed: number) => page.evaluate(s => (window as any).__sim(s, REPLAY), seed);
|
|
150
|
+
expect(hash(await run(1234))).toBe(hash(await run(1234)));
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Deterministic visual regression** — `toHaveScreenshot` under **software GL**
|
|
154
|
+
(`--use-gl=swiftshader`), a **frozen frame** (pause the loop / step a fixed
|
|
155
|
+
number of ticks at a fixed delta), and **masked dynamic regions** (timers,
|
|
156
|
+
particles). Without all three, screenshots flake. Pin Playwright's browser and
|
|
157
|
+
OS in CI so the baseline is stable.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
await page.evaluate(() => { const g=(window as any).__game; g.loop.sleep(); g.step(0,16.6); });
|
|
161
|
+
await expect(page).toHaveScreenshot("game.png", { mask: [page.locator("#timer")], maxDiffPixelRatio: 0.01 });
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Bundle-size budget** — assert the built bundle stays under a byte budget so a
|
|
165
|
+
stray dependency or an un-split `phaser` chunk fails the PR. Wire it to the prod
|
|
166
|
+
build's `manualChunks` split ([[phaser-build-deploy]]).
|
|
167
|
+
|
|
168
|
+
```jsonc
|
|
169
|
+
// size-limit / custom check after `bun run build`
|
|
170
|
+
[{ "path": "dist/assets/index-*.js", "limit": "60 kB" },
|
|
171
|
+
{ "path": "dist/assets/phaser-*.js", "limit": "400 kB" }]
|
|
172
|
+
```
|
|
173
|
+
|
|
88
174
|
## Project conventions
|
|
89
175
|
|
|
90
|
-
- `bun run test` = Vitest (layers 1–2, coverage-gated
|
|
91
|
-
|
|
176
|
+
- `bun run test` = Vitest (layers 1–2, coverage-gated **over `src/logic/**`
|
|
177
|
+
only** — scenes/entities are out of scope here). The layer-5 gates run as their
|
|
178
|
+
own CI jobs against a built `vite preview` (and a headless boot for the
|
|
179
|
+
determinism gate).
|
|
180
|
+
- Build pins are part of the contract: `phaser ^4.2.0`, Vite, TypeScript 6.
|
|
92
181
|
- Tests never assert on private scene fields; they assert on logic outputs
|
|
93
|
-
(layer 1)
|
|
182
|
+
(layer 1), observable behavior (layer 4), or the runtime invariants the gates
|
|
183
|
+
measure (layer 5).
|
|
94
184
|
|
|
95
185
|
## Verification
|
|
96
186
|
|
|
97
187
|
The testing setup itself is verified when `bun run test` passes with coverage
|
|
98
|
-
over `src/logic/**`,
|
|
99
|
-
|
|
188
|
+
over `src/logic/**`, the Playwright smoke fails when you deliberately break boot
|
|
189
|
+
(rename a pack file), and each layer-5 gate fails on its targeted regression —
|
|
190
|
+
leak gate when a listener loses its `.off()`, determinism gate when a
|
|
191
|
+
`Math.random()` sneaks in, visual gate when the frame changes. A gate that
|
|
192
|
+
can't fail is decoration.
|
|
@@ -206,7 +206,7 @@ check_env() {
|
|
|
206
206
|
| Solid Queue jobs stuck | Worker process crashed | Check `docker compose logs worker`; restart worker container |
|
|
207
207
|
| Migrations fail on deploy | Unsafe migration detected by Strong Migrations | Fix the migration per Strong Migrations guidance, then redeploy |
|
|
208
208
|
| `ActiveRecord::NoDatabaseError` | Database not created | `bin/rails db:create` (local) or check ECS task definition env vars |
|
|
209
|
-
| AWS credentials expired | SSO session timed out |
|
|
209
|
+
| AWS credentials expired | SSO session timed out locally, or missing env-backed profile headless | Interactive local: refresh the local AWS profile outside headless automation. Headless: use the configured environment-backed assume-role profile |
|
|
210
210
|
| ECS tasks keep restarting | Health check failing or OOM | Check CloudWatch logs for the task; verify `/up` returns 200; check memory limits |
|
|
211
211
|
| OpenTelemetry traces missing | Collector not configured or endpoint wrong | Verify `OTEL_EXPORTER_OTLP_ENDPOINT` in environment; check collector sidecar logs |
|
|
212
212
|
| `Bundler::GemNotFound` | Missing `bundle install` after Gemfile change | `bundle install` locally; for deploy, rebuild Docker image |
|
|
@@ -206,7 +206,7 @@ check_env() {
|
|
|
206
206
|
| Solid Queue jobs stuck | Worker process crashed | Check `docker compose logs worker`; restart worker container |
|
|
207
207
|
| Migrations fail on deploy | Unsafe migration detected by Strong Migrations | Fix the migration per Strong Migrations guidance, then redeploy |
|
|
208
208
|
| `ActiveRecord::NoDatabaseError` | Database not created | `bin/rails db:create` (local) or check ECS task definition env vars |
|
|
209
|
-
| AWS credentials expired | SSO session timed out |
|
|
209
|
+
| AWS credentials expired | SSO session timed out locally, or missing env-backed profile headless | Interactive local: refresh the local AWS profile outside headless automation. Headless: use the configured environment-backed assume-role profile |
|
|
210
210
|
| ECS tasks keep restarting | Health check failing or OOM | Check CloudWatch logs for the task; verify `/up` returns 200; check memory limits |
|
|
211
211
|
| OpenTelemetry traces missing | Collector not configured or endpoint wrong | Verify `OTEL_EXPORTER_OTLP_ENDPOINT` in environment; check collector sidecar logs |
|
|
212
212
|
| `Bundler::GemNotFound` | Missing `bundle install` after Gemfile change | `bundle install` locally; for deploy, rebuild Docker image |
|
|
@@ -206,7 +206,7 @@ check_env() {
|
|
|
206
206
|
| Solid Queue jobs stuck | Worker process crashed | Check `docker compose logs worker`; restart worker container |
|
|
207
207
|
| Migrations fail on deploy | Unsafe migration detected by Strong Migrations | Fix the migration per Strong Migrations guidance, then redeploy |
|
|
208
208
|
| `ActiveRecord::NoDatabaseError` | Database not created | `bin/rails db:create` (local) or check ECS task definition env vars |
|
|
209
|
-
| AWS credentials expired | SSO session timed out |
|
|
209
|
+
| AWS credentials expired | SSO session timed out locally, or missing env-backed profile headless | Interactive local: refresh the local AWS profile outside headless automation. Headless: use the configured environment-backed assume-role profile |
|
|
210
210
|
| ECS tasks keep restarting | Health check failing or OOM | Check CloudWatch logs for the task; verify `/up` returns 200; check memory limits |
|
|
211
211
|
| OpenTelemetry traces missing | Collector not configured or endpoint wrong | Verify `OTEL_EXPORTER_OTLP_ENDPOINT` in environment; check collector sidecar logs |
|
|
212
212
|
| `Bundler::GemNotFound` | Missing `bundle install` after Gemfile change | `bundle install` locally; for deploy, rebuild Docker image |
|
|
@@ -206,7 +206,7 @@ check_env() {
|
|
|
206
206
|
| Solid Queue jobs stuck | Worker process crashed | Check `docker compose logs worker`; restart worker container |
|
|
207
207
|
| Migrations fail on deploy | Unsafe migration detected by Strong Migrations | Fix the migration per Strong Migrations guidance, then redeploy |
|
|
208
208
|
| `ActiveRecord::NoDatabaseError` | Database not created | `bin/rails db:create` (local) or check ECS task definition env vars |
|
|
209
|
-
| AWS credentials expired | SSO session timed out |
|
|
209
|
+
| AWS credentials expired | SSO session timed out locally, or missing env-backed profile headless | Interactive local: refresh the local AWS profile outside headless automation. Headless: use the configured environment-backed assume-role profile |
|
|
210
210
|
| ECS tasks keep restarting | Health check failing or OOM | Check CloudWatch logs for the task; verify `/up` returns 200; check memory limits |
|
|
211
211
|
| OpenTelemetry traces missing | Collector not configured or endpoint wrong | Verify `OTEL_EXPORTER_OTLP_ENDPOINT` in environment; check collector sidecar logs |
|
|
212
212
|
| `Bundler::GemNotFound` | Missing `bundle install` after Gemfile change | `bundle install` locally; for deploy, rebuild Docker image |
|
|
@@ -78,13 +78,18 @@ Group the findings as:
|
|
|
78
78
|
git, and coreutils are present; `gh` is available but should be installed explicitly if scripts
|
|
79
79
|
call it. Classify each `REQUIRED` (core path uses it) vs `OPTIONAL` (only a dormant stack/skill
|
|
80
80
|
uses it). Typical finds: `gh`, `jq`, `docker` (ZAP), `aws`, `acli`, `ruby`/`rubocop`,
|
|
81
|
-
`python3`, `playwright`/chromium, secret scanners.
|
|
81
|
+
`python3`, `playwright`/chromium, secret scanners. Treat AWS as more than a
|
|
82
|
+
binary install: if the repo invokes `aws`, imports AWS SDK packages, uses CDK/Serverless/SST,
|
|
83
|
+
references `AWS_*` env vars, or documents `aws sso login` / `sso_*` profile setup, add the AWS
|
|
84
|
+
credential findings in group 4b as well as the `aws` CLI tool finding.
|
|
82
85
|
|
|
83
86
|
4. **Environment variables & secrets** — scan for `process.env.*`, `${VAR}`/`$VAR` in shell,
|
|
84
87
|
`secrets.*`/`env:` in CI, and config-referenced tokens. Group by integration (GitHub, AWS,
|
|
85
88
|
Atlassian/JIRA/Confluence, Notion, Linear, Anthropic, notifications, feature flags, other).
|
|
86
89
|
Cross-reference `.lisa.config.json` `tracker`/`source` to mark which credentials are **active**
|
|
87
|
-
for this repo vs **dormant** (`OPTIONAL`).
|
|
90
|
+
for this repo vs **dormant** (`OPTIONAL`). Separately classify host-project AWS usage in group 4b:
|
|
91
|
+
AWS can be required even when it is not the tracker or PRD source. Distinguish *where* each var
|
|
92
|
+
must be set, because the
|
|
88
93
|
answer differs and getting it wrong sends the user to do redundant work:
|
|
89
94
|
|
|
90
95
|
- **Committed `.claude/settings.json` `env` flags** (e.g. `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS`,
|
|
@@ -128,6 +133,34 @@ Group the findings as:
|
|
|
128
133
|
guessed. If a value the table needs (server URL, project key, workspace id, email, team key) is
|
|
129
134
|
missing from `.lisa.config.json`, flag it as a `GAP`/`Action:` to set it, rather than inventing one.
|
|
130
135
|
|
|
136
|
+
4b. **AWS operations credentials** — scan the host project for AWS usage independent of Lisa's
|
|
137
|
+
tracker/source config:
|
|
138
|
+
|
|
139
|
+
- `aws` CLI invocations in `scripts/`, package scripts, committed skills/agents, or
|
|
140
|
+
`.github/workflows/`.
|
|
141
|
+
- AWS SDK imports/packages (`@aws-sdk/*`, `aws-sdk`, `boto3`, CDK, Serverless, SST, Amplify,
|
|
142
|
+
Terraform providers).
|
|
143
|
+
- `aws sso login`, `aws:signin:*`, `sso_start_url`, `sso_account_id`, `sso_role_name`, or
|
|
144
|
+
`sso_session` in docs or config.
|
|
145
|
+
- `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_DEFAULT_REGION`,
|
|
146
|
+
`AWS_PROFILE`, role ARN, external ID, account, or CloudWatch/STS references.
|
|
147
|
+
|
|
148
|
+
If AWS is present, emit an AWS credential finding and inventory entry. If the repo's current
|
|
149
|
+
local path is `aws sso login` or an `aws:signin:*` script, classify that local path as a `GAP`
|
|
150
|
+
for headless remote routines: it is an interactive browser/device-authorization flow and cannot
|
|
151
|
+
complete in the cloud. Route to the AWS row in the Credential reference instead of suggesting
|
|
152
|
+
SSO auth. The finding must name the headless substrate: a dedicated IAM principal (user or role)
|
|
153
|
+
with only permission to `sts:AssumeRole`, environment credentials in the routine UI, and
|
|
154
|
+
`~/.aws/config` profiles using `role_arn`, `credential_source = Environment`, optional
|
|
155
|
+
`external_id`, and `region`.
|
|
156
|
+
|
|
157
|
+
When the repository contains concrete non-secret AWS metadata (role ARNs, account aliases,
|
|
158
|
+
profile names, regions, or ExternalId values), include it in an `awsProfiles` inventory array so
|
|
159
|
+
`/lisa:generate-claude-remote-build-script` can write matching `~/.aws/config` profiles. Never
|
|
160
|
+
invent account IDs, ExternalIds, role names, or regions. If AWS is detected but profile metadata
|
|
161
|
+
is absent, emit the required AWS secret names plus an action to add project-specific profile
|
|
162
|
+
metadata to the generated artifact or environment notes.
|
|
163
|
+
|
|
131
164
|
5. **MCP servers** — read every committed `.mcp.json`. For each server report transport and auth.
|
|
132
165
|
Project-scoped HTTP/SSE servers with no interactive auth are `OK`. Flag stdio servers as
|
|
133
166
|
`RISK`/`GAP` (need a local process — only viable if the cloud session can spawn them from the
|
|
@@ -234,6 +267,31 @@ unsuffixed `…_TOKEN`/`…_KEY` is the simplest to set in a single-account rout
|
|
|
234
267
|
- Acquire: `https://linear.app/<workspace>/settings/account/security` → Personal API keys → New API key.
|
|
235
268
|
- Access: the personal API key inherits the user's workspace permissions — the user must be able to read/create/update Issues in the destination team.
|
|
236
269
|
|
|
270
|
+
### AWS — host-project operations, logs, deploys, CDK/Serverless/SST, or AWS SDK usage
|
|
271
|
+
- Headless substrate: a dedicated IAM principal (user or role) with long-lived bootstrap credentials
|
|
272
|
+
stored in the routine environment, used only to call `sts:AssumeRole` into per-account operational
|
|
273
|
+
roles. The routine writes `~/.aws/config` profiles with `role_arn`, `credential_source =
|
|
274
|
+
Environment`, optional `external_id`, and `region`; agents use `aws --profile <profile> ...`.
|
|
275
|
+
- Env: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`; optional `AWS_SESSION_TOKEN` for temporary
|
|
276
|
+
bootstrap credentials; `AWS_DEFAULT_REGION` when no profile region is provided. Role ARNs,
|
|
277
|
+
ExternalIds, profile names, and regions are non-secret project metadata and belong in generated
|
|
278
|
+
artifacts or project docs, not in the skill's global guidance.
|
|
279
|
+
- Allowlist: `*.amazonaws.com`, or narrower service hosts such as `sts.amazonaws.com`,
|
|
280
|
+
`logs.<region>.amazonaws.com`, `cloudwatch.<region>.amazonaws.com`,
|
|
281
|
+
`xray.<region>.amazonaws.com`, `ssm.<region>.amazonaws.com`, and service-specific endpoints the
|
|
282
|
+
repo uses.
|
|
283
|
+
- Access: the bootstrap principal should have only `sts:AssumeRole` on the scoped operational role
|
|
284
|
+
ARNs, preferably guarded by an `ExternalId` condition. The assumed roles carry the real
|
|
285
|
+
permissions needed by the repo (CloudWatch Logs, deploy, SSM, etc.) as short-lived STS
|
|
286
|
+
credentials.
|
|
287
|
+
- IAM Identity Center caveat: an IAM Identity Center permission set provisions an `AWSReservedSSO_*`
|
|
288
|
+
role whose trust allows the SSO service, not an arbitrary IAM principal. A headless IAM principal
|
|
289
|
+
cannot directly assume that reserved role. Use a separate assumable role that attaches the same
|
|
290
|
+
policy as the permission set, with the delegated Identity Center admin keeping that policy as the
|
|
291
|
+
source of truth.
|
|
292
|
+
- Alternative: IAM Roles Anywhere (X.509 to STS) when long-lived bootstrap access keys are
|
|
293
|
+
unacceptable.
|
|
294
|
+
|
|
237
295
|
## Output
|
|
238
296
|
|
|
239
297
|
Render the report grouped exactly as above. Start with one `Summary:` line, then a `Counts:` line
|
|
@@ -296,6 +354,15 @@ so the generator can render acquisition comments into its template:
|
|
|
296
354
|
{ "name": "jam", "transport": "http", "auth": "oauth", "headlessUsable": true, "replacedBy": "jam CLI + JAM_PAT", "dormant": true },
|
|
297
355
|
{ "name": "sonarqube", "transport": "stdio", "auth": "local-wrapper", "headlessUsable": true, "replacedBy": "SONAR_TOKEN + SonarCloud Web API", "dormant": true }
|
|
298
356
|
],
|
|
357
|
+
"awsProfiles": [
|
|
358
|
+
{
|
|
359
|
+
"name": "dev",
|
|
360
|
+
"roleArn": "arn:aws:iam::<account-id>:role/<role-name>",
|
|
361
|
+
"credentialSource": "Environment",
|
|
362
|
+
"externalId": "<project-external-id>",
|
|
363
|
+
"region": "us-east-1"
|
|
364
|
+
}
|
|
365
|
+
],
|
|
299
366
|
"gaps": [ "auto-memory not synced to cloud", "bun package fetch behind proxy", "OS keychain absent — env-var token form only" ],
|
|
300
367
|
"platform": {
|
|
301
368
|
"githubProxy": {
|
|
@@ -346,6 +413,9 @@ so the generator can render acquisition comments into its template:
|
|
|
346
413
|
to environment editors; never imply the generated script can hide them.
|
|
347
414
|
- A browser-OAuth MCP (or interactively-authed CLI) backing an active integration is a `GAP`; the
|
|
348
415
|
remediation is its token substrate from the table, not "authenticate the MCP".
|
|
416
|
+
- `aws sso login`, `sso_*` profile configuration, and project scripts that wrap AWS SSO are
|
|
417
|
+
interactive-auth paths. In a headless routine they are a `GAP`, not a setup step. Route AWS access
|
|
418
|
+
to env-var bootstrap credentials plus assume-role profiles.
|
|
349
419
|
- For non-tracker MCPs, a documented CLI, PAT/API-key, or REST substrate converts the finding from
|
|
350
420
|
a flat `GAP` into an `OPTIONAL` headless recovery path; unknown vendors remain gaps with a
|
|
351
421
|
vendor-substrate follow-up, not invented credentials.
|