@rool-dev/extension 0.3.5
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 +458 -0
- package/dist/cli/build-pipeline.d.ts +18 -0
- package/dist/cli/build-pipeline.d.ts.map +1 -0
- package/dist/cli/build-pipeline.js +160 -0
- package/dist/cli/build.d.ts +9 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +17 -0
- package/dist/cli/dev.d.ts +10 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +257 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +34 -0
- package/dist/cli/init.d.ts +8 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +113 -0
- package/dist/cli/publish.d.ts +9 -0
- package/dist/cli/publish.d.ts.map +1 -0
- package/dist/cli/publish.js +65 -0
- package/dist/cli/vite-utils.d.ts +23 -0
- package/dist/cli/vite-utils.d.ts.map +1 -0
- package/dist/cli/vite-utils.js +105 -0
- package/dist/client.d.ts +139 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +360 -0
- package/dist/dev/AppGrid.svelte +246 -0
- package/dist/dev/AppGrid.svelte.d.ts +14 -0
- package/dist/dev/AppGrid.svelte.d.ts.map +1 -0
- package/dist/dev/DevHostController.d.ts +85 -0
- package/dist/dev/DevHostController.d.ts.map +1 -0
- package/dist/dev/DevHostController.js +429 -0
- package/dist/dev/HostShell.svelte +119 -0
- package/dist/dev/HostShell.svelte.d.ts +11 -0
- package/dist/dev/HostShell.svelte.d.ts.map +1 -0
- package/dist/dev/Sidebar.svelte +290 -0
- package/dist/dev/Sidebar.svelte.d.ts +22 -0
- package/dist/dev/Sidebar.svelte.d.ts.map +1 -0
- package/dist/dev/TabBar.svelte +83 -0
- package/dist/dev/TabBar.svelte.d.ts +14 -0
- package/dist/dev/TabBar.svelte.d.ts.map +1 -0
- package/dist/dev/app.css +1 -0
- package/dist/dev/host-shell.d.ts +8 -0
- package/dist/dev/host-shell.d.ts.map +1 -0
- package/dist/dev/host-shell.js +15282 -0
- package/dist/dev/host-shell.js.map +1 -0
- package/dist/dev/vite-env.d.ts +4 -0
- package/dist/host.d.ts +55 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +203 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/manifest.d.ts +40 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +11 -0
- package/dist/protocol.d.ts +48 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +14 -0
- package/dist/reactive.svelte.d.ts +150 -0
- package/dist/reactive.svelte.d.ts.map +1 -0
- package/dist/reactive.svelte.js +362 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/package.json +79 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DevHostController — business logic for the dev host shell.
|
|
3
|
+
*
|
|
4
|
+
* Owns the RoolClient lifecycle, space management, channel-per-extension management,
|
|
5
|
+
* bridge hosting, and published extension management. The Svelte component is a thin
|
|
6
|
+
* view layer that reads this controller's state and calls its methods.
|
|
7
|
+
*
|
|
8
|
+
* The controller is self-sufficient: it manages the full lifecycle including
|
|
9
|
+
* DOM flush (via injected tick) and bridge binding. Svelte components can call
|
|
10
|
+
* controller methods directly without needing wrappers.
|
|
11
|
+
*/
|
|
12
|
+
import { RoolClient } from '@rool-dev/sdk';
|
|
13
|
+
import { createBridgeHost } from '../host.js';
|
|
14
|
+
import { ENV_URLS } from '../manifest.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// localStorage helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
function storageGet(key) {
|
|
19
|
+
try {
|
|
20
|
+
return localStorage.getItem(key);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function storageSet(key, value) {
|
|
27
|
+
try {
|
|
28
|
+
if (value === null)
|
|
29
|
+
localStorage.removeItem(key);
|
|
30
|
+
else
|
|
31
|
+
localStorage.setItem(key, value);
|
|
32
|
+
}
|
|
33
|
+
catch { /* ignore */ }
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// DevHostController
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
export class DevHostController {
|
|
39
|
+
// --- Config (immutable after construction) ---
|
|
40
|
+
channelId;
|
|
41
|
+
extensionUrl;
|
|
42
|
+
manifest;
|
|
43
|
+
manifestError;
|
|
44
|
+
// --- SDK client ---
|
|
45
|
+
client;
|
|
46
|
+
// --- Observable state (Svelte component mirrors these via $state) ---
|
|
47
|
+
spaces = [];
|
|
48
|
+
currentSpaceId = null;
|
|
49
|
+
statusText = 'Initializing...';
|
|
50
|
+
statusState = 'off';
|
|
51
|
+
placeholderText = 'Authenticating...';
|
|
52
|
+
env;
|
|
53
|
+
publishedExtensions = [];
|
|
54
|
+
installedExtensionIds = [];
|
|
55
|
+
sidebarCollapsed = false;
|
|
56
|
+
publishState = 'idle';
|
|
57
|
+
publishMessage = null;
|
|
58
|
+
publishUrl = null;
|
|
59
|
+
// --- Per-tab state (imperative, not rendered directly) ---
|
|
60
|
+
channels = {};
|
|
61
|
+
iframeEls = {};
|
|
62
|
+
bridgeHosts = {};
|
|
63
|
+
// --- Dependencies ---
|
|
64
|
+
_onChange;
|
|
65
|
+
_tick;
|
|
66
|
+
// --- Storage keys ---
|
|
67
|
+
_spaceKey;
|
|
68
|
+
constructor(options, onChange, tick) {
|
|
69
|
+
this.channelId = options.channelId;
|
|
70
|
+
this.extensionUrl = options.extensionUrl;
|
|
71
|
+
this.manifest = options.manifest;
|
|
72
|
+
this.manifestError = options.manifestError;
|
|
73
|
+
this._onChange = onChange;
|
|
74
|
+
this._tick = tick;
|
|
75
|
+
this._spaceKey = `rool-devhost:${options.channelId}:space`;
|
|
76
|
+
// Restore persisted state
|
|
77
|
+
this.env = this._getSavedEnv();
|
|
78
|
+
this.sidebarCollapsed = storageGet('rool-devhost:collapsed') === 'true';
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Derived
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
get tabs() {
|
|
84
|
+
const localTab = {
|
|
85
|
+
id: 'local',
|
|
86
|
+
name: this.manifest?.name ?? 'Local',
|
|
87
|
+
url: this.extensionUrl,
|
|
88
|
+
isLocal: true,
|
|
89
|
+
};
|
|
90
|
+
const extensionTabs = this.installedExtensionIds
|
|
91
|
+
.map((id) => {
|
|
92
|
+
const ch = this.channels[id];
|
|
93
|
+
if (!ch?.extensionUrl)
|
|
94
|
+
return null;
|
|
95
|
+
return {
|
|
96
|
+
id,
|
|
97
|
+
name: ch.channelName ?? id,
|
|
98
|
+
url: ch.extensionUrl,
|
|
99
|
+
isLocal: false,
|
|
100
|
+
};
|
|
101
|
+
})
|
|
102
|
+
.filter((t) => t !== null);
|
|
103
|
+
return [localTab, ...extensionTabs];
|
|
104
|
+
}
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Bootstrap
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
async boot() {
|
|
109
|
+
const urls = ENV_URLS[this.env];
|
|
110
|
+
this.client = new RoolClient({ baseUrl: urls.baseUrl, authUrl: urls.authUrl });
|
|
111
|
+
const authenticated = await this.client.initialize();
|
|
112
|
+
if (!authenticated) {
|
|
113
|
+
this.placeholderText = 'Redirecting to login...';
|
|
114
|
+
this.statusText = 'Authenticating...';
|
|
115
|
+
this.statusState = 'loading';
|
|
116
|
+
this._onChange();
|
|
117
|
+
this.client.login('Extension Dev Host');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.placeholderText = 'Loading spaces...';
|
|
121
|
+
this.statusText = 'Loading spaces...';
|
|
122
|
+
this.statusState = 'loading';
|
|
123
|
+
this._onChange();
|
|
124
|
+
const [spaceList, extensionList] = await Promise.all([
|
|
125
|
+
this.client.listSpaces(),
|
|
126
|
+
this.client.listExtensions().catch(() => []),
|
|
127
|
+
]);
|
|
128
|
+
this.spaces = spaceList;
|
|
129
|
+
this.publishedExtensions = extensionList;
|
|
130
|
+
this.client.on('spaceAdded', (space) => {
|
|
131
|
+
if (!this.spaces.some((s) => s.id === space.id)) {
|
|
132
|
+
this.spaces = [...this.spaces, space];
|
|
133
|
+
this._onChange();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
this.client.on('spaceRemoved', (id) => {
|
|
137
|
+
this.spaces = this.spaces.filter((s) => s.id !== id);
|
|
138
|
+
if (this.currentSpaceId === id) {
|
|
139
|
+
this.currentSpaceId = null;
|
|
140
|
+
this.statusText = 'Disconnected';
|
|
141
|
+
this.statusState = 'off';
|
|
142
|
+
}
|
|
143
|
+
this._onChange();
|
|
144
|
+
});
|
|
145
|
+
this.client.on('spaceRenamed', (id, name) => {
|
|
146
|
+
this.spaces = this.spaces.map((s) => (s.id === id ? { ...s, name } : s));
|
|
147
|
+
this._onChange();
|
|
148
|
+
});
|
|
149
|
+
this.statusText = 'Ready';
|
|
150
|
+
this.statusState = 'off';
|
|
151
|
+
const savedSpace = storageGet(this._spaceKey);
|
|
152
|
+
if (savedSpace && this.spaces.some((s) => s.id === savedSpace)) {
|
|
153
|
+
await this.selectSpace(savedSpace);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this.placeholderText = 'Select a space to load the extension';
|
|
157
|
+
this._onChange();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Space selection
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
async selectSpace(spaceId) {
|
|
164
|
+
this._destroyAllBridgesAndChannels();
|
|
165
|
+
this.currentSpaceId = spaceId;
|
|
166
|
+
storageSet(this._spaceKey, spaceId);
|
|
167
|
+
this.statusText = 'Opening channels...';
|
|
168
|
+
this.statusState = 'loading';
|
|
169
|
+
this.placeholderText = 'Opening channels...';
|
|
170
|
+
this._onChange();
|
|
171
|
+
try {
|
|
172
|
+
// Open the local extension's channel
|
|
173
|
+
const localChannel = await this.client.openChannel(spaceId, this.channelId);
|
|
174
|
+
this.channels['local'] = localChannel;
|
|
175
|
+
// Apply manifest settings to the local channel
|
|
176
|
+
await this._syncManifest(localChannel, this.manifest);
|
|
177
|
+
// Discover installed extensions: channels with an extensionUrl
|
|
178
|
+
const space = await this.client.openSpace(spaceId);
|
|
179
|
+
const spaceChannels = space.getChannels();
|
|
180
|
+
this.installedExtensionIds = spaceChannels
|
|
181
|
+
.filter((ch) => ch.extensionUrl && ch.id !== this.channelId)
|
|
182
|
+
.map((ch) => ch.id);
|
|
183
|
+
// Open channels for each installed extension (server already applied manifest)
|
|
184
|
+
for (const extId of this.installedExtensionIds) {
|
|
185
|
+
try {
|
|
186
|
+
const ch = await this.client.openChannel(spaceId, extId);
|
|
187
|
+
this.channels[extId] = ch;
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
console.error(`Failed to open channel for extension ${extId}:`, e);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Show iframes, wait for DOM to mount them, then bind bridges
|
|
194
|
+
this.placeholderText = null;
|
|
195
|
+
this._onChange();
|
|
196
|
+
await this._tick();
|
|
197
|
+
this._bindAllBridges();
|
|
198
|
+
const spaceName = this.spaces.find((s) => s.id === this.currentSpaceId)?.name ?? spaceId;
|
|
199
|
+
this.statusText = `Connected \u2014 ${spaceName}`;
|
|
200
|
+
this.statusState = 'ok';
|
|
201
|
+
this._onChange();
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
console.error('Failed to open channel:', e);
|
|
205
|
+
this.placeholderText = `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
206
|
+
this.statusText = 'Error';
|
|
207
|
+
this.statusState = 'off';
|
|
208
|
+
this._onChange();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Extension installation / removal
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
/**
|
|
215
|
+
* Install an extension in the current space.
|
|
216
|
+
*
|
|
217
|
+
* Opens the channel first, THEN adds the tab. This ensures the channel
|
|
218
|
+
* exists when the iframe mounts so registerIframe → _bindBridge can
|
|
219
|
+
* connect the bridge before the extension sends its init message.
|
|
220
|
+
*/
|
|
221
|
+
async installExtension(extensionId) {
|
|
222
|
+
if (!this.currentSpaceId)
|
|
223
|
+
return;
|
|
224
|
+
if (this.installedExtensionIds.includes(extensionId))
|
|
225
|
+
return;
|
|
226
|
+
try {
|
|
227
|
+
// Step 1: install extension (server applies manifest: name, systemInstruction, collections)
|
|
228
|
+
const channelId = await this.client.installExtension(this.currentSpaceId, extensionId);
|
|
229
|
+
// Step 2: open channel for live subscription
|
|
230
|
+
const ch = await this.client.openChannel(this.currentSpaceId, channelId);
|
|
231
|
+
this.channels[extensionId] = ch;
|
|
232
|
+
// Step 3: add the card, flush DOM, bind bridge
|
|
233
|
+
this.installedExtensionIds = [...this.installedExtensionIds, extensionId];
|
|
234
|
+
this._onChange();
|
|
235
|
+
await this._tick();
|
|
236
|
+
this._bindBridge(extensionId);
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
console.error(`Failed to install extension ${extensionId}:`, e);
|
|
240
|
+
this.installedExtensionIds = this.installedExtensionIds.filter((id) => id !== extensionId);
|
|
241
|
+
this._onChange();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Uninstall an extension from the current space.
|
|
246
|
+
* Deletes the channel and removes the card.
|
|
247
|
+
*/
|
|
248
|
+
removeExtension(extensionId) {
|
|
249
|
+
this._destroyTab(extensionId);
|
|
250
|
+
this.installedExtensionIds = this.installedExtensionIds.filter((id) => id !== extensionId);
|
|
251
|
+
this._onChange();
|
|
252
|
+
// Delete the channel in the background (fire-and-forget)
|
|
253
|
+
if (this.currentSpaceId) {
|
|
254
|
+
this.client.deleteChannel(this.currentSpaceId, extensionId).catch((e) => {
|
|
255
|
+
console.error(`Failed to delete channel for extension ${extensionId}:`, e);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Publishing
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
async publish() {
|
|
263
|
+
if (!this.manifest) {
|
|
264
|
+
this.publishState = 'error';
|
|
265
|
+
this.publishMessage = 'No valid manifest found';
|
|
266
|
+
this._onChange();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.publishState = 'building';
|
|
270
|
+
this.publishMessage = null;
|
|
271
|
+
this.publishUrl = null;
|
|
272
|
+
this._onChange();
|
|
273
|
+
try {
|
|
274
|
+
// Step 1: trigger server-side Vite build + zip
|
|
275
|
+
const buildRes = await fetch('/__rool-host/publish', { method: 'POST' });
|
|
276
|
+
if (!buildRes.ok) {
|
|
277
|
+
const body = await buildRes.json().catch(() => ({ error: 'Build failed' }));
|
|
278
|
+
throw new Error(body.error || 'Build failed');
|
|
279
|
+
}
|
|
280
|
+
const zipBlob = await buildRes.blob();
|
|
281
|
+
// Step 2: publish via SDK
|
|
282
|
+
this.publishState = 'uploading';
|
|
283
|
+
this._onChange();
|
|
284
|
+
const result = await this.client.publishExtension(this.manifest.id, {
|
|
285
|
+
bundle: zipBlob,
|
|
286
|
+
});
|
|
287
|
+
// Step 3: update published extensions list
|
|
288
|
+
const existingIdx = this.publishedExtensions.findIndex((a) => a.extensionId === result.extensionId);
|
|
289
|
+
if (existingIdx >= 0) {
|
|
290
|
+
this.publishedExtensions = [
|
|
291
|
+
...this.publishedExtensions.slice(0, existingIdx),
|
|
292
|
+
result,
|
|
293
|
+
...this.publishedExtensions.slice(existingIdx + 1),
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
this.publishedExtensions = [...this.publishedExtensions, result];
|
|
298
|
+
}
|
|
299
|
+
this.publishState = 'done';
|
|
300
|
+
this.publishUrl = result.url;
|
|
301
|
+
this._onChange();
|
|
302
|
+
// Auto-clear success state after 5 seconds
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
if (this.publishState === 'done') {
|
|
305
|
+
this.publishState = 'idle';
|
|
306
|
+
this.publishUrl = null;
|
|
307
|
+
this._onChange();
|
|
308
|
+
}
|
|
309
|
+
}, 5000);
|
|
310
|
+
}
|
|
311
|
+
catch (e) {
|
|
312
|
+
this.publishState = 'error';
|
|
313
|
+
this.publishMessage = e instanceof Error ? e.message : String(e);
|
|
314
|
+
this._onChange();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// Environment switching
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
async switchEnv(newEnv) {
|
|
321
|
+
if (newEnv === this.env)
|
|
322
|
+
return;
|
|
323
|
+
this.env = newEnv;
|
|
324
|
+
storageSet('rool-devhost:env', newEnv);
|
|
325
|
+
this._destroyAllBridgesAndChannels();
|
|
326
|
+
this.currentSpaceId = null;
|
|
327
|
+
this.spaces = [];
|
|
328
|
+
this.publishedExtensions = [];
|
|
329
|
+
this.installedExtensionIds = [];
|
|
330
|
+
this._onChange();
|
|
331
|
+
await this.boot();
|
|
332
|
+
}
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Sidebar
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
toggleSidebar() {
|
|
337
|
+
this.sidebarCollapsed = !this.sidebarCollapsed;
|
|
338
|
+
storageSet('rool-devhost:collapsed', String(this.sidebarCollapsed));
|
|
339
|
+
this._onChange();
|
|
340
|
+
}
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Iframe registration (called by Svelte action)
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
registerIframe(tabId, el) {
|
|
345
|
+
this.iframeEls[tabId] = el;
|
|
346
|
+
this._bindBridge(tabId);
|
|
347
|
+
}
|
|
348
|
+
unregisterIframe(tabId) {
|
|
349
|
+
delete this.iframeEls[tabId];
|
|
350
|
+
}
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// Cleanup
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
logout() {
|
|
355
|
+
this.client.logout();
|
|
356
|
+
window.location.reload();
|
|
357
|
+
}
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// Private helpers
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
_bindBridge(tabId) {
|
|
362
|
+
const el = this.iframeEls[tabId];
|
|
363
|
+
const ch = this.channels[tabId];
|
|
364
|
+
if (el && ch && !this.bridgeHosts[tabId]) {
|
|
365
|
+
this.bridgeHosts[tabId] = createBridgeHost({ channel: ch, iframe: el });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
_bindAllBridges() {
|
|
369
|
+
for (const tab of this.tabs) {
|
|
370
|
+
this._bindBridge(tab.id);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
_destroyTab(tabId) {
|
|
374
|
+
this.bridgeHosts[tabId]?.destroy();
|
|
375
|
+
delete this.bridgeHosts[tabId];
|
|
376
|
+
this.channels[tabId]?.close();
|
|
377
|
+
delete this.channels[tabId];
|
|
378
|
+
delete this.iframeEls[tabId];
|
|
379
|
+
}
|
|
380
|
+
_destroyAllBridgesAndChannels() {
|
|
381
|
+
for (const host of Object.values(this.bridgeHosts)) {
|
|
382
|
+
host.destroy();
|
|
383
|
+
}
|
|
384
|
+
for (const ch of Object.values(this.channels)) {
|
|
385
|
+
ch.close();
|
|
386
|
+
}
|
|
387
|
+
this.bridgeHosts = {};
|
|
388
|
+
this.channels = {};
|
|
389
|
+
this.iframeEls = {};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Idempotently sync a manifest's settings (name, system instruction, collections)
|
|
393
|
+
* onto a channel. Safe to call every time the extension is opened.
|
|
394
|
+
*/
|
|
395
|
+
async _syncManifest(channel, manifest) {
|
|
396
|
+
if (!manifest)
|
|
397
|
+
return;
|
|
398
|
+
if (channel.channelName !== manifest.name) {
|
|
399
|
+
await channel.rename(manifest.name);
|
|
400
|
+
}
|
|
401
|
+
const targetInstruction = manifest.systemInstruction ?? null;
|
|
402
|
+
const currentInstruction = channel.getSystemInstruction() ?? null;
|
|
403
|
+
if (currentInstruction !== targetInstruction) {
|
|
404
|
+
await channel.setSystemInstruction(targetInstruction);
|
|
405
|
+
}
|
|
406
|
+
const currentSchema = channel.getSchema();
|
|
407
|
+
const syncCollections = async (colls) => {
|
|
408
|
+
for (const [name, fields] of Object.entries(colls)) {
|
|
409
|
+
if (name in currentSchema) {
|
|
410
|
+
await channel.alterCollection(name, fields);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
await channel.createCollection(name, fields);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const { write: w, read: r } = manifest.collections;
|
|
418
|
+
if (w && w !== '*')
|
|
419
|
+
await syncCollections(w);
|
|
420
|
+
if (r && r !== '*')
|
|
421
|
+
await syncCollections(r);
|
|
422
|
+
}
|
|
423
|
+
_getSavedEnv() {
|
|
424
|
+
const saved = storageGet('rool-devhost:env');
|
|
425
|
+
if (saved === 'local' || saved === 'dev' || saved === 'prod')
|
|
426
|
+
return saved;
|
|
427
|
+
return 'prod';
|
|
428
|
+
}
|
|
429
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, tick } from 'svelte';
|
|
3
|
+
import { DevHostController } from './DevHostController.js';
|
|
4
|
+
import type { ExtensionTab } from './DevHostController.js';
|
|
5
|
+
import type { Manifest } from '../manifest.js';
|
|
6
|
+
import type { RoolSpaceInfo, PublishedExtensionInfo } from '@rool-dev/sdk';
|
|
7
|
+
import type { Environment } from '../manifest.js';
|
|
8
|
+
import Sidebar from './Sidebar.svelte';
|
|
9
|
+
import AppGrid from './AppGrid.svelte';
|
|
10
|
+
|
|
11
|
+
// Props injected from the mount entry
|
|
12
|
+
interface Props {
|
|
13
|
+
channelId: string;
|
|
14
|
+
extensionUrl: string;
|
|
15
|
+
manifest: Manifest | null;
|
|
16
|
+
manifestError: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props: Props = $props();
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Controller + reactive state mirror
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
let spaces: RoolSpaceInfo[] = $state([]);
|
|
26
|
+
let currentSpaceId: string | null = $state(null);
|
|
27
|
+
let statusText: string = $state('Initializing...');
|
|
28
|
+
let statusState: 'ok' | 'loading' | 'off' = $state('off');
|
|
29
|
+
let placeholderText: string | null = $state('Authenticating...');
|
|
30
|
+
let sidebarCollapsed: boolean = $state(false);
|
|
31
|
+
let env: Environment = $state('prod');
|
|
32
|
+
let publishedExtensions: PublishedExtensionInfo[] = $state([]);
|
|
33
|
+
let installedExtensionIds: string[] = $state([]);
|
|
34
|
+
let tabs: ExtensionTab[] = $state([]);
|
|
35
|
+
let publishState: 'idle' | 'building' | 'uploading' | 'done' | 'error' = $state('idle');
|
|
36
|
+
let publishMessage: string | null = $state(null);
|
|
37
|
+
let publishUrl: string | null = $state(null);
|
|
38
|
+
|
|
39
|
+
// UI-only state (not in controller)
|
|
40
|
+
let dropdownOpen: boolean = $state(false);
|
|
41
|
+
|
|
42
|
+
const controller = new DevHostController(
|
|
43
|
+
props,
|
|
44
|
+
syncState,
|
|
45
|
+
tick,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
function syncState() {
|
|
49
|
+
spaces = controller.spaces;
|
|
50
|
+
currentSpaceId = controller.currentSpaceId;
|
|
51
|
+
statusText = controller.statusText;
|
|
52
|
+
statusState = controller.statusState;
|
|
53
|
+
placeholderText = controller.placeholderText;
|
|
54
|
+
sidebarCollapsed = controller.sidebarCollapsed;
|
|
55
|
+
env = controller.env;
|
|
56
|
+
publishedExtensions = controller.publishedExtensions;
|
|
57
|
+
installedExtensionIds = controller.installedExtensionIds;
|
|
58
|
+
tabs = controller.tabs;
|
|
59
|
+
publishState = controller.publishState;
|
|
60
|
+
publishMessage = controller.publishMessage;
|
|
61
|
+
publishUrl = controller.publishUrl;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Derived: published apps not yet installed (excluding the local dev app)
|
|
65
|
+
let uninstalledExtensions = $derived(
|
|
66
|
+
publishedExtensions.filter((ext) => ext.extensionId !== props.channelId && !installedExtensionIds.includes(ext.extensionId)),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Initial sync
|
|
70
|
+
syncState();
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Bootstrap
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
onMount(() => {
|
|
77
|
+
controller.boot();
|
|
78
|
+
|
|
79
|
+
function handleClickOutside(e: MouseEvent) {
|
|
80
|
+
if (dropdownOpen && !(e.target as Element)?.closest('[data-dropdown]')) {
|
|
81
|
+
dropdownOpen = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
document.addEventListener('click', handleClickOutside);
|
|
85
|
+
return () => document.removeEventListener('click', handleClickOutside);
|
|
86
|
+
});
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<!-- Sidebar -->
|
|
90
|
+
<Sidebar
|
|
91
|
+
{controller}
|
|
92
|
+
manifest={props.manifest}
|
|
93
|
+
manifestError={props.manifestError}
|
|
94
|
+
{spaces}
|
|
95
|
+
{currentSpaceId}
|
|
96
|
+
{env}
|
|
97
|
+
{statusText}
|
|
98
|
+
{statusState}
|
|
99
|
+
{sidebarCollapsed}
|
|
100
|
+
{publishState}
|
|
101
|
+
{publishMessage}
|
|
102
|
+
{publishUrl}
|
|
103
|
+
bind:dropdownOpen
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<!-- Main area -->
|
|
107
|
+
<div class="flex-1 min-w-0 flex flex-col">
|
|
108
|
+
{#if placeholderText}
|
|
109
|
+
<div class="flex items-center justify-center h-full text-slate-400 text-sm">{placeholderText}</div>
|
|
110
|
+
{:else}
|
|
111
|
+
<AppGrid
|
|
112
|
+
{controller}
|
|
113
|
+
{tabs}
|
|
114
|
+
uninstalledExtensions={uninstalledExtensions}
|
|
115
|
+
onInstallExtension={(id) => controller.installExtension(id)}
|
|
116
|
+
onRemoveExtension={(id) => controller.removeExtension(id)}
|
|
117
|
+
/>
|
|
118
|
+
{/if}
|
|
119
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Manifest } from '../manifest.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
channelId: string;
|
|
4
|
+
extensionUrl: string;
|
|
5
|
+
manifest: Manifest | null;
|
|
6
|
+
manifestError: string | null;
|
|
7
|
+
}
|
|
8
|
+
declare const HostShell: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type HostShell = ReturnType<typeof HostShell>;
|
|
10
|
+
export default HostShell;
|
|
11
|
+
//# sourceMappingURL=HostShell.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HostShell.svelte.d.ts","sourceRoot":"","sources":["../../src/dev/HostShell.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAQ7C,UAAU,KAAK;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAgGH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|