yaver-feedback-react-native 0.9.0 → 0.9.3

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.
Files changed (144) hide show
  1. package/README.md +102 -1
  2. package/app.plugin.js +126 -9
  3. package/dist/AuthOverlay.d.ts +1 -14
  4. package/dist/AuthOverlay.js +9 -62
  5. package/dist/BlackBox.js +9 -1
  6. package/dist/DeployPanel.js +65 -0
  7. package/dist/Discovery.js +13 -8
  8. package/dist/DogfoodRuntime.d.ts +124 -0
  9. package/dist/DogfoodRuntime.js +273 -0
  10. package/dist/FeedbackModal.js +505 -65
  11. package/dist/LoginScreen.d.ts +1 -5
  12. package/dist/LoginScreen.js +2 -6
  13. package/dist/MachinePickerScreen.d.ts +1 -3
  14. package/dist/MachinePickerScreen.js +6 -18
  15. package/dist/P2PClient.d.ts +178 -2
  16. package/dist/P2PClient.js +493 -3
  17. package/dist/P2PDogfoodDriver.d.ts +12 -0
  18. package/dist/P2PDogfoodDriver.js +118 -0
  19. package/dist/PairDeviceModal.d.ts +2 -3
  20. package/dist/ShakeDetector.js +2 -0
  21. package/dist/VibeChatScreen.d.ts +11 -1
  22. package/dist/VibeChatScreen.js +200 -41
  23. package/dist/YaverFeedback.d.ts +132 -0
  24. package/dist/YaverFeedback.js +633 -65
  25. package/dist/YaverModeBadge.d.ts +22 -0
  26. package/dist/YaverModeBadge.js +219 -0
  27. package/dist/__tests__/AuthDevices.test.js +1 -48
  28. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  29. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  30. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  31. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  32. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  33. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  34. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  35. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  36. package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
  37. package/dist/__tests__/DogfoodRuntime.test.js +116 -0
  38. package/dist/__tests__/P2PClient.test.js +2 -2
  39. package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
  40. package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
  41. package/dist/__tests__/ReportIdentity.test.d.ts +25 -0
  42. package/dist/__tests__/ReportIdentity.test.js +180 -0
  43. package/dist/__tests__/SDKToken.test.js +1 -1
  44. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  45. package/dist/__tests__/ShakeToggle.test.js +121 -0
  46. package/dist/__tests__/YaverFeedback.test.js +13 -3
  47. package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
  48. package/dist/__tests__/deviceDogfood.test.js +86 -0
  49. package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
  50. package/dist/__tests__/dogfoodPolicy.test.js +33 -0
  51. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  52. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  53. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  54. package/dist/__tests__/reloadActions.test.js +129 -0
  55. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  56. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  57. package/dist/__tests__/types.test.js +5 -5
  58. package/dist/_core/ansi.d.ts +117 -0
  59. package/dist/_core/ansi.js +468 -0
  60. package/dist/_core/ansi.test.d.ts +1 -0
  61. package/dist/_core/ansi.test.js +225 -0
  62. package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
  63. package/dist/_core/buildFeedbackPrompt.js +18 -72
  64. package/dist/_core/constants.d.ts +19 -6
  65. package/dist/_core/constants.js +20 -7
  66. package/dist/_core/device.d.ts +1 -5
  67. package/dist/_core/device.js +5 -14
  68. package/dist/_core/endpoints.d.ts +0 -7
  69. package/dist/_core/endpoints.js +0 -7
  70. package/dist/_core/index.d.ts +4 -0
  71. package/dist/_core/index.js +4 -0
  72. package/dist/_core/remoteless.d.ts +44 -0
  73. package/dist/_core/remoteless.js +75 -0
  74. package/dist/_core/trace.d.ts +47 -0
  75. package/dist/_core/trace.js +38 -0
  76. package/dist/_core/trace.test.d.ts +1 -0
  77. package/dist/_core/trace.test.js +60 -0
  78. package/dist/auth.d.ts +3 -60
  79. package/dist/auth.js +6 -88
  80. package/dist/deviceDogfood.d.ts +53 -0
  81. package/dist/deviceDogfood.js +137 -0
  82. package/dist/dogfoodPolicy.d.ts +25 -0
  83. package/dist/dogfoodPolicy.js +24 -0
  84. package/dist/index.d.ts +17 -4
  85. package/dist/index.js +33 -7
  86. package/dist/reloadActions.d.ts +88 -0
  87. package/dist/reloadActions.js +200 -0
  88. package/dist/storeShots.d.ts +67 -0
  89. package/dist/storeShots.js +137 -0
  90. package/dist/types.d.ts +208 -8
  91. package/package.json +13 -4
  92. package/src/AuthOverlay.tsx +20 -106
  93. package/src/BlackBox.ts +10 -1
  94. package/src/DeployPanel.tsx +74 -0
  95. package/src/Discovery.ts +13 -8
  96. package/src/DogfoodRuntime.ts +373 -0
  97. package/src/FeedbackModal.tsx +619 -79
  98. package/src/LoginScreen.tsx +2 -22
  99. package/src/MachinePickerScreen.tsx +6 -21
  100. package/src/P2PClient.ts +580 -4
  101. package/src/P2PDogfoodDriver.ts +132 -0
  102. package/src/PairDeviceModal.tsx +2 -3
  103. package/src/ShakeDetector.ts +1 -0
  104. package/src/VibeChatScreen.tsx +232 -42
  105. package/src/YaverFeedback.ts +631 -68
  106. package/src/YaverModeBadge.tsx +234 -0
  107. package/src/__tests__/AuthDevices.test.ts +1 -52
  108. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  109. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  110. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  111. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  112. package/src/__tests__/DogfoodRuntime.test.ts +135 -0
  113. package/src/__tests__/P2PClient.test.ts +2 -2
  114. package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
  115. package/src/__tests__/ReportIdentity.test.ts +212 -0
  116. package/src/__tests__/SDKToken.test.ts +1 -1
  117. package/src/__tests__/ShakeToggle.test.ts +153 -0
  118. package/src/__tests__/YaverFeedback.test.ts +15 -3
  119. package/src/__tests__/deviceDogfood.test.ts +77 -0
  120. package/src/__tests__/dogfoodPolicy.test.ts +34 -0
  121. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  122. package/src/__tests__/reloadActions.test.ts +171 -0
  123. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  124. package/src/__tests__/types.test.ts +5 -5
  125. package/src/_core/ansi.test.ts +250 -0
  126. package/src/_core/ansi.ts +475 -0
  127. package/src/_core/buildFeedbackPrompt.ts +18 -95
  128. package/src/_core/constants.ts +20 -6
  129. package/src/_core/device.ts +5 -16
  130. package/src/_core/endpoints.ts +0 -7
  131. package/src/_core/index.ts +4 -0
  132. package/src/_core/remoteless.ts +110 -0
  133. package/src/_core/trace.test.ts +58 -0
  134. package/src/_core/trace.ts +75 -0
  135. package/src/auth.ts +7 -156
  136. package/src/deviceDogfood.ts +171 -0
  137. package/src/dogfoodPolicy.ts +40 -0
  138. package/src/index.ts +61 -11
  139. package/src/reloadActions.ts +273 -0
  140. package/src/storeShots.ts +189 -0
  141. package/src/types.ts +209 -8
  142. package/dist/GuestOnboardingScreen.d.ts +0 -8
  143. package/dist/GuestOnboardingScreen.js +0 -282
  144. package/src/GuestOnboardingScreen.tsx +0 -307
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Embeddable Dogfood orchestration for React Native hosts.
3
+ *
4
+ * The SDK deliberately does not own the host's button, WebView, navigation, or
5
+ * Yaver transport. It owns the failure-prone lifecycle underneath them:
6
+ * explicit trigger -> prepare -> start -> ready, bounded log retention,
7
+ * generation-safe Retry, and deterministic cleanup of partial sessions.
8
+ *
9
+ * Yaver mobile uses this class for Yaver-on-Yaver. Third-party apps use the
10
+ * same class with their own project descriptor and a driver backed by
11
+ * P2PClient (or another authenticated Yaver transport).
12
+ */
13
+ export type DogfoodLane = 'browser' | 'hermes' | 'webrtc';
14
+ export type DogfoodPhase = 'idle' | 'preparing' | 'starting' | 'compiling' | 'ready' | 'failed' | 'stopping' | 'stopped';
15
+ export interface DogfoodProject {
16
+ name: string;
17
+ workDir: string;
18
+ framework: string;
19
+ lane: DogfoodLane;
20
+ /** Optional source URL for drivers that can clone missing source. */
21
+ repositoryUrl?: string;
22
+ /** Optional native target from /remote-runtime/capabilities for WebRTC. */
23
+ nativeTargetId?: string;
24
+ }
25
+ export interface DogfoodLaneOption {
26
+ lane: DogfoodLane;
27
+ label: string;
28
+ supported: boolean;
29
+ default: boolean;
30
+ reason?: string;
31
+ }
32
+ /** One framework-to-lane matrix for Yaver and third-party consumers. */
33
+ export declare function dogfoodLaneOptions(framework: string, capabilities?: {
34
+ nativeRuntimeAvailable?: boolean;
35
+ selfDevelopment?: boolean;
36
+ }): DogfoodLaneOption[];
37
+ export declare function defaultDogfoodLane(framework: string): DogfoodLane;
38
+ export interface DogfoodLogLine {
39
+ text: string;
40
+ at: number;
41
+ stream?: 'stdout' | 'stderr' | 'system';
42
+ }
43
+ export interface DogfoodFailure {
44
+ code: string;
45
+ error: string;
46
+ remedy: string;
47
+ retryable: boolean;
48
+ fixPrompt?: string;
49
+ }
50
+ export interface DogfoodResult {
51
+ lane: DogfoodLane;
52
+ url?: string;
53
+ sessionId?: string;
54
+ metadata?: Record<string, unknown>;
55
+ }
56
+ export interface DogfoodSnapshot {
57
+ phase: DogfoodPhase;
58
+ attempt: number;
59
+ project: DogfoodProject;
60
+ message: string;
61
+ logs: readonly DogfoodLogLine[];
62
+ startedAt?: number;
63
+ lastOutputAt?: number;
64
+ result?: DogfoodResult;
65
+ failure?: DogfoodFailure;
66
+ }
67
+ export interface DogfoodRunContext {
68
+ project: DogfoodProject;
69
+ attempt: number;
70
+ /** Raw package-manager/compiler output. ANSI is intentionally preserved. */
71
+ log(line: string | DogfoodLogLine): void;
72
+ setPhase(phase: Extract<DogfoodPhase, 'preparing' | 'starting' | 'compiling'>, message: string): void;
73
+ /**
74
+ * Register cleanup immediately after acquiring a resource.
75
+ *
76
+ * `session` cleanups run on failure/stop. `transient` cleanups also run when
77
+ * ownership is handed to another screen (normally an SSE subscription).
78
+ */
79
+ registerCleanup(cleanup: () => void | Promise<void>, scope?: 'session' | 'transient'): void;
80
+ isCurrent(): boolean;
81
+ }
82
+ export interface DogfoodDriver {
83
+ prepare?(context: DogfoodRunContext): Promise<void>;
84
+ start(context: DogfoodRunContext): Promise<DogfoodResult>;
85
+ }
86
+ export interface DogfoodControllerOptions {
87
+ maxLogLines?: number;
88
+ onChange?: (snapshot: DogfoodSnapshot) => void;
89
+ }
90
+ export declare class DogfoodRuntimeError extends Error {
91
+ readonly failure: DogfoodFailure;
92
+ constructor(failure: DogfoodFailure);
93
+ }
94
+ export declare function validateDogfoodProject(project: DogfoodProject): DogfoodFailure | null;
95
+ /** Convert one /dev/events frame into console lines without hiding raw output. */
96
+ export declare function runtimeLogLinesFromDevEvent(event: any): string[];
97
+ /** Backwards-friendly name for consumers that discovered this via Dogfood. */
98
+ export declare const dogfoodLogLinesFromDevEvent: typeof runtimeLogLinesFromDevEvent;
99
+ export declare class DogfoodController {
100
+ readonly project: DogfoodProject;
101
+ private readonly driver;
102
+ private generation;
103
+ private cleanups;
104
+ private runPromise;
105
+ private readonly maxLogLines;
106
+ private readonly onChange?;
107
+ private state;
108
+ constructor(project: DogfoodProject, driver: DogfoodDriver, options?: DogfoodControllerOptions);
109
+ snapshot(): DogfoodSnapshot;
110
+ /** Nothing starts until the host calls this method from its explicit action. */
111
+ trigger(): Promise<DogfoodResult>;
112
+ retry(): Promise<DogfoodResult>;
113
+ private run;
114
+ /** Stop the active/partial run and release every resource. Idempotent. */
115
+ stop(): Promise<void>;
116
+ /**
117
+ * Transfer the live session to a host-owned preview screen. Stream/timer
118
+ * cleanups run; session cleanup is discarded because the receiving screen
119
+ * now owns it.
120
+ */
121
+ handoff(): Promise<DogfoodResult | undefined>;
122
+ private runCleanups;
123
+ private replace;
124
+ }
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ /**
3
+ * Embeddable Dogfood orchestration for React Native hosts.
4
+ *
5
+ * The SDK deliberately does not own the host's button, WebView, navigation, or
6
+ * Yaver transport. It owns the failure-prone lifecycle underneath them:
7
+ * explicit trigger -> prepare -> start -> ready, bounded log retention,
8
+ * generation-safe Retry, and deterministic cleanup of partial sessions.
9
+ *
10
+ * Yaver mobile uses this class for Yaver-on-Yaver. Third-party apps use the
11
+ * same class with their own project descriptor and a driver backed by
12
+ * P2PClient (or another authenticated Yaver transport).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DogfoodController = exports.dogfoodLogLinesFromDevEvent = exports.DogfoodRuntimeError = void 0;
16
+ exports.dogfoodLaneOptions = dogfoodLaneOptions;
17
+ exports.defaultDogfoodLane = defaultDogfoodLane;
18
+ exports.validateDogfoodProject = validateDogfoodProject;
19
+ exports.runtimeLogLinesFromDevEvent = runtimeLogLinesFromDevEvent;
20
+ /** One framework-to-lane matrix for Yaver and third-party consumers. */
21
+ function dogfoodLaneOptions(framework, capabilities = {}) {
22
+ const normalized = String(framework || '').trim().toLowerCase();
23
+ const reactNative = normalized === 'expo' || normalized === 'react-native';
24
+ const browserCapable = reactNative || [
25
+ 'flutter', 'web', 'next', 'nextjs', 'vite', 'remix', 'svelte', 'vue', 'angular',
26
+ ].includes(normalized);
27
+ const nativeAvailable = capabilities.nativeRuntimeAvailable === true;
28
+ const hermesReason = reactNative
29
+ ? undefined
30
+ : 'Hermes is available only for Expo and React Native projects.';
31
+ const browserReason = browserCapable
32
+ ? undefined
33
+ : 'The browser lane is available for browser-capable projects such as React Native, Expo, Flutter, and web apps.';
34
+ return [
35
+ { lane: 'browser', label: 'Browser lane', supported: browserCapable, default: browserCapable, reason: browserReason },
36
+ { lane: 'hermes', label: 'Hermes', supported: !hermesReason, default: false, reason: hermesReason },
37
+ {
38
+ lane: 'webrtc', label: 'WebRTC native', supported: nativeAvailable, default: false,
39
+ reason: nativeAvailable ? undefined : 'No native simulator, emulator, or device runtime is available on this machine.',
40
+ },
41
+ ];
42
+ }
43
+ function defaultDogfoodLane(framework) {
44
+ return dogfoodLaneOptions(framework).find((option) => option.default && option.supported)?.lane || 'browser';
45
+ }
46
+ class DogfoodRuntimeError extends Error {
47
+ constructor(failure) {
48
+ super(failure.error);
49
+ this.name = 'DogfoodRuntimeError';
50
+ this.failure = failure;
51
+ }
52
+ }
53
+ exports.DogfoodRuntimeError = DogfoodRuntimeError;
54
+ function validateDogfoodProject(project) {
55
+ const framework = String(project.framework || '').trim().toLowerCase();
56
+ if (!String(project.workDir || '').trim()) {
57
+ return {
58
+ code: 'DOGFOOD_PROJECT_PATH_REQUIRED',
59
+ error: 'Dogfood needs the project directory on the selected machine.',
60
+ remedy: 'Choose or clone the project source, then trigger Dogfood again.',
61
+ retryable: false,
62
+ };
63
+ }
64
+ if (project.lane === 'hermes' && framework !== 'expo' && framework !== 'react-native') {
65
+ return {
66
+ code: 'DOGFOOD_HERMES_FRAMEWORK_UNSUPPORTED',
67
+ error: `Hermes cannot run a ${framework || 'non-React-Native'} project.`,
68
+ remedy: framework === 'flutter'
69
+ ? 'Use the browser lane (Flutter web) or WebRTC for a native Flutter runtime.'
70
+ : 'Use the browser or WebRTC lane, or select an Expo/React Native project.',
71
+ retryable: false,
72
+ };
73
+ }
74
+ return null;
75
+ }
76
+ /** Convert one /dev/events frame into console lines without hiding raw output. */
77
+ function runtimeLogLinesFromDevEvent(event) {
78
+ if (!event || typeof event !== 'object')
79
+ return [];
80
+ if (event.type === 'log' && typeof event.logLine === 'string') {
81
+ return event.logLine.trimEnd() ? [event.logLine.trimEnd()] : [];
82
+ }
83
+ if (event.type === 'snapshot' && Array.isArray(event.snapshot?.recentLogs)) {
84
+ return event.snapshot.recentLogs
85
+ .map((line) => String(line).trimEnd())
86
+ .filter(Boolean);
87
+ }
88
+ if (event.type === 'progress' || event.type === 'phase') {
89
+ const phase = typeof event.phase === 'string' ? event.phase.replace(/_/g, ' ') : 'working';
90
+ const pct = typeof event.pct === 'number' ? ` ${Math.round(event.pct)}%` : '';
91
+ const file = typeof event.currentFile === 'string' && event.currentFile
92
+ ? ` · ${String(event.currentFile).split('/').slice(-2).join('/')}`
93
+ : '';
94
+ return [`${phase}${pct}${file}`.trim()];
95
+ }
96
+ if (event.type === 'error') {
97
+ return String(event.message || 'Dev server failed')
98
+ .split('\n')
99
+ .map((line) => line.trimEnd())
100
+ .filter(Boolean);
101
+ }
102
+ if (typeof event.message === 'string' && event.message.trim() &&
103
+ ['starting', 'building', 'phase', 'ready', 'reload', 'stopped'].includes(String(event.type))) {
104
+ return [event.message.trimEnd()];
105
+ }
106
+ return [];
107
+ }
108
+ /** Backwards-friendly name for consumers that discovered this via Dogfood. */
109
+ exports.dogfoodLogLinesFromDevEvent = runtimeLogLinesFromDevEvent;
110
+ function failureFrom(error) {
111
+ if (error instanceof DogfoodRuntimeError)
112
+ return error.failure;
113
+ const candidate = error;
114
+ return {
115
+ code: typeof candidate?.code === 'string' ? candidate.code : 'DOGFOOD_START_FAILED',
116
+ error: error instanceof Error ? error.message : String(error || 'Dogfood could not start.'),
117
+ remedy: typeof candidate?.remedy === 'string'
118
+ ? candidate.remedy
119
+ : 'Fix the named project or runtime failure, then retry Dogfood.',
120
+ retryable: candidate?.retryable !== false,
121
+ fixPrompt: typeof candidate?.fixPrompt === 'string' ? candidate.fixPrompt : undefined,
122
+ };
123
+ }
124
+ class DogfoodController {
125
+ constructor(project, driver, options = {}) {
126
+ this.generation = 0;
127
+ // Cleanup ownership is per attempt. An old async attempt may finish after
128
+ // Stop + Retry has already acquired a new runtime; a shared list lets that
129
+ // obsolete attempt tear down the newer session. Generation-keyed ownership
130
+ // makes that race impossible while keeping Stop able to release everything.
131
+ this.cleanups = new Map();
132
+ this.runPromise = null;
133
+ this.project = project;
134
+ this.driver = driver;
135
+ this.maxLogLines = Math.max(20, options.maxLogLines ?? 200);
136
+ this.onChange = options.onChange;
137
+ this.state = { phase: 'idle', attempt: 0, project, message: 'Ready to dogfood', logs: [] };
138
+ }
139
+ snapshot() {
140
+ return { ...this.state, logs: [...this.state.logs] };
141
+ }
142
+ /** Nothing starts until the host calls this method from its explicit action. */
143
+ trigger() {
144
+ if (this.runPromise)
145
+ return this.runPromise;
146
+ const promise = this.run();
147
+ this.runPromise = promise;
148
+ void promise.finally(() => {
149
+ if (this.runPromise === promise)
150
+ this.runPromise = null;
151
+ }).catch(() => { });
152
+ return promise;
153
+ }
154
+ retry() {
155
+ return this.trigger();
156
+ }
157
+ async run() {
158
+ const generation = ++this.generation;
159
+ const attempt = this.state.attempt + 1;
160
+ await this.runCleanups('all');
161
+ const invalid = validateDogfoodProject(this.project);
162
+ if (invalid) {
163
+ this.replace({ phase: 'failed', attempt, project: this.project, message: invalid.error, logs: [], failure: invalid });
164
+ throw new DogfoodRuntimeError(invalid);
165
+ }
166
+ this.replace({
167
+ phase: 'preparing', attempt, project: this.project,
168
+ message: `Preparing ${this.project.name}…`, logs: [], startedAt: Date.now(),
169
+ });
170
+ const context = {
171
+ project: this.project,
172
+ attempt,
173
+ log: (line) => {
174
+ if (generation !== this.generation)
175
+ return;
176
+ const entry = typeof line === 'string'
177
+ ? { text: line, at: Date.now(), stream: 'stdout' }
178
+ : { ...line, at: line.at || Date.now() };
179
+ if (!entry.text.trim())
180
+ return;
181
+ const logs = [...this.state.logs, entry].slice(-this.maxLogLines);
182
+ this.replace({ ...this.state, logs, lastOutputAt: entry.at });
183
+ },
184
+ setPhase: (phase, message) => {
185
+ if (generation === this.generation)
186
+ this.replace({ ...this.state, phase, message });
187
+ },
188
+ registerCleanup: (cleanup, scope = 'session') => {
189
+ if (generation !== this.generation) {
190
+ void Promise.resolve(cleanup()).catch(() => { });
191
+ return;
192
+ }
193
+ const owned = this.cleanups.get(generation) || [];
194
+ owned.push({ fn: cleanup, scope });
195
+ this.cleanups.set(generation, owned);
196
+ },
197
+ isCurrent: () => generation === this.generation,
198
+ };
199
+ try {
200
+ await this.driver.prepare?.(context);
201
+ if (!context.isCurrent())
202
+ throw new DogfoodRuntimeError({
203
+ code: 'DOGFOOD_ATTEMPT_REPLACED', error: 'A newer Dogfood attempt replaced this one.',
204
+ remedy: 'Wait for the newer attempt.', retryable: true,
205
+ });
206
+ context.setPhase('starting', `Starting ${this.project.name} on the ${this.project.lane} lane…`);
207
+ const result = await this.driver.start(context);
208
+ if (!context.isCurrent()) {
209
+ await this.runCleanups('all', generation);
210
+ throw new DogfoodRuntimeError({
211
+ code: 'DOGFOOD_ATTEMPT_REPLACED', error: 'A newer Dogfood attempt replaced this one.',
212
+ remedy: 'Wait for the newer attempt.', retryable: true,
213
+ });
214
+ }
215
+ this.replace({ ...this.state, phase: 'ready', message: `${this.project.name} is ready`, result, failure: undefined });
216
+ return result;
217
+ }
218
+ catch (error) {
219
+ const failure = failureFrom(error);
220
+ await this.runCleanups('all', generation);
221
+ if (generation === this.generation) {
222
+ this.replace({ ...this.state, phase: 'failed', message: failure.error, failure, result: undefined });
223
+ }
224
+ throw error instanceof DogfoodRuntimeError ? error : new DogfoodRuntimeError(failure);
225
+ }
226
+ }
227
+ /** Stop the active/partial run and release every resource. Idempotent. */
228
+ async stop() {
229
+ ++this.generation;
230
+ this.runPromise = null;
231
+ this.replace({ ...this.state, phase: 'stopping', message: `Stopping ${this.project.name}…` });
232
+ await this.runCleanups('all');
233
+ this.replace({ ...this.state, phase: 'stopped', message: `${this.project.name} stopped`, result: undefined });
234
+ }
235
+ /**
236
+ * Transfer the live session to a host-owned preview screen. Stream/timer
237
+ * cleanups run; session cleanup is discarded because the receiving screen
238
+ * now owns it.
239
+ */
240
+ async handoff() {
241
+ if (this.state.phase !== 'ready')
242
+ return undefined;
243
+ await this.runCleanups('transient', this.generation);
244
+ // The receiving screen now owns the live session. Forget its session
245
+ // cleanup without touching cleanups belonging to any other generation.
246
+ this.cleanups.delete(this.generation);
247
+ return this.state.result;
248
+ }
249
+ async runCleanups(which, generation) {
250
+ const generations = generation === undefined ? [...this.cleanups.keys()] : [generation];
251
+ const selected = [];
252
+ for (const key of generations) {
253
+ const owned = this.cleanups.get(key) || [];
254
+ selected.push(...(which === 'all' ? owned : owned.filter((item) => item.scope === 'transient')));
255
+ const retained = which === 'all' ? [] : owned.filter((item) => item.scope !== 'transient');
256
+ if (retained.length)
257
+ this.cleanups.set(key, retained);
258
+ else
259
+ this.cleanups.delete(key);
260
+ }
261
+ for (const cleanup of selected.reverse()) {
262
+ try {
263
+ await cleanup.fn();
264
+ }
265
+ catch { /* cleanup is best-effort but never skipped */ }
266
+ }
267
+ }
268
+ replace(next) {
269
+ this.state = next;
270
+ this.onChange?.(this.snapshot());
271
+ }
272
+ }
273
+ exports.DogfoodController = DogfoodController;