sh3-core 0.5.0 → 0.5.2
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/api.d.ts +1 -0
- package/dist/api.js +2 -0
- package/dist/diagnostic/DiagnosticPanel.svelte +20 -0
- package/dist/shell-shard/ShellHome.svelte +7 -1
- package/dist/store/InstalledView.svelte +61 -0
- package/dist/store/StoreView.svelte +54 -1
- package/dist/store/storeApp.js +1 -1
- package/dist/store/storeShard.svelte.d.ts +2 -0
- package/dist/store/storeShard.svelte.js +85 -4
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +1 -1
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';
|
|
@@ -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>
|
|
@@ -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;
|
|
@@ -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)}
|
|
@@ -182,6 +216,7 @@
|
|
|
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;
|
|
@@ -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: 4px;
|
|
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: 4px;
|
|
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
|
|
@@ -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: 4px;
|
|
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;
|
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.
|
|
51
|
+
version: '0.2.0',
|
|
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
|
};
|
package/dist/version.js
ADDED