sh3-core 0.5.0 → 0.5.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/dist/Shell.svelte +2 -2
- package/dist/api.d.ts +1 -0
- package/dist/api.js +2 -0
- package/dist/apps/lifecycle.d.ts +6 -1
- package/dist/apps/lifecycle.js +28 -4
- package/dist/apps/registry.svelte.d.ts +5 -2
- package/dist/apps/registry.svelte.js +6 -7
- package/dist/apps/types.d.ts +13 -0
- package/dist/diagnostic/DiagnosticPanel.svelte +20 -0
- package/dist/diagnostic/DiagnosticPromptModal.svelte +1 -1
- package/dist/layout/DragPreview.svelte +1 -1
- package/dist/overlays/ModalFrame.svelte +1 -1
- package/dist/overlays/PopupFrame.svelte +1 -1
- package/dist/overlays/ToastItem.svelte +1 -1
- package/dist/primitives/TabbedPanel.svelte +1 -1
- package/dist/registry/installer.js +0 -2
- package/dist/shards/activate.svelte.d.ts +13 -6
- package/dist/shards/activate.svelte.js +19 -8
- package/dist/shards/types.d.ts +11 -0
- package/dist/shell-shard/ShellHome.svelte +13 -7
- package/dist/store/InstalledView.svelte +66 -5
- package/dist/store/StoreView.svelte +69 -16
- package/dist/store/storeApp.js +1 -1
- package/dist/store/storeShard.svelte.d.ts +2 -0
- package/dist/store/storeShard.svelte.js +89 -7
- package/dist/tokens.css +14 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +1 -1
package/dist/Shell.svelte
CHANGED
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
background: transparent;
|
|
161
161
|
color: var(--shell-fg-muted);
|
|
162
162
|
border: 1px solid var(--shell-border);
|
|
163
|
-
border-radius:
|
|
163
|
+
border-radius: var(--shell-radius);
|
|
164
164
|
cursor: pointer;
|
|
165
165
|
}
|
|
166
166
|
.shell-tabbar-home-button:hover {
|
|
@@ -180,6 +180,6 @@
|
|
|
180
180
|
color: #fff;
|
|
181
181
|
background: var(--shell-accent);
|
|
182
182
|
padding: 1px 6px;
|
|
183
|
-
border-radius:
|
|
183
|
+
border-radius: var(--shell-radius-lg);
|
|
184
184
|
}
|
|
185
185
|
</style>
|
package/dist/api.d.ts
CHANGED
|
@@ -23,4 +23,5 @@ export declare const capabilities: {
|
|
|
23
23
|
readonly hotInstall: boolean;
|
|
24
24
|
};
|
|
25
25
|
export type { ServerShard, ServerShardContext } from './server-shard/types';
|
|
26
|
+
export { VERSION } from './version';
|
|
26
27
|
export { setTokenOverrides, clearTokenOverrides, getTokenOverrides, } from './theme';
|
package/dist/api.js
CHANGED
|
@@ -44,5 +44,7 @@ export const capabilities = {
|
|
|
44
44
|
/** Whether this target supports hot-installing packages via dynamic import from blob URL. */
|
|
45
45
|
hotInstall: typeof Blob !== 'undefined' && typeof URL.createObjectURL === 'function',
|
|
46
46
|
};
|
|
47
|
+
// Package version.
|
|
48
|
+
export { VERSION } from './version';
|
|
47
49
|
// Theme token override API (shell-level theming support).
|
|
48
50
|
export { setTokenOverrides, clearTokenOverrides, getTokenOverrides, } from './theme';
|
package/dist/apps/lifecycle.d.ts
CHANGED
|
@@ -31,7 +31,12 @@ export declare function unloadApp(id: string): void;
|
|
|
31
31
|
* refcount hold intact, and its view containers stay alive in the pool.
|
|
32
32
|
* Launching the same app again is a root swap only.
|
|
33
33
|
*
|
|
34
|
+
* Fires `suspend` hooks on all required shards (in order), then on the
|
|
35
|
+
* app itself. Any hook returning `false` cancels navigation — the user
|
|
36
|
+
* stays in the app. Returns `true` if navigation succeeded, `false` if
|
|
37
|
+
* cancelled.
|
|
38
|
+
*
|
|
34
39
|
* Writes `null` to `__shell__:last-app` so reloading the page while on
|
|
35
40
|
* home lands on home, not on the formerly-active app.
|
|
36
41
|
*/
|
|
37
|
-
export declare function returnToHome():
|
|
42
|
+
export declare function returnToHome(): Promise<boolean>;
|
package/dist/apps/lifecycle.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* return-to-home (null). Boot reads it to decide whether to auto-launch.
|
|
13
13
|
*/
|
|
14
14
|
import { createStateZones } from '../state/zones.svelte';
|
|
15
|
-
import { activateShard, deactivateShard, registeredShards, } from '../shards/activate.svelte';
|
|
15
|
+
import { activateShard, deactivateShard, getShardContext, registeredShards, } from '../shards/activate.svelte';
|
|
16
16
|
import { attachApp, detachApp, switchToApp, switchToHome, } from '../layout/store.svelte';
|
|
17
17
|
import { activeApp, getRegisteredApp } from './registry.svelte';
|
|
18
18
|
import { createZoneManager } from '../state/manage';
|
|
@@ -69,7 +69,7 @@ function getOrCreateAppContext(appId) {
|
|
|
69
69
|
* @throws If the app is not registered or a required shard is not registered.
|
|
70
70
|
*/
|
|
71
71
|
export async function launchApp(id) {
|
|
72
|
-
var _a;
|
|
72
|
+
var _a, _b, _c;
|
|
73
73
|
const app = getRegisteredApp(id);
|
|
74
74
|
if (!app) {
|
|
75
75
|
throw new Error(`Cannot launch app "${id}": not registered`);
|
|
@@ -82,6 +82,14 @@ export async function launchApp(id) {
|
|
|
82
82
|
unloadApp(activeApp.id);
|
|
83
83
|
}
|
|
84
84
|
else if (activeApp.id === id) {
|
|
85
|
+
// Re-entering the same app from Home — fire resume hooks.
|
|
86
|
+
for (const shardId of app.manifest.requiredShards) {
|
|
87
|
+
const shard = registeredShards.get(shardId);
|
|
88
|
+
const shardCtx = getShardContext(shardId);
|
|
89
|
+
if (shard && shardCtx)
|
|
90
|
+
void ((_a = shard.resume) === null || _a === void 0 ? void 0 : _a.call(shard, shardCtx));
|
|
91
|
+
}
|
|
92
|
+
void ((_b = app.resume) === null || _b === void 0 ? void 0 : _b.call(app, getOrCreateAppContext(id)));
|
|
85
93
|
switchToApp();
|
|
86
94
|
writeLastApp(id);
|
|
87
95
|
return;
|
|
@@ -99,7 +107,7 @@ export async function launchApp(id) {
|
|
|
99
107
|
// Attach the layout (creates the workspace-zone proxy with version
|
|
100
108
|
// gate) and run the app's optional activate hook.
|
|
101
109
|
attachApp(app);
|
|
102
|
-
void ((
|
|
110
|
+
void ((_c = app.activate) === null || _c === void 0 ? void 0 : _c.call(app, getOrCreateAppContext(id)));
|
|
103
111
|
activeApp.id = id;
|
|
104
112
|
switchToApp();
|
|
105
113
|
writeLastApp(id);
|
|
@@ -151,10 +159,26 @@ export function unloadApp(id) {
|
|
|
151
159
|
* refcount hold intact, and its view containers stay alive in the pool.
|
|
152
160
|
* Launching the same app again is a root swap only.
|
|
153
161
|
*
|
|
162
|
+
* Fires `suspend` hooks on all required shards (in order), then on the
|
|
163
|
+
* app itself. Any hook returning `false` cancels navigation — the user
|
|
164
|
+
* stays in the app. Returns `true` if navigation succeeded, `false` if
|
|
165
|
+
* cancelled.
|
|
166
|
+
*
|
|
154
167
|
* Writes `null` to `__shell__:last-app` so reloading the page while on
|
|
155
168
|
* home lands on home, not on the formerly-active app.
|
|
156
169
|
*/
|
|
157
|
-
export function returnToHome() {
|
|
170
|
+
export async function returnToHome() {
|
|
171
|
+
const app = activeApp.id ? getRegisteredApp(activeApp.id) : null;
|
|
172
|
+
if (app) {
|
|
173
|
+
for (const shardId of app.manifest.requiredShards) {
|
|
174
|
+
const shard = registeredShards.get(shardId);
|
|
175
|
+
if ((shard === null || shard === void 0 ? void 0 : shard.suspend) && (await shard.suspend()) === false)
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
if (app.suspend && (await app.suspend()) === false)
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
158
181
|
switchToHome();
|
|
159
182
|
writeLastApp(null);
|
|
183
|
+
return true;
|
|
160
184
|
}
|
|
@@ -14,8 +14,11 @@ export declare const activeApp: {
|
|
|
14
14
|
id: string | null;
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
* Register an app with the framework.
|
|
18
|
-
*
|
|
17
|
+
* Register (or re-register) an app with the framework.
|
|
18
|
+
*
|
|
19
|
+
* If an app with the same id already exists it is silently replaced,
|
|
20
|
+
* which is the expected path during package updates — the new bundle is
|
|
21
|
+
* loaded and re-registered without requiring a full page reload.
|
|
19
22
|
*
|
|
20
23
|
* @param app - The app module to register.
|
|
21
24
|
*/
|
|
@@ -21,17 +21,16 @@ export const registeredApps = $state(new Map());
|
|
|
21
21
|
*/
|
|
22
22
|
export const activeApp = $state({ id: null });
|
|
23
23
|
/**
|
|
24
|
-
* Register an app with the framework.
|
|
25
|
-
*
|
|
24
|
+
* Register (or re-register) an app with the framework.
|
|
25
|
+
*
|
|
26
|
+
* If an app with the same id already exists it is silently replaced,
|
|
27
|
+
* which is the expected path during package updates — the new bundle is
|
|
28
|
+
* loaded and re-registered without requiring a full page reload.
|
|
26
29
|
*
|
|
27
30
|
* @param app - The app module to register.
|
|
28
31
|
*/
|
|
29
32
|
export function registerApp(app) {
|
|
30
|
-
|
|
31
|
-
if (registeredApps.has(id)) {
|
|
32
|
-
throw new Error(`App "${id}" is already registered`);
|
|
33
|
-
}
|
|
34
|
-
registeredApps.set(id, app);
|
|
33
|
+
registeredApps.set(app.manifest.id, app);
|
|
35
34
|
}
|
|
36
35
|
/**
|
|
37
36
|
* Reactive snapshot of all registered app manifests. Shell home iterates
|
package/dist/apps/types.d.ts
CHANGED
|
@@ -77,4 +77,17 @@ export interface App {
|
|
|
77
77
|
activate?(ctx: AppContext): void | Promise<void>;
|
|
78
78
|
/** Optional hook called before the app's shards are deactivated and the layout is detached. */
|
|
79
79
|
deactivate?(): void | Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Called when the user navigates to Home while the app is active. The
|
|
82
|
+
* app's shards, state, and pooled views remain alive. Return `false`
|
|
83
|
+
* (sync or async) to cancel the navigation — useful for "unsaved
|
|
84
|
+
* changes" confirmation modals.
|
|
85
|
+
*/
|
|
86
|
+
suspend?(): void | false | Promise<void | false>;
|
|
87
|
+
/**
|
|
88
|
+
* Called when the app is re-entered from Home (root swap back to app).
|
|
89
|
+
* Does NOT fire on first launch — that is `activate`. Receives the
|
|
90
|
+
* same `AppContext` that `activate` received.
|
|
91
|
+
*/
|
|
92
|
+
resume?(ctx: AppContext): void | Promise<void>;
|
|
80
93
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* Reads through the public sh3 API surface only.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { onMount } from 'svelte';
|
|
14
15
|
import {
|
|
15
16
|
listRegisteredApps,
|
|
16
17
|
getActiveApp,
|
|
@@ -25,11 +26,30 @@
|
|
|
25
26
|
const regShards = $derived(Array.from(registeredShards.values()));
|
|
26
27
|
const actShards = $derived(Array.from(activeShards.keys()));
|
|
27
28
|
|
|
29
|
+
let serverVersion = $state<string | null>(null);
|
|
30
|
+
|
|
31
|
+
onMount(async () => {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch('/api/version');
|
|
34
|
+
if (res.ok) {
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
serverVersion = data.version;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Server unreachable — leave null.
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
28
43
|
</script>
|
|
29
44
|
|
|
30
45
|
<div class="diagnostic">
|
|
31
46
|
<h2>Diagnostic</h2>
|
|
32
47
|
|
|
48
|
+
<section>
|
|
49
|
+
<h3>Server version</h3>
|
|
50
|
+
<p>{serverVersion ?? '—'}</p>
|
|
51
|
+
</section>
|
|
52
|
+
|
|
33
53
|
<section>
|
|
34
54
|
<h3>Active app</h3>
|
|
35
55
|
<p>{active ? `${active.label} (${active.id})` : 'none'}</p>
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
background: var(--shell-accent-muted);
|
|
74
74
|
color: var(--shell-fg);
|
|
75
75
|
border: 1px solid var(--shell-border-strong);
|
|
76
|
-
border-radius:
|
|
76
|
+
border-radius: var(--shell-radius-sm);
|
|
77
77
|
cursor: pointer;
|
|
78
78
|
}
|
|
79
79
|
button:hover { background: var(--shell-accent); }
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
|
|
50
50
|
color: var(--shell-fg);
|
|
51
51
|
border: 1px solid var(--shell-accent);
|
|
52
|
-
border-radius:
|
|
52
|
+
border-radius: var(--shell-radius-sm);
|
|
53
53
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
54
54
|
font-size: 12px;
|
|
55
55
|
font-family: var(--shell-font-ui);
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
|
|
77
77
|
color: var(--shell-fg);
|
|
78
78
|
border: 1px solid var(--shell-border-strong);
|
|
79
|
-
border-radius:
|
|
79
|
+
border-radius: var(--shell-radius);
|
|
80
80
|
min-width: 320px;
|
|
81
81
|
max-width: min(640px, 90vw);
|
|
82
82
|
max-height: 90vh;
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
|
|
77
77
|
color: var(--shell-fg);
|
|
78
78
|
border: 1px solid var(--shell-border-strong);
|
|
79
|
-
border-radius:
|
|
79
|
+
border-radius: var(--shell-radius-sm);
|
|
80
80
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
81
81
|
min-width: 120px;
|
|
82
82
|
outline: none;
|
|
@@ -97,8 +97,6 @@ export async function installPackage(bundle, meta) {
|
|
|
97
97
|
registerApp(app);
|
|
98
98
|
}
|
|
99
99
|
catch (err) {
|
|
100
|
-
// Registration failure (e.g. duplicate id) -- the package is persisted
|
|
101
|
-
// but not usable until reload. Log and continue.
|
|
102
100
|
console.warn(`[sh3] Package "${meta.id}" installed but registration failed (will retry on next boot):`, err instanceof Error ? err.message : err);
|
|
103
101
|
hotLoaded = false;
|
|
104
102
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Shard } from './types';
|
|
1
|
+
import type { Shard, ShardContext } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Reactive registry of every shard known to the host. Keys are shard ids.
|
|
4
4
|
* Populated once at boot by the glob-discovery loop in main.ts (through
|
|
@@ -10,12 +10,14 @@ import type { Shard } from './types';
|
|
|
10
10
|
export declare const registeredShards: Map<string, Shard>;
|
|
11
11
|
export declare const activeShards: Map<string, Shard>;
|
|
12
12
|
/**
|
|
13
|
-
* Register a shard with the framework so it can later be
|
|
14
|
-
* the shard in `registeredShards` but does not run
|
|
15
|
-
* at app launch (or
|
|
13
|
+
* Register (or re-register) a shard with the framework so it can later be
|
|
14
|
+
* activated. Records the shard in `registeredShards` but does not run
|
|
15
|
+
* `activate` — that happens at app launch (or for self-starting shards).
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* If a shard with the same id already exists it is silently replaced,
|
|
18
|
+
* which is the expected path during package updates. If the old shard was
|
|
19
|
+
* active it is deactivated first so the new version can be cleanly
|
|
20
|
+
* activated on next launch.
|
|
19
21
|
*/
|
|
20
22
|
export declare function registerShard(shard: Shard): void;
|
|
21
23
|
/**
|
|
@@ -43,3 +45,8 @@ export declare function deactivateShard(id: string): void;
|
|
|
43
45
|
* @param id - The `ShardManifest.id` to check.
|
|
44
46
|
*/
|
|
45
47
|
export declare function isActive(id: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Return the ShardContext for an active shard, or undefined if not active.
|
|
50
|
+
* Used by lifecycle.ts to pass context to `shard.resume()`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getShardContext(id: string): ShardContext | undefined;
|
|
@@ -39,17 +39,19 @@ export const registeredShards = $state(new Map());
|
|
|
39
39
|
const active = new Map();
|
|
40
40
|
export const activeShards = $state(new Map());
|
|
41
41
|
/**
|
|
42
|
-
* Register a shard with the framework so it can later be
|
|
43
|
-
* the shard in `registeredShards` but does not run
|
|
44
|
-
* at app launch (or
|
|
42
|
+
* Register (or re-register) a shard with the framework so it can later be
|
|
43
|
+
* activated. Records the shard in `registeredShards` but does not run
|
|
44
|
+
* `activate` — that happens at app launch (or for self-starting shards).
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* If a shard with the same id already exists it is silently replaced,
|
|
47
|
+
* which is the expected path during package updates. If the old shard was
|
|
48
|
+
* active it is deactivated first so the new version can be cleanly
|
|
49
|
+
* activated on next launch.
|
|
48
50
|
*/
|
|
49
51
|
export function registerShard(shard) {
|
|
50
52
|
const id = shard.manifest.id;
|
|
51
|
-
if (registeredShards.has(id)) {
|
|
52
|
-
|
|
53
|
+
if (registeredShards.has(id) && activeShards.has(id)) {
|
|
54
|
+
deactivateShard(id);
|
|
53
55
|
}
|
|
54
56
|
registeredShards.set(id, shard);
|
|
55
57
|
}
|
|
@@ -73,7 +75,7 @@ export async function activateShard(id) {
|
|
|
73
75
|
// and is now being required by an app). Idempotent — no error.
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
76
|
-
const entry = { shard, viewIds: new Set(), cleanupFns: [] };
|
|
78
|
+
const entry = { shard, ctx: undefined, viewIds: new Set(), cleanupFns: [] };
|
|
77
79
|
// envState holds the reactive env data for this shard.
|
|
78
80
|
// Must be declared with $state at variable declaration time (Svelte 5 rule).
|
|
79
81
|
const envState = $state({
|
|
@@ -122,6 +124,7 @@ export async function activateShard(id) {
|
|
|
122
124
|
? createZoneManager()
|
|
123
125
|
: undefined,
|
|
124
126
|
};
|
|
127
|
+
entry.ctx = ctx;
|
|
125
128
|
active.set(id, entry);
|
|
126
129
|
activeShards.set(id, shard);
|
|
127
130
|
await shard.activate(ctx);
|
|
@@ -173,3 +176,11 @@ export function deactivateShard(id) {
|
|
|
173
176
|
export function isActive(id) {
|
|
174
177
|
return active.has(id);
|
|
175
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Return the ShardContext for an active shard, or undefined if not active.
|
|
181
|
+
* Used by lifecycle.ts to pass context to `shard.resume()`.
|
|
182
|
+
*/
|
|
183
|
+
export function getShardContext(id) {
|
|
184
|
+
var _a;
|
|
185
|
+
return (_a = active.get(id)) === null || _a === void 0 ? void 0 : _a.ctx;
|
|
186
|
+
}
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -193,4 +193,15 @@ export interface Shard {
|
|
|
193
193
|
autostart?(ctx: ShardContext): void | Promise<void>;
|
|
194
194
|
/** Optional cleanup hook called when the shard is deactivated. Release timers, subscriptions, and external resources here. */
|
|
195
195
|
deactivate?(): void | Promise<void>;
|
|
196
|
+
/**
|
|
197
|
+
* Called when the owning app is suspended (Home button). The shard
|
|
198
|
+
* remains active; its views and state are preserved. Return `false`
|
|
199
|
+
* (sync or async) to cancel the navigation.
|
|
200
|
+
*/
|
|
201
|
+
suspend?(): void | false | Promise<void | false>;
|
|
202
|
+
/**
|
|
203
|
+
* Called when the owning app resumes from Home. Receives the same
|
|
204
|
+
* `ShardContext` that `activate` received.
|
|
205
|
+
*/
|
|
206
|
+
resume?(ctx: ShardContext): void | Promise<void>;
|
|
196
207
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 3. Elevate prompt — shown when not elevated
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { listRegisteredApps, launchApp, isAdmin } from '../api';
|
|
9
|
+
import { listRegisteredApps, launchApp, isAdmin, VERSION } from '../api';
|
|
10
10
|
import { elevate, deescalate } from '../auth/index';
|
|
11
11
|
|
|
12
12
|
const apps = $derived(listRegisteredApps());
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
<div class="shell-home">
|
|
39
39
|
<header class="shell-home-header">
|
|
40
40
|
<h1>SH3</h1>
|
|
41
|
+
<span class="shell-home-version">v{VERSION}</span>
|
|
41
42
|
<span class="shell-home-alpha">alpha</span>
|
|
42
43
|
{#if elevated}
|
|
43
44
|
<button type="button" class="shell-home-deescalate" onclick={handleDeescalate}>
|
|
@@ -153,6 +154,11 @@
|
|
|
153
154
|
color: var(--shell-accent);
|
|
154
155
|
letter-spacing: 2px;
|
|
155
156
|
}
|
|
157
|
+
.shell-home-version {
|
|
158
|
+
font-size: 13px;
|
|
159
|
+
color: var(--shell-fg-subtle);
|
|
160
|
+
letter-spacing: 0.04em;
|
|
161
|
+
}
|
|
156
162
|
.shell-home-alpha {
|
|
157
163
|
font-size: 11px;
|
|
158
164
|
font-weight: 700;
|
|
@@ -197,7 +203,7 @@
|
|
|
197
203
|
padding: 14px 18px;
|
|
198
204
|
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
|
|
199
205
|
border: 1px solid var(--shell-border);
|
|
200
|
-
border-radius:
|
|
206
|
+
border-radius: var(--shell-radius-md);
|
|
201
207
|
}
|
|
202
208
|
.shell-home-entry-label {
|
|
203
209
|
grid-column: 1;
|
|
@@ -217,7 +223,7 @@
|
|
|
217
223
|
background: var(--shell-accent);
|
|
218
224
|
color: var(--shell-bg);
|
|
219
225
|
border: none;
|
|
220
|
-
border-radius:
|
|
226
|
+
border-radius: var(--shell-radius);
|
|
221
227
|
font-weight: 600;
|
|
222
228
|
cursor: pointer;
|
|
223
229
|
}
|
|
@@ -229,7 +235,7 @@
|
|
|
229
235
|
background: transparent;
|
|
230
236
|
color: var(--shell-fg-subtle);
|
|
231
237
|
border: 1px solid var(--shell-border);
|
|
232
|
-
border-radius:
|
|
238
|
+
border-radius: var(--shell-radius);
|
|
233
239
|
cursor: pointer;
|
|
234
240
|
font-size: 12px;
|
|
235
241
|
}
|
|
@@ -252,7 +258,7 @@
|
|
|
252
258
|
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
|
|
253
259
|
color: var(--shell-fg);
|
|
254
260
|
border: 1px solid var(--shell-border);
|
|
255
|
-
border-radius:
|
|
261
|
+
border-radius: var(--shell-radius);
|
|
256
262
|
font-family: monospace;
|
|
257
263
|
font-size: 13px;
|
|
258
264
|
}
|
|
@@ -264,7 +270,7 @@
|
|
|
264
270
|
background: var(--shell-accent);
|
|
265
271
|
color: var(--shell-bg);
|
|
266
272
|
border: none;
|
|
267
|
-
border-radius:
|
|
273
|
+
border-radius: var(--shell-radius);
|
|
268
274
|
font-weight: 600;
|
|
269
275
|
cursor: pointer;
|
|
270
276
|
white-space: nowrap;
|
|
@@ -279,6 +285,6 @@
|
|
|
279
285
|
font-size: 12px;
|
|
280
286
|
color: var(--shell-error, #d32f2f);
|
|
281
287
|
background: color-mix(in srgb, var(--shell-error, #d32f2f) 10%, transparent);
|
|
282
|
-
border-radius:
|
|
288
|
+
border-radius: var(--shell-radius);
|
|
283
289
|
}
|
|
284
290
|
</style>
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
const ctx = storeContext;
|
|
15
15
|
|
|
16
16
|
let uninstallingIds = $state<Set<string>>(new Set());
|
|
17
|
+
let updatingIds = $state<Set<string>>(new Set());
|
|
18
|
+
let updateError = $state<string | null>(null);
|
|
17
19
|
|
|
18
20
|
async function handleUninstall(id: string) {
|
|
19
21
|
if (uninstallingIds.has(id)) return;
|
|
@@ -32,6 +34,23 @@
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
async function handleUpdate(id: string) {
|
|
38
|
+
if (updatingIds.has(id)) return;
|
|
39
|
+
|
|
40
|
+
updatingIds = new Set([...updatingIds, id]);
|
|
41
|
+
updateError = null;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await ctx.updatePackage(id);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
updateError = err instanceof Error ? err.message : String(err);
|
|
47
|
+
} finally {
|
|
48
|
+
const next = new Set(updatingIds);
|
|
49
|
+
next.delete(id);
|
|
50
|
+
updatingIds = next;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
function handleRefresh() {
|
|
36
55
|
ctx.refreshInstalled();
|
|
37
56
|
}
|
|
@@ -55,6 +74,10 @@
|
|
|
55
74
|
<button class="installed-refresh" onclick={handleRefresh}>Refresh</button>
|
|
56
75
|
</header>
|
|
57
76
|
|
|
77
|
+
{#if updateError}
|
|
78
|
+
<div class="installed-error">{updateError}</div>
|
|
79
|
+
{/if}
|
|
80
|
+
|
|
58
81
|
{#if ctx.state.ephemeral.installed.length === 0}
|
|
59
82
|
<div class="installed-empty">No packages installed.</div>
|
|
60
83
|
{:else}
|
|
@@ -75,6 +98,17 @@
|
|
|
75
98
|
<span>Installed: {formatDate(pkg.installedAt)}</span>
|
|
76
99
|
</div>
|
|
77
100
|
<div class="installed-item-actions">
|
|
101
|
+
{#if pkg.id in ctx.state.ephemeral.updatable}
|
|
102
|
+
{@const target = ctx.state.ephemeral.updatable[pkg.id]}
|
|
103
|
+
{@const updating = updatingIds.has(pkg.id)}
|
|
104
|
+
<button
|
|
105
|
+
class="installed-update-btn"
|
|
106
|
+
onclick={() => handleUpdate(pkg.id)}
|
|
107
|
+
disabled={updating || uninstalling}
|
|
108
|
+
>
|
|
109
|
+
{updating ? 'Updating...' : `Update -> ${target.latest.version}`}
|
|
110
|
+
</button>
|
|
111
|
+
{/if}
|
|
78
112
|
<button
|
|
79
113
|
class="installed-uninstall-btn"
|
|
80
114
|
onclick={() => handleUninstall(pkg.id)}
|
|
@@ -91,7 +125,7 @@
|
|
|
91
125
|
|
|
92
126
|
<style>
|
|
93
127
|
.installed-view {
|
|
94
|
-
font-family: var(--shell-font
|
|
128
|
+
font-family: var(--shell-font-ui);
|
|
95
129
|
color: var(--shell-fg, #e0e0e0);
|
|
96
130
|
background: var(--shell-bg, #1e1e1e);
|
|
97
131
|
padding: 16px;
|
|
@@ -115,7 +149,7 @@
|
|
|
115
149
|
background: var(--shell-accent, #007acc);
|
|
116
150
|
color: #fff;
|
|
117
151
|
border: none;
|
|
118
|
-
border-radius:
|
|
152
|
+
border-radius: var(--shell-radius);
|
|
119
153
|
cursor: pointer;
|
|
120
154
|
font-family: inherit;
|
|
121
155
|
font-size: 0.875rem;
|
|
@@ -137,7 +171,7 @@
|
|
|
137
171
|
.installed-item {
|
|
138
172
|
background: var(--shell-input-bg, #2a2a2a);
|
|
139
173
|
border: 1px solid var(--shell-border, #444);
|
|
140
|
-
border-radius:
|
|
174
|
+
border-radius: var(--shell-radius-md);
|
|
141
175
|
padding: 12px 14px;
|
|
142
176
|
display: flex;
|
|
143
177
|
flex-direction: column;
|
|
@@ -155,7 +189,7 @@
|
|
|
155
189
|
.installed-item-badge {
|
|
156
190
|
font-size: 0.6875rem;
|
|
157
191
|
padding: 1px 6px;
|
|
158
|
-
border-radius:
|
|
192
|
+
border-radius: var(--shell-radius-sm);
|
|
159
193
|
text-transform: uppercase;
|
|
160
194
|
font-weight: 600;
|
|
161
195
|
letter-spacing: 0.04em;
|
|
@@ -182,13 +216,14 @@
|
|
|
182
216
|
.installed-item-actions {
|
|
183
217
|
display: flex;
|
|
184
218
|
justify-content: flex-end;
|
|
219
|
+
gap: 8px;
|
|
185
220
|
}
|
|
186
221
|
.installed-uninstall-btn {
|
|
187
222
|
padding: 4px 12px;
|
|
188
223
|
background: transparent;
|
|
189
224
|
color: var(--shell-error, #d32f2f);
|
|
190
225
|
border: 1px solid var(--shell-error, #d32f2f);
|
|
191
|
-
border-radius:
|
|
226
|
+
border-radius: var(--shell-radius);
|
|
192
227
|
cursor: pointer;
|
|
193
228
|
font-family: inherit;
|
|
194
229
|
font-size: 0.8125rem;
|
|
@@ -200,4 +235,30 @@
|
|
|
200
235
|
opacity: 0.6;
|
|
201
236
|
cursor: not-allowed;
|
|
202
237
|
}
|
|
238
|
+
.installed-update-btn {
|
|
239
|
+
padding: 4px 12px;
|
|
240
|
+
background: var(--shell-warning, #ff9800);
|
|
241
|
+
color: #fff;
|
|
242
|
+
border: none;
|
|
243
|
+
border-radius: var(--shell-radius);
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
font-family: inherit;
|
|
246
|
+
font-size: 0.8125rem;
|
|
247
|
+
}
|
|
248
|
+
.installed-update-btn:hover:not(:disabled) {
|
|
249
|
+
filter: brightness(1.1);
|
|
250
|
+
}
|
|
251
|
+
.installed-update-btn:disabled {
|
|
252
|
+
opacity: 0.6;
|
|
253
|
+
cursor: not-allowed;
|
|
254
|
+
}
|
|
255
|
+
.installed-error {
|
|
256
|
+
padding: 8px 12px;
|
|
257
|
+
margin-bottom: 12px;
|
|
258
|
+
background: color-mix(in srgb, var(--shell-error, #d32f2f) 15%, transparent);
|
|
259
|
+
color: var(--shell-error, #d32f2f);
|
|
260
|
+
border: 1px solid var(--shell-error, #d32f2f);
|
|
261
|
+
border-radius: var(--shell-radius);
|
|
262
|
+
font-size: 0.8125rem;
|
|
263
|
+
}
|
|
203
264
|
</style>
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
let search = $state('');
|
|
18
18
|
let typeFilter = $state<'all' | 'shard' | 'app'>('all');
|
|
19
19
|
let installingIds = $state<Set<string>>(new Set());
|
|
20
|
+
let updatingIds = $state<Set<string>>(new Set());
|
|
20
21
|
let installError = $state<string | null>(null);
|
|
21
22
|
let newRegistryUrl = $state('');
|
|
22
23
|
|
|
@@ -66,6 +67,31 @@
|
|
|
66
67
|
return String(pkg.latest.contractVersion) !== String(contract.version);
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function hasUpdate(id: string): boolean {
|
|
71
|
+
return id in ctx.state.ephemeral.updatable;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function installedVersion(id: string): string {
|
|
75
|
+
return ctx.state.ephemeral.installed.find((p: InstalledPackage) => p.id === id)?.version ?? '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function handleUpdate(id: string) {
|
|
79
|
+
if (updatingIds.has(id)) return;
|
|
80
|
+
|
|
81
|
+
updatingIds = new Set([...updatingIds, id]);
|
|
82
|
+
installError = null;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await ctx.updatePackage(id);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
installError = err instanceof Error ? err.message : String(err);
|
|
88
|
+
} finally {
|
|
89
|
+
const next = new Set(updatingIds);
|
|
90
|
+
next.delete(id);
|
|
91
|
+
updatingIds = next;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
async function handleInstall(pkg: ResolvedPackage) {
|
|
70
96
|
const id = pkg.entry.id;
|
|
71
97
|
if (installingIds.has(id)) return;
|
|
@@ -181,6 +207,8 @@
|
|
|
181
207
|
{@const installed = isInstalled(pkg.entry.id)}
|
|
182
208
|
{@const mismatch = hasContractMismatch(pkg)}
|
|
183
209
|
{@const installing = installingIds.has(pkg.entry.id)}
|
|
210
|
+
{@const updatable = hasUpdate(pkg.entry.id)}
|
|
211
|
+
{@const updating = updatingIds.has(pkg.entry.id)}
|
|
184
212
|
<div class="store-card">
|
|
185
213
|
<div class="store-card-header">
|
|
186
214
|
<div class="store-card-icon">
|
|
@@ -208,7 +236,15 @@
|
|
|
208
236
|
</div>
|
|
209
237
|
{/if}
|
|
210
238
|
<div class="store-card-actions">
|
|
211
|
-
{#if installed}
|
|
239
|
+
{#if installed && updatable}
|
|
240
|
+
<button
|
|
241
|
+
class="store-update-btn"
|
|
242
|
+
onclick={() => handleUpdate(pkg.entry.id)}
|
|
243
|
+
disabled={updating}
|
|
244
|
+
>
|
|
245
|
+
{updating ? 'Updating...' : `Update ${installedVersion(pkg.entry.id)} -> ${pkg.latest.version}`}
|
|
246
|
+
</button>
|
|
247
|
+
{:else if installed}
|
|
212
248
|
<span class="store-installed-label">Installed</span>
|
|
213
249
|
{:else}
|
|
214
250
|
<button
|
|
@@ -237,7 +273,7 @@
|
|
|
237
273
|
|
|
238
274
|
<style>
|
|
239
275
|
.store-view {
|
|
240
|
-
font-family: var(--shell-font
|
|
276
|
+
font-family: var(--shell-font-ui);
|
|
241
277
|
color: var(--shell-fg, #e0e0e0);
|
|
242
278
|
background: var(--shell-bg, #1e1e1e);
|
|
243
279
|
padding: 16px;
|
|
@@ -265,7 +301,7 @@
|
|
|
265
301
|
background: var(--shell-input-bg, #2a2a2a);
|
|
266
302
|
color: var(--shell-fg, #e0e0e0);
|
|
267
303
|
border: 1px solid var(--shell-border, #444);
|
|
268
|
-
border-radius:
|
|
304
|
+
border-radius: var(--shell-radius);
|
|
269
305
|
font-family: inherit;
|
|
270
306
|
font-size: 0.875rem;
|
|
271
307
|
}
|
|
@@ -277,7 +313,7 @@
|
|
|
277
313
|
background: var(--shell-input-bg, #2a2a2a);
|
|
278
314
|
color: var(--shell-fg, #e0e0e0);
|
|
279
315
|
border: 1px solid var(--shell-border, #444);
|
|
280
|
-
border-radius:
|
|
316
|
+
border-radius: var(--shell-radius);
|
|
281
317
|
font-family: inherit;
|
|
282
318
|
font-size: 0.875rem;
|
|
283
319
|
}
|
|
@@ -286,7 +322,7 @@
|
|
|
286
322
|
background: var(--shell-accent, #007acc);
|
|
287
323
|
color: #fff;
|
|
288
324
|
border: none;
|
|
289
|
-
border-radius:
|
|
325
|
+
border-radius: var(--shell-radius);
|
|
290
326
|
cursor: pointer;
|
|
291
327
|
font-family: inherit;
|
|
292
328
|
font-size: 0.875rem;
|
|
@@ -301,7 +337,7 @@
|
|
|
301
337
|
background: color-mix(in srgb, var(--shell-error, #d32f2f) 15%, transparent);
|
|
302
338
|
color: var(--shell-error, #d32f2f);
|
|
303
339
|
border: 1px solid var(--shell-error, #d32f2f);
|
|
304
|
-
border-radius:
|
|
340
|
+
border-radius: var(--shell-radius);
|
|
305
341
|
font-size: 0.8125rem;
|
|
306
342
|
}
|
|
307
343
|
.store-grid {
|
|
@@ -312,7 +348,7 @@
|
|
|
312
348
|
.store-card {
|
|
313
349
|
background: var(--shell-input-bg, #2a2a2a);
|
|
314
350
|
border: 1px solid var(--shell-border, #444);
|
|
315
|
-
border-radius:
|
|
351
|
+
border-radius: var(--shell-radius-md);
|
|
316
352
|
padding: 14px;
|
|
317
353
|
display: flex;
|
|
318
354
|
flex-direction: column;
|
|
@@ -337,7 +373,7 @@
|
|
|
337
373
|
.store-icon-img {
|
|
338
374
|
width: 36px;
|
|
339
375
|
height: 36px;
|
|
340
|
-
border-radius:
|
|
376
|
+
border-radius: var(--shell-radius);
|
|
341
377
|
object-fit: cover;
|
|
342
378
|
}
|
|
343
379
|
.store-icon-placeholder {
|
|
@@ -348,7 +384,7 @@
|
|
|
348
384
|
justify-content: center;
|
|
349
385
|
background: var(--shell-accent, #007acc);
|
|
350
386
|
color: #fff;
|
|
351
|
-
border-radius:
|
|
387
|
+
border-radius: var(--shell-radius);
|
|
352
388
|
font-weight: 700;
|
|
353
389
|
font-size: 1rem;
|
|
354
390
|
}
|
|
@@ -365,7 +401,7 @@
|
|
|
365
401
|
.store-card-badge {
|
|
366
402
|
font-size: 0.6875rem;
|
|
367
403
|
padding: 1px 6px;
|
|
368
|
-
border-radius:
|
|
404
|
+
border-radius: var(--shell-radius-sm);
|
|
369
405
|
text-transform: uppercase;
|
|
370
406
|
font-weight: 600;
|
|
371
407
|
letter-spacing: 0.04em;
|
|
@@ -397,7 +433,7 @@
|
|
|
397
433
|
color: var(--shell-warning, #ff9800);
|
|
398
434
|
padding: 4px 8px;
|
|
399
435
|
background: color-mix(in srgb, var(--shell-warning, #ff9800) 10%, transparent);
|
|
400
|
-
border-radius:
|
|
436
|
+
border-radius: var(--shell-radius-sm);
|
|
401
437
|
}
|
|
402
438
|
.store-card-actions {
|
|
403
439
|
margin-top: auto;
|
|
@@ -409,7 +445,7 @@
|
|
|
409
445
|
background: var(--shell-accent, #007acc);
|
|
410
446
|
color: #fff;
|
|
411
447
|
border: none;
|
|
412
|
-
border-radius:
|
|
448
|
+
border-radius: var(--shell-radius);
|
|
413
449
|
cursor: pointer;
|
|
414
450
|
font-family: inherit;
|
|
415
451
|
font-size: 0.8125rem;
|
|
@@ -423,6 +459,23 @@
|
|
|
423
459
|
color: var(--shell-success, #4caf50);
|
|
424
460
|
font-weight: 600;
|
|
425
461
|
}
|
|
462
|
+
.store-update-btn {
|
|
463
|
+
padding: 5px 14px;
|
|
464
|
+
background: var(--shell-warning, #ff9800);
|
|
465
|
+
color: #fff;
|
|
466
|
+
border: none;
|
|
467
|
+
border-radius: var(--shell-radius);
|
|
468
|
+
cursor: pointer;
|
|
469
|
+
font-family: inherit;
|
|
470
|
+
font-size: 0.8125rem;
|
|
471
|
+
}
|
|
472
|
+
.store-update-btn:hover:not(:disabled) {
|
|
473
|
+
filter: brightness(1.1);
|
|
474
|
+
}
|
|
475
|
+
.store-update-btn:disabled {
|
|
476
|
+
opacity: 0.6;
|
|
477
|
+
cursor: not-allowed;
|
|
478
|
+
}
|
|
426
479
|
.store-empty {
|
|
427
480
|
text-align: center;
|
|
428
481
|
padding: 32px 16px;
|
|
@@ -442,7 +495,7 @@
|
|
|
442
495
|
padding: 4px 8px;
|
|
443
496
|
background: var(--shell-input-bg, #2a2a2a);
|
|
444
497
|
border: 1px solid var(--shell-border, #444);
|
|
445
|
-
border-radius:
|
|
498
|
+
border-radius: var(--shell-radius);
|
|
446
499
|
font-size: 0.8125rem;
|
|
447
500
|
}
|
|
448
501
|
.store-registry-url {
|
|
@@ -456,7 +509,7 @@
|
|
|
456
509
|
background: transparent;
|
|
457
510
|
color: var(--shell-error, #d32f2f);
|
|
458
511
|
border: 1px solid var(--shell-error, #d32f2f);
|
|
459
|
-
border-radius:
|
|
512
|
+
border-radius: var(--shell-radius-sm);
|
|
460
513
|
cursor: pointer;
|
|
461
514
|
font-size: 0.75rem;
|
|
462
515
|
flex-shrink: 0;
|
|
@@ -473,7 +526,7 @@
|
|
|
473
526
|
background: var(--shell-input-bg, #2a2a2a);
|
|
474
527
|
color: var(--shell-fg, #e0e0e0);
|
|
475
528
|
border: 1px solid var(--shell-border, #444);
|
|
476
|
-
border-radius:
|
|
529
|
+
border-radius: var(--shell-radius);
|
|
477
530
|
font-family: inherit;
|
|
478
531
|
font-size: 0.8125rem;
|
|
479
532
|
}
|
|
@@ -485,7 +538,7 @@
|
|
|
485
538
|
background: var(--shell-accent, #007acc);
|
|
486
539
|
color: #fff;
|
|
487
540
|
border: none;
|
|
488
|
-
border-radius:
|
|
541
|
+
border-radius: var(--shell-radius);
|
|
489
542
|
cursor: pointer;
|
|
490
543
|
font-family: inherit;
|
|
491
544
|
font-size: 0.8125rem;
|
package/dist/store/storeApp.js
CHANGED
|
@@ -13,6 +13,7 @@ interface StoreZoneSchema {
|
|
|
13
13
|
ephemeral: {
|
|
14
14
|
catalog: ResolvedPackage[];
|
|
15
15
|
installed: InstalledPackage[];
|
|
16
|
+
updatable: Record<string, ResolvedPackage>;
|
|
16
17
|
loading: boolean;
|
|
17
18
|
error: string | null;
|
|
18
19
|
};
|
|
@@ -24,6 +25,7 @@ export interface StoreContext {
|
|
|
24
25
|
isAdmin: boolean;
|
|
25
26
|
refreshCatalog(): Promise<void>;
|
|
26
27
|
refreshInstalled(): Promise<void>;
|
|
28
|
+
updatePackage(id: string): Promise<void>;
|
|
27
29
|
addRegistry(url: string): Promise<void>;
|
|
28
30
|
removeRegistry(url: string): Promise<void>;
|
|
29
31
|
}
|
|
@@ -14,9 +14,31 @@
|
|
|
14
14
|
import { mount, unmount } from 'svelte';
|
|
15
15
|
import StoreView from './StoreView.svelte';
|
|
16
16
|
import InstalledView from './InstalledView.svelte';
|
|
17
|
-
import { fetchRegistries } from '../registry/client';
|
|
18
|
-
import { listInstalledPackages } from '../registry/installer';
|
|
19
|
-
import {
|
|
17
|
+
import { fetchRegistries, fetchBundle, buildPackageMeta } from '../registry/client';
|
|
18
|
+
import { installPackage, listInstalledPackages } from '../registry/installer';
|
|
19
|
+
import { loadBundle, savePackage } from '../registry/storage';
|
|
20
|
+
import { serverInstallPackage, fetchServerPackages } from '../env/client';
|
|
21
|
+
/**
|
|
22
|
+
* Compare two semver-like version strings.
|
|
23
|
+
* Returns true only if `available` is strictly greater than `installed`.
|
|
24
|
+
* Compares major.minor.patch left-to-right as integers.
|
|
25
|
+
* Non-numeric segments are treated as 0.
|
|
26
|
+
*/
|
|
27
|
+
function isNewerVersion(available, installed) {
|
|
28
|
+
var _a, _b;
|
|
29
|
+
const a = available.split('.').map((s) => parseInt(s, 10) || 0);
|
|
30
|
+
const b = installed.split('.').map((s) => parseInt(s, 10) || 0);
|
|
31
|
+
const len = Math.max(a.length, b.length);
|
|
32
|
+
for (let i = 0; i < len; i++) {
|
|
33
|
+
const av = (_a = a[i]) !== null && _a !== void 0 ? _a : 0;
|
|
34
|
+
const bv = (_b = b[i]) !== null && _b !== void 0 ? _b : 0;
|
|
35
|
+
if (av > bv)
|
|
36
|
+
return true;
|
|
37
|
+
if (av < bv)
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
20
42
|
/**
|
|
21
43
|
* Module-level context set during activate(). Imported by the Svelte
|
|
22
44
|
* view components so they can read/write store state and trigger refreshes.
|
|
@@ -26,7 +48,7 @@ export const storeShard = {
|
|
|
26
48
|
manifest: {
|
|
27
49
|
id: 'sh3-store',
|
|
28
50
|
label: 'Package Store',
|
|
29
|
-
version: '0.1
|
|
51
|
+
version: '0.2.1',
|
|
30
52
|
views: [
|
|
31
53
|
{ id: 'sh3-store:browse', label: 'Store' },
|
|
32
54
|
{ id: 'sh3-store:installed', label: 'Installed' },
|
|
@@ -38,16 +60,28 @@ export const storeShard = {
|
|
|
38
60
|
ephemeral: {
|
|
39
61
|
catalog: [],
|
|
40
62
|
installed: [],
|
|
63
|
+
updatable: {},
|
|
41
64
|
loading: false,
|
|
42
65
|
error: null,
|
|
43
66
|
},
|
|
44
67
|
});
|
|
68
|
+
function recomputeUpdatable() {
|
|
69
|
+
const result = {};
|
|
70
|
+
for (const pkg of state.ephemeral.installed) {
|
|
71
|
+
const catalogEntry = state.ephemeral.catalog.find((c) => c.entry.id === pkg.id);
|
|
72
|
+
if (catalogEntry && isNewerVersion(catalogEntry.latest.version, pkg.version)) {
|
|
73
|
+
result[pkg.id] = catalogEntry;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
state.ephemeral.updatable = result;
|
|
77
|
+
}
|
|
45
78
|
async function refreshCatalog() {
|
|
46
79
|
state.ephemeral.loading = true;
|
|
47
80
|
state.ephemeral.error = null;
|
|
48
81
|
try {
|
|
49
82
|
const results = await fetchRegistries(env.registries);
|
|
50
83
|
state.ephemeral.catalog = results;
|
|
84
|
+
recomputeUpdatable();
|
|
51
85
|
}
|
|
52
86
|
catch (err) {
|
|
53
87
|
state.ephemeral.error =
|
|
@@ -71,6 +105,7 @@ export const storeShard = {
|
|
|
71
105
|
installedAt: (_c = p.installedAt) !== null && _c !== void 0 ? _c : '',
|
|
72
106
|
});
|
|
73
107
|
});
|
|
108
|
+
recomputeUpdatable();
|
|
74
109
|
}
|
|
75
110
|
catch (err) {
|
|
76
111
|
console.warn('[sh3-store] Failed to list installed packages:', err instanceof Error ? err.message : err);
|
|
@@ -78,6 +113,7 @@ export const storeShard = {
|
|
|
78
113
|
try {
|
|
79
114
|
const packages = await listInstalledPackages();
|
|
80
115
|
state.ephemeral.installed = packages;
|
|
116
|
+
recomputeUpdatable();
|
|
81
117
|
}
|
|
82
118
|
catch (_a) {
|
|
83
119
|
// Nothing to show.
|
|
@@ -95,12 +131,57 @@ export const storeShard = {
|
|
|
95
131
|
const registries = env.registries.filter((r) => r !== url);
|
|
96
132
|
await ctx.envUpdate({ registries });
|
|
97
133
|
}
|
|
134
|
+
async function updatePackage(id) {
|
|
135
|
+
var _a, _b;
|
|
136
|
+
const catalogEntry = state.ephemeral.updatable[id];
|
|
137
|
+
if (!catalogEntry)
|
|
138
|
+
return;
|
|
139
|
+
const installedRecord = state.ephemeral.installed.find((p) => p.id === id);
|
|
140
|
+
if (!installedRecord)
|
|
141
|
+
return;
|
|
142
|
+
// 1. Fetch new bundle.
|
|
143
|
+
const bundle = await fetchBundle(catalogEntry.latest, catalogEntry.sourceRegistry);
|
|
144
|
+
const meta = buildPackageMeta(catalogEntry, catalogEntry.latest);
|
|
145
|
+
// 2. Snapshot current state for rollback.
|
|
146
|
+
const oldBundle = await loadBundle(id);
|
|
147
|
+
const oldRecord = Object.assign({}, installedRecord);
|
|
148
|
+
// 3. Push to server.
|
|
149
|
+
const manifest = {
|
|
150
|
+
id: meta.id,
|
|
151
|
+
type: meta.type,
|
|
152
|
+
label: catalogEntry.entry.label,
|
|
153
|
+
version: meta.version,
|
|
154
|
+
contractVersion: meta.contractVersion,
|
|
155
|
+
sourceRegistry: meta.sourceRegistry,
|
|
156
|
+
installedAt: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
const serverResult = await serverInstallPackage(manifest, bundle);
|
|
159
|
+
if (!serverResult.ok) {
|
|
160
|
+
throw new Error((_a = serverResult.error) !== null && _a !== void 0 ? _a : 'Server update failed');
|
|
161
|
+
}
|
|
162
|
+
// 4. Install locally (overwrites IndexedDB + re-registers).
|
|
163
|
+
const result = await installPackage(bundle, meta);
|
|
164
|
+
if (!result.success) {
|
|
165
|
+
// Rollback: restore old bundle and metadata.
|
|
166
|
+
if (oldBundle) {
|
|
167
|
+
try {
|
|
168
|
+
await savePackage(id, oldBundle, oldRecord);
|
|
169
|
+
}
|
|
170
|
+
catch (rollbackErr) {
|
|
171
|
+
console.warn(`[sh3-store] Rollback failed for "${id}":`, rollbackErr instanceof Error ? rollbackErr.message : rollbackErr);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
throw new Error((_b = result.error) !== null && _b !== void 0 ? _b : 'Local install failed during update');
|
|
175
|
+
}
|
|
176
|
+
await refreshInstalled();
|
|
177
|
+
}
|
|
98
178
|
storeContext = {
|
|
99
179
|
env,
|
|
100
180
|
state,
|
|
101
181
|
get isAdmin() { return ctx.isAdmin; },
|
|
102
182
|
refreshCatalog,
|
|
103
183
|
refreshInstalled,
|
|
184
|
+
updatePackage,
|
|
104
185
|
addRegistry,
|
|
105
186
|
removeRegistry,
|
|
106
187
|
};
|
|
@@ -127,10 +208,11 @@ export const storeShard = {
|
|
|
127
208
|
};
|
|
128
209
|
ctx.registerView('sh3-store:browse', browseFactory);
|
|
129
210
|
ctx.registerView('sh3-store:installed', installedFactory);
|
|
211
|
+
// refreshInstalled can run immediately (hits server, no env needed).
|
|
212
|
+
refreshInstalled();
|
|
130
213
|
},
|
|
131
214
|
autostart() {
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
// the launcher without requiring an app to declare it.
|
|
215
|
+
// Runs after env hydration, so registries are populated.
|
|
216
|
+
storeContext.refreshCatalog();
|
|
135
217
|
},
|
|
136
218
|
};
|
package/dist/tokens.css
CHANGED
|
@@ -32,12 +32,26 @@
|
|
|
32
32
|
--shell-accent: #6ea8fe;
|
|
33
33
|
--shell-accent-muted: #3a5580;
|
|
34
34
|
|
|
35
|
+
/* Inputs */
|
|
36
|
+
--shell-input-bg: #2a2a2a;
|
|
37
|
+
|
|
38
|
+
/* Semantic */
|
|
39
|
+
--shell-error: #f87171;
|
|
40
|
+
--shell-warning: #fbbf24;
|
|
41
|
+
--shell-success: #34d399;
|
|
42
|
+
|
|
35
43
|
/* Typography */
|
|
36
44
|
--shell-font-ui: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
37
45
|
--shell-font-mono: ui-monospace, "Cascadia Code", "Consolas", monospace;
|
|
38
46
|
--shell-font-size: 13px;
|
|
39
47
|
--shell-line: 1.45;
|
|
40
48
|
|
|
49
|
+
/* Radius */
|
|
50
|
+
--shell-radius-sm: 3px;
|
|
51
|
+
--shell-radius: 4px;
|
|
52
|
+
--shell-radius-md: 6px;
|
|
53
|
+
--shell-radius-lg: 8px;
|
|
54
|
+
|
|
41
55
|
/* Spacing */
|
|
42
56
|
--shell-pad-xs: 2px;
|
|
43
57
|
--shell-pad-sm: 4px;
|
package/dist/version.js
ADDED