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