sh3-core 0.2.0 → 0.3.1

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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Static manifest file included in every SH3 artifact directory.
3
+ * Generated at build time from the shard/app manifest in code.
4
+ * This is the single source of truth for package metadata —
5
+ * registries, servers, and install tools read this instead of
6
+ * requiring manual metadata entry.
7
+ */
8
+ export interface ArtifactManifest {
9
+ /** Unique package identifier. Matches the shard/app manifest id. */
10
+ id: string;
11
+ /** Whether this is a shard or app (or both via combo bundle). */
12
+ type: 'shard' | 'app' | 'combo';
13
+ /** Human-readable display name. */
14
+ label: string;
15
+ /** Version string (semver). */
16
+ version: string;
17
+ /** SH3 contract version this artifact was built against. */
18
+ contractVersion: number;
19
+ /** Relative path to the client bundle within the artifact directory. Omit if server-only. */
20
+ client?: string;
21
+ /** Relative path to the server bundle within the artifact directory. Omit if client-only. */
22
+ server?: string;
23
+ /** Short description (one or two sentences). */
24
+ description?: string;
25
+ /** Author name. */
26
+ author?: string;
27
+ /** Package dependencies. */
28
+ requires?: Array<{
29
+ id: string;
30
+ versionRange: string;
31
+ }>;
32
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/build.d.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  * });
18
18
  */
19
19
  import type { Plugin } from 'vite';
20
+ import type { ArtifactManifest } from './artifact';
20
21
  /**
21
22
  * Vite plugin that inlines extracted CSS into the JS bundle.
22
23
  *
@@ -27,3 +28,22 @@ import type { Plugin } from 'vite';
27
28
  * as a self-executing `<style>` injector, and delete the CSS files.
28
29
  */
29
30
  export declare function sh3CssInline(): Plugin;
31
+ /**
32
+ * Options for the sh3Artifact plugin.
33
+ */
34
+ export interface Sh3ArtifactOptions {
35
+ /** Path to pre-built server bundle. If provided, gets copied as server.js. */
36
+ serverEntry?: string;
37
+ /** Override manifest fields not extractable from code (description, author, requires). */
38
+ manifest?: Partial<Pick<ArtifactManifest, 'description' | 'author' | 'requires'>>;
39
+ }
40
+ /**
41
+ * Vite plugin that produces a distributable artifact directory after build.
42
+ *
43
+ * After Vite's lib build completes, this plugin:
44
+ * 1. Finds the entry chunk and renames it to client.js
45
+ * 2. Optionally copies a pre-built server bundle as server.js
46
+ * 3. Extracts manifest fields (id, label, version, type) from the source
47
+ * 4. Writes manifest.json alongside the bundles
48
+ */
49
+ export declare function sh3Artifact(options?: Sh3ArtifactOptions): Plugin;
package/dist/build.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * },
17
17
  * });
18
18
  */
