yaver-feedback-react-native 0.9.2 → 0.9.4
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/README.md +102 -1
- package/dist/AuthOverlay.d.ts +1 -14
- package/dist/AuthOverlay.js +9 -62
- package/dist/Discovery.js +0 -6
- package/dist/DogfoodRuntime.d.ts +124 -0
- package/dist/DogfoodRuntime.js +273 -0
- package/dist/FeedbackModal.js +347 -61
- package/dist/LoginScreen.d.ts +1 -5
- package/dist/LoginScreen.js +2 -6
- package/dist/MachinePickerScreen.d.ts +1 -3
- package/dist/MachinePickerScreen.js +6 -18
- package/dist/P2PClient.d.ts +116 -3
- package/dist/P2PClient.js +273 -3
- package/dist/P2PDogfoodDriver.d.ts +12 -0
- package/dist/P2PDogfoodDriver.js +118 -0
- package/dist/PairDeviceModal.d.ts +2 -3
- package/dist/ShakeDetector.js +31 -17
- package/dist/VibeChatScreen.d.ts +11 -1
- package/dist/VibeChatScreen.js +200 -41
- package/dist/YaverFeedback.d.ts +34 -0
- package/dist/YaverFeedback.js +124 -19
- package/dist/YaverModeBadge.d.ts +22 -0
- package/dist/YaverModeBadge.js +219 -0
- package/dist/__tests__/AuthDevices.test.js +1 -48
- package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
- package/dist/__tests__/DogfoodRuntime.test.js +116 -0
- package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
- package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
- package/dist/__tests__/ReportIdentity.test.d.ts +25 -1
- package/dist/__tests__/ReportIdentity.test.js +34 -22
- package/dist/__tests__/ShakeDetectorWeb.test.d.ts +1 -0
- package/dist/__tests__/ShakeDetectorWeb.test.js +27 -0
- package/dist/__tests__/YaverFeedback.test.js +13 -3
- package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
- package/dist/__tests__/deviceDogfood.test.js +86 -0
- package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
- package/dist/__tests__/dogfoodPolicy.test.js +33 -0
- package/dist/_core/ansi.d.ts +117 -0
- package/dist/_core/ansi.js +468 -0
- package/dist/_core/ansi.test.d.ts +1 -0
- package/dist/_core/ansi.test.js +225 -0
- package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
- package/dist/_core/buildFeedbackPrompt.js +18 -72
- package/dist/_core/constants.d.ts +19 -6
- package/dist/_core/constants.js +20 -7
- package/dist/_core/device.d.ts +9 -23
- package/dist/_core/device.js +13 -28
- package/dist/_core/endpoints.d.ts +0 -7
- package/dist/_core/endpoints.js +0 -7
- package/dist/_core/index.d.ts +4 -0
- package/dist/_core/index.js +4 -0
- package/dist/_core/remoteless.d.ts +44 -0
- package/dist/_core/remoteless.js +75 -0
- package/dist/_core/trace.d.ts +47 -0
- package/dist/_core/trace.js +38 -0
- package/dist/_core/trace.test.d.ts +1 -0
- package/dist/_core/trace.test.js +60 -0
- package/dist/auth.d.ts +3 -60
- package/dist/auth.js +6 -88
- package/dist/deviceDogfood.d.ts +53 -0
- package/dist/deviceDogfood.js +137 -0
- package/dist/dogfoodPolicy.d.ts +25 -0
- package/dist/dogfoodPolicy.js +24 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +24 -8
- package/dist/reloadActions.js +2 -2
- package/dist/types.d.ts +50 -7
- package/package.json +12 -3
- package/src/AuthOverlay.tsx +20 -106
- package/src/Discovery.ts +0 -6
- package/src/DogfoodRuntime.ts +373 -0
- package/src/FeedbackModal.tsx +445 -67
- package/src/LoginScreen.tsx +2 -22
- package/src/MachinePickerScreen.tsx +6 -21
- package/src/P2PClient.ts +347 -4
- package/src/P2PDogfoodDriver.ts +132 -0
- package/src/PairDeviceModal.tsx +2 -3
- package/src/ShakeDetector.ts +31 -18
- package/src/VibeChatScreen.tsx +232 -42
- package/src/YaverFeedback.ts +133 -22
- package/src/YaverModeBadge.tsx +234 -0
- package/src/__tests__/AuthDevices.test.ts +1 -52
- package/src/__tests__/DogfoodRuntime.test.ts +135 -0
- package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
- package/src/__tests__/ReportIdentity.test.ts +36 -27
- package/src/__tests__/ShakeDetectorWeb.test.ts +30 -0
- package/src/__tests__/YaverFeedback.test.ts +15 -3
- package/src/__tests__/deviceDogfood.test.ts +77 -0
- package/src/__tests__/dogfoodPolicy.test.ts +34 -0
- package/src/_core/ansi.test.ts +250 -0
- package/src/_core/ansi.ts +475 -0
- package/src/_core/buildFeedbackPrompt.ts +18 -95
- package/src/_core/constants.ts +20 -6
- package/src/_core/device.ts +13 -30
- package/src/_core/endpoints.ts +0 -7
- package/src/_core/index.ts +4 -0
- package/src/_core/remoteless.ts +110 -0
- package/src/_core/trace.test.ts +58 -0
- package/src/_core/trace.ts +75 -0
- package/src/auth.ts +7 -156
- package/src/deviceDogfood.ts +171 -0
- package/src/dogfoodPolicy.ts +40 -0
- package/src/index.ts +40 -11
- package/src/reloadActions.ts +2 -2
- package/src/types.ts +45 -7
- package/dist/GuestOnboardingScreen.d.ts +0 -8
- package/dist/GuestOnboardingScreen.js +0 -282
- package/src/GuestOnboardingScreen.tsx +0 -307
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ Starting with **0.6.0**, the SDK login screen mirrors the Yaver mobile app: nati
|
|
|
60
60
|
Drop in `<FeedbackModal />` and the first time a user triggers feedback without a cached session, they get:
|
|
61
61
|
|
|
62
62
|
1. A full-screen login modal — Apple (native) / Google / GitHub / GitLab / Microsoft (in-app browser) / email + password (inline).
|
|
63
|
-
2. A picker of the
|
|
63
|
+
2. A picker of the authenticated account's own dev boxes.
|
|
64
64
|
|
|
65
65
|
Both screens persist their selection to `AsyncStorage`, so subsequent launches reconnect silently.
|
|
66
66
|
|
|
@@ -89,6 +89,107 @@ function App() {
|
|
|
89
89
|
|
|
90
90
|
Shake your phone to open the feedback modal. On mobile the quick-access icon stays hidden until that first shake, then remains tappable for the rest of the session unless the user hides it. For launch, the sheet exposes three actions only: hot reload, vibing, and screenshot & fix.
|
|
91
91
|
|
|
92
|
+
### Account-scoped Dogfood mode
|
|
93
|
+
|
|
94
|
+
An app can send only explicitly approved app accounts directly into Vibing,
|
|
95
|
+
while everyone else keeps the normal Feedback sheet:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
YaverFeedback.init({
|
|
99
|
+
dogfood: {
|
|
100
|
+
enabled: true,
|
|
101
|
+
currentAccountId: session.user.id,
|
|
102
|
+
accountIds: ['approved-app-account-id'],
|
|
103
|
+
label: 'My App',
|
|
104
|
+
onExit: () => setDogfoodPreference(false),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The match is exact and fails closed when the current account is absent. The
|
|
110
|
+
small `Y` identifies Dogfood mode; tapping it offers a confirmed exit back to
|
|
111
|
+
Feedback mode. This allowlist controls SDK presentation only—tasks and reloads
|
|
112
|
+
still require the user's normal Yaver authentication.
|
|
113
|
+
|
|
114
|
+
### Device-enrolled Dogfood for apps without their own OAuth/backend
|
|
115
|
+
|
|
116
|
+
Mount `FeedbackModal`, then call the reusable wizard from the host app's
|
|
117
|
+
Settings button:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
YaverFeedback.beginDogfoodOnboarding({
|
|
121
|
+
appId: 'io.example.app',
|
|
122
|
+
label: 'Example',
|
|
123
|
+
projectName: 'example',
|
|
124
|
+
framework: 'expo',
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The wizard runs Yaver OAuth first, then machine, coding runner/model, project,
|
|
129
|
+
and Browser/Hermes/WebRTC selection. It renders raw build logs and exposes the
|
|
130
|
+
ready preview. The app also creates a random installation ID and an Ed25519 key
|
|
131
|
+
inside SecureStore. The ID is a public handle, not a credential: enrollment
|
|
132
|
+
and each short session require a server challenge signed by the private key.
|
|
133
|
+
An owner approves, cancels, or revokes the installation from Yaver Settings or
|
|
134
|
+
MCP. Re-registering rotates the key/ID, preserves only the logical local slot,
|
|
135
|
+
and supersedes that slot's prior active generation without disabling another
|
|
136
|
+
phone.
|
|
137
|
+
|
|
138
|
+
Install `expo-secure-store` and `expo-crypto` in Expo hosts. Bare React Native
|
|
139
|
+
hosts may provide a `secureStore` implementation. Runtime/build controls still
|
|
140
|
+
require full Yaver OAuth; the account-free installation token defaults to
|
|
141
|
+
`feedback` + `blackbox` and cannot be rotated into a long-lived owner token.
|
|
142
|
+
|
|
143
|
+
### Embeddable Dogfood runtime
|
|
144
|
+
|
|
145
|
+
App owners can also embed the actual source → dev-server → render lifecycle.
|
|
146
|
+
`DogfoodController` is deliberately headless: constructing it does nothing;
|
|
147
|
+
your own button calls `trigger()`, your preview renders `result.url`, and your
|
|
148
|
+
console renders `snapshot.logs`. Retry and partial-session cleanup are handled
|
|
149
|
+
by the controller.
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import {
|
|
153
|
+
DogfoodController,
|
|
154
|
+
P2PClient,
|
|
155
|
+
createP2PDogfoodDriver,
|
|
156
|
+
} from 'yaver-feedback-react-native';
|
|
157
|
+
|
|
158
|
+
const client = new P2PClient(agentUrl, signedInUserToken);
|
|
159
|
+
const dogfood = new DogfoodController(
|
|
160
|
+
{
|
|
161
|
+
name: 'My App',
|
|
162
|
+
workDir: '/workspace/my-app',
|
|
163
|
+
framework: 'expo', // also react-native, flutter, vite, next
|
|
164
|
+
lane: 'browser', // Hermes is Expo/RN only; Flutter uses browser/WebRTC
|
|
165
|
+
},
|
|
166
|
+
createP2PDogfoodDriver(client),
|
|
167
|
+
{
|
|
168
|
+
onChange(snapshot) {
|
|
169
|
+
setPhase(snapshot.phase);
|
|
170
|
+
setConsoleText(snapshot.logs.map((line) => line.text).join('\n'));
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Explicit user intent: call only from your Dogfood button.
|
|
176
|
+
const onDogfoodPress = () => dogfood.trigger().then(({ url }) => setPreviewUrl(url));
|
|
177
|
+
const onRetryPress = () => dogfood.retry();
|
|
178
|
+
const onLeaveScreen = () => dogfood.stop();
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The default P2P driver calls the same `/dev/start`, `/dev/status`, `/dev/stop`,
|
|
182
|
+
and `/dev/events` endpoints as Yaver's Projects browser lane. Package-manager
|
|
183
|
+
and compiler output—including `$ npm install`, Expo/Metro, and Flutter web
|
|
184
|
+
output—arrives uncollapsed in `snapshot.logs`. Starting tools requires the
|
|
185
|
+
normal signed-in Yaver user token; a narrow feedback-only SDK token cannot
|
|
186
|
+
spawn processes.
|
|
187
|
+
|
|
188
|
+
For Yaver itself, installing `yaver-cli` supplies these agent endpoints. The
|
|
189
|
+
native Yaver app checks the selected box for a real Yaver checkout and offers
|
|
190
|
+
`Clone Yaver source` when it is absent; cloning still occurs only after that
|
|
191
|
+
explicit tap.
|
|
192
|
+
|
|
92
193
|
### Quick Icon Styling
|
|
93
194
|
|
|
94
195
|
The quick icon is configurable at compile time through `YaverFeedback.init(...)`, with render-time overrides still available through `<QuickActionIcon />` props when needed. If you do nothing, the SDK uses a high-visibility orange bubble by default so it stays distinct from the blue/indigo FAB styling many mobile apps already have.
|
package/dist/AuthOverlay.d.ts
CHANGED
|
@@ -1,16 +1,3 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
/**
|
|
3
|
-
* Presentation layer for the SDK's auth + machine-picker modals.
|
|
4
|
-
*
|
|
5
|
-
* Mounts automatically inside `<FeedbackModal />`, so consumers of the SDK
|
|
6
|
-
* get in-app login with no extra wiring. It listens for events emitted by
|
|
7
|
-
* `YaverFeedback.showLogin()` / `showMachinePicker()`:
|
|
8
|
-
*
|
|
9
|
-
* yaverFeedback:startLogin → show login modal
|
|
10
|
-
* yaverFeedback:startMachinePicker → show machine picker
|
|
11
|
-
*
|
|
12
|
-
* The overlay closes itself once login/pick succeeds, then re-emits
|
|
13
|
-
* `yaverFeedback:startReport` so the user continues straight into the
|
|
14
|
-
* feedback flow they originally triggered.
|
|
15
|
-
*/
|
|
2
|
+
/** Owner sign-in and owner-machine selection for the Feedback SDK. */
|
|
16
3
|
export declare const AuthOverlay: React.FC;
|
package/dist/AuthOverlay.js
CHANGED
|
@@ -38,74 +38,47 @@ const react_1 = __importStar(require("react"));
|
|
|
38
38
|
const react_native_1 = require("react-native");
|
|
39
39
|
const LoginScreen_1 = require("./LoginScreen");
|
|
40
40
|
const MachinePickerScreen_1 = require("./MachinePickerScreen");
|
|
41
|
-
const GuestOnboardingScreen_1 = require("./GuestOnboardingScreen");
|
|
42
41
|
const YaverFeedback_1 = require("./YaverFeedback");
|
|
43
42
|
const auth_1 = require("./auth");
|
|
44
|
-
/**
|
|
45
|
-
* Presentation layer for the SDK's auth + machine-picker modals.
|
|
46
|
-
*
|
|
47
|
-
* Mounts automatically inside `<FeedbackModal />`, so consumers of the SDK
|
|
48
|
-
* get in-app login with no extra wiring. It listens for events emitted by
|
|
49
|
-
* `YaverFeedback.showLogin()` / `showMachinePicker()`:
|
|
50
|
-
*
|
|
51
|
-
* yaverFeedback:startLogin → show login modal
|
|
52
|
-
* yaverFeedback:startMachinePicker → show machine picker
|
|
53
|
-
*
|
|
54
|
-
* The overlay closes itself once login/pick succeeds, then re-emits
|
|
55
|
-
* `yaverFeedback:startReport` so the user continues straight into the
|
|
56
|
-
* feedback flow they originally triggered.
|
|
57
|
-
*/
|
|
43
|
+
/** Owner sign-in and owner-machine selection for the Feedback SDK. */
|
|
58
44
|
const AuthOverlay = () => {
|
|
59
45
|
const [loginVisible, setLoginVisible] = (0, react_1.useState)(false);
|
|
60
|
-
const [guestVisible, setGuestVisible] = (0, react_1.useState)(false);
|
|
61
46
|
const [pickerVisible, setPickerVisible] = (0, react_1.useState)(false);
|
|
62
47
|
const [token, setToken] = (0, react_1.useState)(null);
|
|
63
|
-
const [pendingInviteCode, setPendingInviteCode] = (0, react_1.useState)(null);
|
|
64
48
|
const activeOverlayRef = (0, react_1.useRef)('none');
|
|
65
49
|
const openLogin = (0, react_1.useCallback)(() => {
|
|
66
50
|
activeOverlayRef.current = 'login';
|
|
67
|
-
setGuestVisible(false);
|
|
68
51
|
setPickerVisible(false);
|
|
69
52
|
setLoginVisible(true);
|
|
70
53
|
}, []);
|
|
71
|
-
const openGuest = (0, react_1.useCallback)(() => {
|
|
72
|
-
activeOverlayRef.current = 'guest';
|
|
73
|
-
setLoginVisible(false);
|
|
74
|
-
setPickerVisible(false);
|
|
75
|
-
setGuestVisible(true);
|
|
76
|
-
}, []);
|
|
77
54
|
const openPicker = (0, react_1.useCallback)(() => {
|
|
78
55
|
activeOverlayRef.current = 'picker';
|
|
79
56
|
setLoginVisible(false);
|
|
80
|
-
setGuestVisible(false);
|
|
81
57
|
setPickerVisible(true);
|
|
82
58
|
}, []);
|
|
83
59
|
const closeAll = (0, react_1.useCallback)(() => {
|
|
84
60
|
activeOverlayRef.current = 'none';
|
|
85
61
|
setLoginVisible(false);
|
|
86
|
-
setGuestVisible(false);
|
|
87
62
|
setPickerVisible(false);
|
|
88
63
|
}, []);
|
|
89
64
|
(0, react_1.useEffect)(() => {
|
|
90
65
|
let mounted = true;
|
|
91
|
-
(
|
|
92
|
-
const cached = await (0, auth_1.getToken)();
|
|
66
|
+
void (0, auth_1.getToken)().then((cached) => {
|
|
93
67
|
if (mounted && cached)
|
|
94
68
|
setToken(cached);
|
|
95
|
-
})
|
|
69
|
+
});
|
|
96
70
|
const loginSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startLogin', () => {
|
|
97
|
-
if (activeOverlayRef.current
|
|
98
|
-
|
|
99
|
-
openLogin();
|
|
71
|
+
if (activeOverlayRef.current === 'none')
|
|
72
|
+
openLogin();
|
|
100
73
|
});
|
|
101
74
|
const pickerSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startMachinePicker', async () => {
|
|
102
75
|
if (activeOverlayRef.current !== 'none')
|
|
103
76
|
return;
|
|
104
77
|
const cached = await (0, auth_1.getToken)();
|
|
105
|
-
if (cached)
|
|
78
|
+
if (cached) {
|
|
106
79
|
setToken(cached);
|
|
107
|
-
if (cached)
|
|
108
80
|
openPicker();
|
|
81
|
+
}
|
|
109
82
|
});
|
|
110
83
|
return () => {
|
|
111
84
|
mounted = false;
|
|
@@ -113,49 +86,23 @@ const AuthOverlay = () => {
|
|
|
113
86
|
pickerSub.remove();
|
|
114
87
|
};
|
|
115
88
|
}, [openLogin, openPicker]);
|
|
116
|
-
const
|
|
89
|
+
const handleLoggedIn = async (newToken) => {
|
|
117
90
|
setToken(newToken);
|
|
118
91
|
await YaverFeedback_1.YaverFeedback.setAuthToken(newToken);
|
|
119
|
-
const devices = await (0, auth_1.listReachableDevices)(newToken).catch(() => ({ owned: [], shared: [] }));
|
|
120
|
-
const cleanedInviteCode = (inviteCode ?? '').trim().toUpperCase();
|
|
121
|
-
if (cleanedInviteCode) {
|
|
122
|
-
setPendingInviteCode(cleanedInviteCode);
|
|
123
|
-
openGuest();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (devices.owned.length === 0 && devices.shared.length === 0) {
|
|
127
|
-
openGuest();
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
92
|
openPicker();
|
|
131
93
|
};
|
|
132
|
-
const handleLoggedIn = async (newToken, opts) => {
|
|
133
|
-
await continueAfterAuth(newToken, opts?.inviteCode);
|
|
134
|
-
};
|
|
135
94
|
const handleDevicePicked = async (device) => {
|
|
136
95
|
await YaverFeedback_1.YaverFeedback.setPreferredDevice(device.deviceId);
|
|
137
96
|
closeAll();
|
|
138
|
-
// Continue straight into the feedback flow the user originally triggered.
|
|
139
97
|
react_native_1.DeviceEventEmitter.emit('yaverFeedback:startReport');
|
|
140
98
|
};
|
|
141
99
|
return (<>
|
|
142
100
|
<react_native_1.Modal visible={loginVisible} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
|
|
143
|
-
<LoginScreen_1.YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={closeAll}
|
|
101
|
+
<LoginScreen_1.YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={closeAll}/>
|
|
144
102
|
</react_native_1.Modal>
|
|
145
|
-
|
|
146
103
|
<react_native_1.Modal visible={pickerVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
|
|
147
104
|
{token && (<MachinePickerScreen_1.YaverMachinePickerScreen token={token} currentDeviceId={YaverFeedback_1.YaverFeedback.getConfig()?.preferredDeviceId} onPick={handleDevicePicked} onCancel={closeAll}/>)}
|
|
148
105
|
</react_native_1.Modal>
|
|
149
|
-
|
|
150
|
-
<react_native_1.Modal visible={guestVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
|
|
151
|
-
{token && (<GuestOnboardingScreen_1.YaverGuestOnboardingScreen token={token} initialInviteCode={pendingInviteCode ?? YaverFeedback_1.YaverFeedback.getConfig()?.guestInviteCode} onContinue={() => {
|
|
152
|
-
setPendingInviteCode(null);
|
|
153
|
-
openPicker();
|
|
154
|
-
}} onCancel={() => {
|
|
155
|
-
setPendingInviteCode(null);
|
|
156
|
-
openPicker();
|
|
157
|
-
}}/>)}
|
|
158
|
-
</react_native_1.Modal>
|
|
159
106
|
</>);
|
|
160
107
|
};
|
|
161
108
|
exports.AuthOverlay = AuthOverlay;
|
package/dist/Discovery.js
CHANGED
|
@@ -179,12 +179,6 @@ class YaverDiscovery {
|
|
|
179
179
|
needsAuth: !!d.needsAuth,
|
|
180
180
|
runnerDown: !!d.runnerDown,
|
|
181
181
|
lastHeartbeat: d.lastHeartbeat ?? 0,
|
|
182
|
-
isGuest: !!d.isGuest,
|
|
183
|
-
hostUserId: d.hostUserId,
|
|
184
|
-
hostName: d.hostName,
|
|
185
|
-
hostEmail: d.hostEmail,
|
|
186
|
-
hostUserIdString: d.hostUserIdString,
|
|
187
|
-
accessScope: d.accessScope ?? 'owner',
|
|
188
182
|
quicHost: d.quicHost ?? d.host ?? '',
|
|
189
183
|
quicPort: d.quicPort ?? 0,
|
|
190
184
|
httpPort: d.httpPort ?? d.quicPort,
|
|
@@ -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;
|