19
- import { readFileSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs';
19
+ import { readFileSync, writeFileSync, unlinkSync, readdirSync, copyFileSync, existsSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
21
  /**
22
22
  * Vite plugin that inlines extracted CSS into the JS bundle.
@@ -83,3 +83,80 @@ export function sh3CssInline() {
83
83
  },
84
84
  };
85
85
  }
86
+ /**
87
+ * Vite plugin that produces a distributable artifact directory after build.
88
+ *
89
+ * After Vite's lib build completes, this plugin:
90
+ * 1. Finds the entry chunk and renames it to client.js
91
+ * 2. Optionally copies a pre-built server bundle as server.js
92
+ * 3. Extracts manifest fields (id, label, version, type) from the source
93
+ * 4. Writes manifest.json alongside the bundles
94
+ */
95
+ export function sh3Artifact(options = {}) {
96
+ let outDir = '';
97
+ let entryFileName = '';
98
+ return {
99
+ name: 'sh3-artifact',
100
+ apply: 'build',
101
+ enforce: 'post',
102
+ configResolved(config) {
103
+ outDir = config.build.outDir;
104
+ },
105
+ generateBundle(_outputOptions, bundle) {
106
+ // Find the entry chunk — the one Rollup marks as isEntry.
107
+ for (const [fileName, chunk] of Object.entries(bundle)) {
108
+ if (chunk.type === 'chunk' && chunk.isEntry) {
109
+ entryFileName = fileName;
110
+ break;
111
+ }
112
+ }
113
+ },
114
+ async closeBundle() {
115
+ var _a;
116
+ if (!entryFileName)
117
+ return;
118
+ const clientSrc = join(outDir, entryFileName);
119
+ const clientDest = join(outDir, 'client.js');
120
+ // Read source before potentially overwriting.
121
+ let source = '';
122
+ try {
123
+ source = readFileSync(clientSrc, 'utf-8');
124
+ }
125
+ catch (_b) {
126
+ console.warn('[sh3-artifact] Could not read entry chunk:', clientSrc);
127
+ return;
128
+ }
129
+ // Rename entry chunk to client.js (only if names differ).
130
+ if (clientSrc !== clientDest) {
131
+ writeFileSync(clientDest, source);
132
+ unlinkSync(clientSrc);
133
+ }
134
+ // --- Extract manifest fields via regex ---
135
+ const extract = (pattern) => {
136
+ const m = source.match(pattern);
137
+ return m ? m[1] : '';
138
+ };
139
+ const id = extract(/\bid\s*:\s*["']([^"']+)["']/);
140
+ const label = extract(/\blabel\s*:\s*["']([^"']+)["']/);
141
+ const version = extract(/\bversion\s*:\s*["']([^"']+)["']/);
142
+ const hasShard = /\bviews\s*:\s*\[/.test(source);
143
+ const hasApp = /\brequiredShards\s*:\s*\[/.test(source);
144
+ const type = hasShard && hasApp ? 'combo' : hasApp ? 'app' : 'shard';
145
+ // --- Optional server bundle ---
146
+ let hasServer = false;
147
+ if (options.serverEntry && existsSync(options.serverEntry)) {
148
+ copyFileSync(options.serverEntry, join(outDir, 'server.js'));
149
+ hasServer = true;
150
+ }
151
+ // --- Write manifest.json ---
152
+ const manifest = Object.assign(Object.assign({ id: id || 'unknown', type, label: label || id || 'unknown', version: version || '0.0.0', contractVersion: 1, client: 'client.js' }, (hasServer ? { server: 'server.js' } : {})), ((_a = options.manifest) !== null && _a !== void 0 ? _a : {}));
153
+ writeFileSync(join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
154
+ // --- Log summary ---
155
+ const files = ['manifest.json', 'client.js'];
156
+ if (hasServer)
157
+ files.push('server.js');
158
+ console.log(`[sh3-artifact] ${manifest.id}@${manifest.version} (${manifest.type}) → ${outDir}`);
159
+ console.log(`[sh3-artifact] files: ${files.join(', ')}`);
160
+ },
161
+ };
162
+ }
@@ -9,21 +9,14 @@ export interface ShellConfig {
9
9
  /** Additional apps to register */
10
10
  apps?: App[];
11
11
  /**
12
- * Packages to pre-install at boot if not already present in IndexedDB.
13
- * Used for environments that ship certain packages out of the box
14
- * (e.g. a registry host pre-includes sh3-registry).
12
+ * Packages discovered by the server at boot.
13
+ * The frontend fetches this list from /api/packages and passes it here.
15
14
  */
16
- preinstall?: Array<{
17
- /** Path to the client bundle file (relative to host root or absolute URL). */
18
- path: string;
19
- /** Path to the server bundle file (optional, for shards with server counterparts). */
20
- serverPath?: string;
21
- /** Package metadata for the install record. */
22
- meta: {
23
- id: string;
24
- type: 'shard' | 'app';
25
- version: string;
26
- };
15
+ discoveredPackages?: Array<{
16
+ id: string;
17
+ type: string;
18
+ version: string;
19
+ bundleUrl: string;
27
20
  }>;
28
21
  /** Mount target — CSS selector or element (defaults to '#app') */
29
22
  target?: string | HTMLElement;
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { mount } from 'svelte';
10
10
  import { Shell } from './index';
11
- import { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner, installPackage, listInstalledPackages, } from './host';
11
+ import { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner, } from './host';
12
12
  import { resolvePlatform } from './platform/index';
13
13
  export async function createShell(config) {
14
14
  var _a, _b;
@@ -23,57 +23,26 @@ export async function createShell(config) {
23
23
  if (platform.localOwner) {
24
24
  setLocalOwner();
25
25
  }
26
- // 1b. Pre-install packages that this host ships out of the box.
27
- if ((_a = config === null || config === void 0 ? void 0 : config.preinstall) === null || _a === void 0 ? void 0 : _a.length) {
28
- const installed = await listInstalledPackages();
29
- const installedIds = new Set(installed.map((p) => p.id));
30
- for (const pkg of config.preinstall) {
31
- if (installedIds.has(pkg.meta.id))
32
- continue;
26
+ // 1b. Load server-discovered packages (fetched by frontend from /api/packages).
27
+ if ((_a = config === null || config === void 0 ? void 0 : config.discoveredPackages) === null || _a === void 0 ? void 0 : _a.length) {
28
+ const { loadBundleModule } = await import('./registry/loader');
29
+ for (const pkg of config.discoveredPackages) {
33
30
  try {
34
- const res = await fetch(pkg.path);
31
+ const res = await fetch(pkg.bundleUrl);
35
32
  if (!res.ok) {
36
- console.warn(`[sh3] Pre-install fetch failed for "${pkg.meta.id}": HTTP ${res.status}`);
33
+ console.warn(`[sh3] Failed to fetch discovered package "${pkg.id}": HTTP ${res.status}`);
37
34
  continue;
38
35
  }
39
- const bundle = await res.arrayBuffer();
40
- const result = await installPackage(bundle, {
41
- id: pkg.meta.id,
42
- type: pkg.meta.type,
43
- version: pkg.meta.version,
44
- contractVersion: '1',
45
- sourceRegistry: 'local',
46
- integrity: '',
47
- hasServerBundle: !!pkg.serverPath,
48
- });
49
- if (!result.success) {
50
- console.warn(`[sh3] Pre-install failed for "${pkg.meta.id}":`, result.error);
51
- continue;
52
- }
53
- // Push server bundle if present
54
- if (pkg.serverPath) {
55
- try {
56
- const serverRes = await fetch(pkg.serverPath);
57
- if (serverRes.ok) {
58
- const serverBytes = await serverRes.arrayBuffer();
59
- const form = new FormData();
60
- form.append('shardId', pkg.meta.id);
61
- form.append('bundle', new Blob([serverBytes], { type: 'application/javascript' }), 'bundle.js');
62
- form.append('manifest', JSON.stringify(pkg.meta));
63
- await fetch('/api/server-shards/install', {
64
- method: 'POST',
65
- body: form,
66
- });
67
- }
68
- }
69
- catch (_c) {
70
- console.warn(`[sh3] Server bundle push failed for pre-install "${pkg.meta.id}"`);
71
- }
72
- }
73
- console.log(`[sh3] Pre-installed: ${pkg.meta.id}`);
36
+ const bytes = await res.arrayBuffer();
37
+ const loaded = await loadBundleModule(bytes);
38
+ for (const s of loaded.shards)
39
+ registerShard(s);
40
+ for (const a of loaded.apps)
41
+ registerApp(a);
42
+ console.log(`[sh3] Loaded discovered package: ${pkg.id}`);
74
43
  }
75
44
  catch (err) {
76
- console.warn(`[sh3] Pre-install error for "${pkg.meta.id}":`, err instanceof Error ? err.message : err);
45
+ console.warn(`[sh3] Failed to load discovered package "${pkg.id}":`, err);
77
46
  }
78
47
  }
79
48
  }
@@ -24,6 +24,7 @@
24
24
  const layout = $derived(inspectActiveLayout());
25
25
  const regShards = $derived(Array.from(registeredShards.values()));
26
26
  const actShards = $derived(Array.from(activeShards.keys()));
27
+
27
28
  </script>
28
29
 
29
30
  <div class="diagnostic">
@@ -68,6 +69,7 @@
68
69
  {/each}
69
70
  </ul>
70
71
  </section>
72
+
71
73
  </div>
72
74
 
73
75
  <style>
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Diagnostic routes view — lists all server API routes.
4
+ *
5
+ * Fetches the route table from GET /api/routes. GET routes render
6
+ * as clickable links for quick testing of parameterless endpoints.
7
+ */
8
+
9
+ interface ApiRoute { method: string; path: string; }
10
+ let routes: ApiRoute[] = $state([]);
11
+ let error: string | null = $state(null);
12
+
13
+ async function fetchRoutes() {
14
+ try {
15
+ const res = await fetch('/api/routes');
16
+ if (!res.ok) { error = `${res.status}`; return; }
17
+ routes = await res.json();
18
+ error = null;
19
+ } catch {
20
+ error = 'unavailable';
21
+ }
22
+ }
23
+
24
+ fetchRoutes();
25
+ </script>
26
+
27
+ <div class="diagnostic">
28
+ <h2>API Routes</h2>
29
+
30
+ {#if error}
31
+ <p class="muted">Server not reachable or route introspection unavailable ({error}).</p>
32
+ {:else}
33
+ <p class="muted">{routes.length} unique routes</p>
34
+ <ul>
35
+ {#each routes as route}
36
+ <li>
37
+ <span class="method" class:get={route.method === 'GET'}>{route.method}</span>
38
+ {#if route.method === 'GET'}
39
+ <a href={route.path} target="_blank" rel="noopener">{route.path}</a>
40
+ {:else}
41
+ <span class="path">{route.path}</span>
42
+ {/if}
43
+ </li>
44
+ {/each}
45
+ </ul>
46
+ {/if}
47
+ </div>
48
+
49
+ <style>
50
+ .diagnostic {
51
+ position: absolute;
52
+ inset: 0;
53
+ padding: 12px 16px;
54
+ overflow: auto;
55
+ background: var(--shell-bg);
56
+ color: var(--shell-fg);
57
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
58
+ font-size: 12px;
59
+ }
60
+ h2 {
61
+ margin: 0 0 12px;
62
+ color: var(--shell-accent);
63
+ font-size: 14px;
64
+ }
65
+ .muted {
66
+ color: var(--shell-fg-muted);
67
+ margin: 0 0 8px;
68
+ }
69
+ ul {
70
+ margin: 0;
71
+ padding: 0;
72
+ list-style: none;
73
+ }
74
+ li {
75
+ margin: 0;
76
+ padding: 2px 0;
77
+ display: flex;
78
+ align-items: baseline;
79
+ gap: 8px;
80
+ }
81
+ .method {
82
+ min-width: 6ch;
83
+ color: var(--shell-fg-muted);
84
+ flex-shrink: 0;
85
+ }
86
+ .method.get {
87
+ color: var(--shell-accent);
88
+ }
89
+ a {
90
+ color: var(--shell-accent);
91
+ text-decoration: none;
92
+ }
93
+ a:hover {
94
+ text-decoration: underline;
95
+ }
96
+ .path {
97
+ color: var(--shell-fg);
98
+ }
99
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const DiagnosticRoutes: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type DiagnosticRoutes = ReturnType<typeof DiagnosticRoutes>;
3
+ export default DiagnosticRoutes;
@@ -19,6 +19,7 @@ export const diagnosticApp = {
19
19
  activeTab: 0,
20
20
  tabs: [
21
21
  { slotId: 'diagnostic.main', viewId: 'diagnostic:panel', label: 'Diagnostic' },
22
+ { slotId: 'diagnostic.routes', viewId: 'diagnostic:routes', label: 'API Routes' },
22
23
  ],
23
24
  },
24
25
  };
@@ -25,6 +25,7 @@
25
25
  */
26
26
  import { mount, unmount } from 'svelte';
27
27
  import DiagnosticPanel from './DiagnosticPanel.svelte';
28
+ import DiagnosticRoutes from './DiagnosticRoutes.svelte';
28
29
  import DiagnosticPromptModal from './DiagnosticPromptModal.svelte';
29
30
  import { shell, getActiveApp, spliceIntoActiveLayout, inspectActiveLayout, } from '../api';
30
31
  export const diagnosticShard = {
@@ -32,7 +33,10 @@ export const diagnosticShard = {
32
33
  id: 'diagnostic',
33
34
  label: 'Diagnostic',
34
35
  version: '0.1.0',
35
- views: [{ id: 'diagnostic:panel', label: 'Diagnostic' }],
36
+ views: [
37
+ { id: 'diagnostic:panel', label: 'Diagnostic' },
38
+ { id: 'diagnostic:routes', label: 'API Routes' },
39
+ ],
36
40
  },
37
41
  activate(ctx) {
38
42
  const factory = {
@@ -46,6 +50,17 @@ export const diagnosticShard = {
46
50
  },
47
51
  };
48
52
  ctx.registerView('diagnostic:panel', factory);
53
+ const routesFactory = {
54
+ mount(container, _context) {
55
+ const instance = mount(DiagnosticRoutes, { target: container });
56
+ return {
57
+ unmount() {
58
+ unmount(instance);
59
+ },
60
+ };
61
+ },
62
+ };
63
+ ctx.registerView('diagnostic:routes', routesFactory);
49
64
  },
50
65
  autostart(ctx) {
51
66
  const state = ctx.state({
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './api';
2
2
  export { default as Shell } from './Shell.svelte';
3
+ export type { ArtifactManifest } from './artifact';
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A server shard is the optional backend counterpart to a client shard.
5
5
  * It runs in Node inside sh3-server and declares routes that are mounted
6
- * under `/s/<shard-id>/`. Server shards have full Node access — filesystem,
6
+ * under `/api/<shard-id>/`. Server shards have full Node access — filesystem,
7
7
  * child_process, network, etc. — and are trusted by the admin who installed
8
8
  * them.
9
9
  *
@@ -39,8 +39,8 @@ export interface ServerShard {
39
39
  id: string;
40
40
  /**
41
41
  * Called once at mount time. Register Hono routes on the provided router.
42
- * Routes are relative to `/s/<shard-id>/` — e.g. `router.get('/data', ...)`
43
- * becomes `GET /s/<shard-id>/data`.
42
+ * Routes are relative to `/api/<shard-id>/` — e.g. `router.get('/data', ...)`
43
+ * becomes `GET /api/<shard-id>/data`.
44
44
  *
45
45
  * May be async if the shard needs to initialise resources before serving.
46
46
  */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A server shard is the optional backend counterpart to a client shard.
5
5
  * It runs in Node inside sh3-server and declares routes that are mounted
6
- * under `/s/<shard-id>/`. Server shards have full Node access — filesystem,
6
+ * under `/api/<shard-id>/`. Server shards have full Node access — filesystem,
7
7
  * child_process, network, etc. — and are trusted by the admin who installed
8
8
  * them.
9
9
  *
@@ -98,7 +98,7 @@ export interface ShardManifest {
98
98
  /**
99
99
  * Optional filename of a server-side bundle for this shard. When present,
100
100
  * sh3-server loads the bundle at boot and mounts its routes at
101
- * `/s/<shard-id>/`. The server bundle runs in Node with full access.
101
+ * `/api/<shard-id>/`. The server bundle runs in Node with full access.
102
102
  * Only relevant for shards installed via the package store; framework-
103
103
  * shipped shards do not use this field.
104
104
  */
@@ -97,7 +97,7 @@
97
97
  <section class="shell-home-section">
98
98
  <h2 class="shell-home-section-title">Admin Mode</h2>
99
99
  <p class="shell-home-elevate-hint">
100
- Enter an API key to access admin apps like the Package Store.
100
+ Elevate Permissions
101
101
  </p>
102
102
  <form class="shell-home-elevate-form" onsubmit={(e) => { e.preventDefault(); handleElevate(); }}>
103
103
  <input
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"