sh3-core 0.13.3 → 0.14.0

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.
Files changed (66) hide show
  1. package/dist/api.d.ts +3 -0
  2. package/dist/api.js +3 -0
  3. package/dist/app/store/StoreView.svelte +15 -4
  4. package/dist/app/store/permissionConfirm.js +1 -2
  5. package/dist/app/store/storeApp.js +0 -1
  6. package/dist/app/store/storeShard.svelte.js +9 -18
  7. package/dist/app/store/storeTypes.d.ts +21 -0
  8. package/dist/app/store/storeTypes.js +33 -0
  9. package/dist/app/store/storeTypes.test.d.ts +1 -0
  10. package/dist/app/store/storeTypes.test.js +41 -0
  11. package/dist/app/store/updatePackage.test.js +1 -1
  12. package/dist/app/store/verbs.test.js +20 -17
  13. package/dist/host.js +2 -0
  14. package/dist/migrations/mode-id-rename.d.ts +9 -0
  15. package/dist/migrations/mode-id-rename.js +39 -0
  16. package/dist/migrations/mode-id-rename.test.d.ts +1 -0
  17. package/dist/migrations/mode-id-rename.test.js +52 -0
  18. package/dist/overlays/FloatFrame.svelte +18 -1
  19. package/dist/overlays/float.d.ts +12 -0
  20. package/dist/overlays/float.js +16 -0
  21. package/dist/overlays/float.test.js +97 -2
  22. package/dist/overlays/modal.js +1 -0
  23. package/dist/overlays/modal.test.js +17 -0
  24. package/dist/overlays/parentHost.d.ts +1 -0
  25. package/dist/overlays/parentHost.js +15 -0
  26. package/dist/overlays/parentHost.test.d.ts +1 -0
  27. package/dist/overlays/parentHost.test.js +39 -0
  28. package/dist/overlays/popup.js +1 -0
  29. package/dist/overlays/popup.test.js +19 -0
  30. package/dist/shell-shard/Terminal.svelte +85 -8
  31. package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
  32. package/dist/shell-shard/contract.d.ts +65 -0
  33. package/dist/shell-shard/contract.js +11 -0
  34. package/dist/shell-shard/dispatch-custom.test.d.ts +1 -0
  35. package/dist/shell-shard/dispatch-custom.test.js +104 -0
  36. package/dist/shell-shard/dispatch.d.ts +14 -1
  37. package/dist/shell-shard/dispatch.js +58 -5
  38. package/dist/shell-shard/modes/builtin.d.ts +2 -2
  39. package/dist/shell-shard/modes/builtin.js +8 -8
  40. package/dist/shell-shard/modes/prefs.js +1 -1
  41. package/dist/shell-shard/modes/prefs.test.js +13 -13
  42. package/dist/shell-shard/modes/registry.test.js +13 -13
  43. package/dist/shell-shard/output.d.ts +3 -0
  44. package/dist/shell-shard/output.js +75 -0
  45. package/dist/shell-shard/output.test.d.ts +1 -0
  46. package/dist/shell-shard/output.test.js +54 -0
  47. package/dist/shell-shard/registerShellMode.d.ts +13 -0
  48. package/dist/shell-shard/registerShellMode.js +14 -0
  49. package/dist/shell-shard/registerShellMode.test.d.ts +1 -0
  50. package/dist/shell-shard/registerShellMode.test.js +19 -0
  51. package/dist/shell-shard/shellShard.svelte.js +8 -1
  52. package/dist/shell-shard/terminal-dispatch.test.js +9 -9
  53. package/dist/shell-shard/toolbar/slots/ModeSlot.svelte +11 -51
  54. package/dist/shell-shard/toolbar/slots/ModeSlot.svelte.d.ts +2 -4
  55. package/dist/shell-shard/toolbar/slots.test.js +6 -6
  56. package/dist/shell-shard/verbs/index.js +2 -0
  57. package/dist/shell-shard/verbs/mode.d.ts +2 -0
  58. package/dist/shell-shard/verbs/mode.js +28 -0
  59. package/dist/shell-shard/verbs/mode.test.d.ts +1 -0
  60. package/dist/shell-shard/verbs/mode.test.js +43 -0
  61. package/dist/verbs/types.d.ts +11 -0
  62. package/dist/version.d.ts +1 -1
  63. package/dist/version.js +1 -1
  64. package/package.json +1 -1
  65. package/dist/app/store/InstalledView.svelte +0 -255
  66. package/dist/app/store/InstalledView.svelte.d.ts +0 -3
@@ -0,0 +1,28 @@
1
+ export const modeVerb = {
2
+ name: 'mode',
3
+ summary: 'List or switch shell modes. Usage: mode | mode <id>',
4
+ async run(ctx, args) {
5
+ const ts = Date.now();
6
+ if (args.length === 0) {
7
+ const modes = ctx.shell.listModes();
8
+ const lines = modes.map((m) => ` ${m.id.padEnd(12)} ${m.label}`);
9
+ ctx.scrollback.push({
10
+ kind: 'text',
11
+ stream: 'stdout',
12
+ chunks: [['Available modes:', ...lines].join('\n') + '\n'],
13
+ ts,
14
+ });
15
+ return;
16
+ }
17
+ const id = args[0];
18
+ const ok = ctx.shell.setMode(id);
19
+ if (!ok) {
20
+ ctx.scrollback.push({
21
+ kind: 'status',
22
+ text: `mode: unknown or restricted mode '${id}'`,
23
+ level: 'error',
24
+ ts,
25
+ });
26
+ }
27
+ },
28
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { modeVerb } from './mode';
3
+ function makeShell(modes = [{ id: 'bash', label: 'Bash' }, { id: 'sh3', label: 'SH3' }]) {
4
+ const setMode = vi.fn((id) => modes.some((m) => m.id === id));
5
+ const listModes = () => modes;
6
+ return { setMode, listModes };
7
+ }
8
+ function makeCtx(shellExt) {
9
+ const pushed = [];
10
+ const ctx = {
11
+ shell: { setMode: shellExt.setMode, listModes: shellExt.listModes },
12
+ scrollback: { push: (e) => pushed.push(e) },
13
+ session: {},
14
+ cwd: '/',
15
+ dispatch: async () => { },
16
+ fs: {},
17
+ };
18
+ return { ctx, pushed };
19
+ }
20
+ describe('mode verb', () => {
21
+ it('lists modes when called with no args', async () => {
22
+ const shell = makeShell();
23
+ const { ctx, pushed } = makeCtx(shell);
24
+ await modeVerb.run(ctx, []);
25
+ const dump = JSON.stringify(pushed);
26
+ expect(dump).toMatch(/bash/);
27
+ expect(dump).toMatch(/sh3/);
28
+ });
29
+ it('switches mode when called with a known id', async () => {
30
+ const shell = makeShell();
31
+ const { ctx } = makeCtx(shell);
32
+ await modeVerb.run(ctx, ['bash']);
33
+ expect(shell.setMode).toHaveBeenCalledWith('bash');
34
+ });
35
+ it('emits an error status when the id is unknown', async () => {
36
+ const shell = makeShell();
37
+ const { ctx, pushed } = makeCtx(shell);
38
+ await modeVerb.run(ctx, ['nope']);
39
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
40
+ expect(err).toBeDefined();
41
+ expect(err.text).toMatch(/nope/);
42
+ });
43
+ });
@@ -66,6 +66,17 @@ export interface ShellApi {
66
66
  userId: string;
67
67
  admin: boolean;
68
68
  };
69
+ /**
70
+ * Switch the active shell mode. The mode must be registered and visible
71
+ * to the current role. Returns true on success, false on unknown or
72
+ * role-restricted id (so verbs can surface a status).
73
+ */
74
+ setMode(id: string): boolean;
75
+ /** List currently visible modes for the current role. */
76
+ listModes(): readonly {
77
+ id: string;
78
+ label: string;
79
+ }[];
69
80
  }
70
81
  export interface VerbContext {
71
82
  shell: ShellApi;
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export declare const VERSION = "0.13.3";
2
+ export declare const VERSION = "0.14.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export const VERSION = '0.13.3';
2
+ export const VERSION = '0.14.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.13.3",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -1,255 +0,0 @@
1
- <script lang="ts">
2
- /*
3
- * InstalledView — lists installed packages with uninstall capability.
4
- *
5
- * Simpler than the browse view: a flat list of installed packages with
6
- * metadata (type, version, contract version, source registry, install date)
7
- * and an uninstall button per entry.
8
- */
9
-
10
- import { storeContext } from './storeShard.svelte';
11
- import { uninstallPackage } from '../../registry/installer';
12
- import { serverUninstallPackage } from '../../env/client';
13
- import { openPermissionConfirmModal } from './permissionConfirm';
14
- import type { InstalledPackage } from '../../registry/types';
15
-
16
- const ctx = storeContext;
17
-
18
- let uninstallingIds = $state<Set<string>>(new Set());
19
- let updatingIds = $state<Set<string>>(new Set());
20
- let updateError = $state<string | null>(null);
21
-
22
- async function handleUninstall(id: string) {
23
- if (uninstallingIds.has(id)) return;
24
-
25
- uninstallingIds = new Set([...uninstallingIds, id]);
26
- try {
27
- await serverUninstallPackage(id);
28
- await uninstallPackage(id);
29
- await ctx.refreshInstalled();
30
- } catch (err) {
31
- console.warn('[sh3-store] Uninstall failed:', err instanceof Error ? err.message : err);
32
- } finally {
33
- const next = new Set(uninstallingIds);
34
- next.delete(id);
35
- uninstallingIds = next;
36
- }
37
- }
38
-
39
- async function handleUpdate(id: string) {
40
- if (updatingIds.has(id)) return;
41
- updatingIds = new Set([...updatingIds, id]);
42
- updateError = null;
43
- try {
44
- await ctx.updatePackage(id, async (added, removed) => {
45
- const pkg = ctx.state.ephemeral.installed.find(
46
- (p: InstalledPackage) => p.id === id,
47
- );
48
- const target = ctx.state.ephemeral.updatable[id];
49
- if (!pkg || !target) return true;
50
- return openPermissionConfirmModal(pkg, target.latest.version, added, removed);
51
- });
52
- } catch (err) {
53
- updateError = err instanceof Error ? err.message : String(err);
54
- } finally {
55
- const next = new Set(updatingIds);
56
- next.delete(id);
57
- updatingIds = next;
58
- }
59
- }
60
-
61
- function handleRefresh() {
62
- ctx.refreshInstalled();
63
- }
64
-
65
- function formatDate(iso: string): string {
66
- try {
67
- return new Date(iso).toLocaleDateString(undefined, {
68
- year: 'numeric',
69
- month: 'short',
70
- day: 'numeric',
71
- });
72
- } catch {
73
- return iso;
74
- }
75
- }
76
- </script>
77
-
78
- <div class="installed-view">
79
- <header class="installed-header">
80
- <h2>Installed Packages</h2>
81
- <button onclick={handleRefresh}>Refresh</button>
82
- </header>
83
-
84
- {#if updateError}
85
- <div class="installed-error">{updateError}</div>
86
- {/if}
87
-
88
- {#if ctx.state.ephemeral.installed.length === 0}
89
- <div class="installed-empty">No packages installed.</div>
90
- {:else}
91
- <ul class="installed-list">
92
- {#each ctx.state.ephemeral.installed as pkg (pkg.id)}
93
- {@const uninstalling = uninstallingIds.has(pkg.id)}
94
- <li class="installed-item">
95
- <div class="installed-item-main">
96
- <span class="installed-item-id">{pkg.id}</span>
97
- <span class="installed-item-badge" class:badge-shard={pkg.type === 'shard'} class:badge-app={pkg.type === 'app'}>
98
- {pkg.type}
99
- </span>
100
- <span class="installed-item-version">{pkg.version}</span>
101
- </div>
102
- <div class="installed-item-meta">
103
- <span>Contract: v{pkg.contractVersion}</span>
104
- <span>Source: {pkg.sourceRegistry}</span>
105
- <span>Installed: {formatDate(pkg.installedAt)}</span>
106
- </div>
107
- <div class="installed-item-actions">
108
- {#if pkg.id in ctx.state.ephemeral.updatable}
109
- {@const target = ctx.state.ephemeral.updatable[pkg.id]}
110
- {@const updating = updatingIds.has(pkg.id)}
111
- <button
112
- class="installed-update-btn"
113
- onclick={() => handleUpdate(pkg.id)}
114
- disabled={updating || uninstalling}
115
- >
116
- {updating ? 'Updating...' : `Update -> ${target.latest.version}`}
117
- </button>
118
- {/if}
119
- <button
120
- class="installed-uninstall-btn"
121
- onclick={() => handleUninstall(pkg.id)}
122
- disabled={uninstalling}
123
- >
124
- {uninstalling ? 'Removing...' : 'Uninstall'}
125
- </button>
126
- </div>
127
- </li>
128
- {/each}
129
- </ul>
130
- {/if}
131
-
132
- </div>
133
-
134
- <style>
135
- .installed-view {
136
- font-family: var(--shell-font-ui);
137
- color: var(--shell-fg, #e0e0e0);
138
- background: var(--shell-bg, #1e1e1e);
139
- padding: 16px;
140
- height: 100%;
141
- overflow-y: auto;
142
- box-sizing: border-box;
143
- }
144
- .installed-header {
145
- display: flex;
146
- align-items: center;
147
- justify-content: space-between;
148
- margin-bottom: 16px;
149
- }
150
- .installed-header h2 {
151
- margin: 0;
152
- font-size: 1.25rem;
153
- font-weight: 600;
154
- }
155
- .installed-empty {
156
- text-align: center;
157
- padding: 32px 16px;
158
- color: var(--shell-fg-muted, #888);
159
- font-size: 0.875rem;
160
- }
161
- .installed-list {
162
- list-style: none;
163
- margin: 0;
164
- padding: 0;
165
- display: flex;
166
- flex-direction: column;
167
- gap: 8px;
168
- }
169
- .installed-item {
170
- background: var(--shell-input-bg, #2a2a2a);
171
- border: 1px solid var(--shell-border, #444);
172
- border-radius: var(--shell-radius-md);
173
- padding: 12px 14px;
174
- display: flex;
175
- flex-direction: column;
176
- gap: 6px;
177
- }
178
- .installed-item-main {
179
- display: flex;
180
- align-items: center;
181
- gap: 8px;
182
- }
183
- .installed-item-id {
184
- font-weight: 600;
185
- font-size: 0.9375rem;
186
- }
187
- .installed-item-badge {
188
- font-size: 0.6875rem;
189
- padding: 1px 6px;
190
- border-radius: var(--shell-radius-sm);
191
- text-transform: uppercase;
192
- font-weight: 600;
193
- letter-spacing: 0.04em;
194
- }
195
- .badge-shard {
196
- background: color-mix(in srgb, var(--shell-accent, #007acc) 25%, transparent);
197
- color: var(--shell-accent, #007acc);
198
- }
199
- .badge-app {
200
- background: color-mix(in srgb, var(--shell-success, #4caf50) 25%, transparent);
201
- color: var(--shell-success, #4caf50);
202
- }
203
- .installed-item-version {
204
- font-size: 0.75rem;
205
- color: var(--shell-fg-muted, #888);
206
- }
207
- .installed-item-meta {
208
- display: flex;
209
- gap: 16px;
210
- flex-wrap: wrap;
211
- font-size: 0.75rem;
212
- color: var(--shell-fg-muted, #888);
213
- }
214
- .installed-item-actions {
215
- display: flex;
216
- justify-content: flex-end;
217
- gap: 8px;
218
- }
219
- .installed-uninstall-btn {
220
- padding: 4px 12px;
221
- background: transparent;
222
- color: var(--shell-error, #d32f2f);
223
- border: 1px solid var(--shell-error, #d32f2f);
224
- font-size: 0.8125rem;
225
- }
226
- .installed-uninstall-btn:hover:not(:disabled) {
227
- background: color-mix(in srgb, var(--shell-error, #d32f2f) 15%, transparent);
228
- }
229
- .installed-uninstall-btn:disabled {
230
- opacity: 0.6;
231
- cursor: not-allowed;
232
- }
233
- .installed-update-btn {
234
- padding: 4px 12px;
235
- background: var(--shell-warning, #fbbf24);
236
- color: var(--shell-fg-on-warning, #1a1b1e);
237
- font-size: 0.8125rem;
238
- }
239
- .installed-update-btn:hover:not(:disabled) {
240
- filter: brightness(1.1);
241
- }
242
- .installed-update-btn:disabled {
243
- opacity: 0.6;
244
- cursor: not-allowed;
245
- }
246
- .installed-error {
247
- padding: 8px 12px;
248
- margin-bottom: 12px;
249
- background: color-mix(in srgb, var(--shell-error, #d32f2f) 15%, transparent);
250
- color: var(--shell-error, #d32f2f);
251
- border: 1px solid var(--shell-error, #d32f2f);
252
- border-radius: var(--shell-radius);
253
- font-size: 0.8125rem;
254
- }
255
- </style>
@@ -1,3 +0,0 @@
1
- declare const InstalledView: import("svelte").Component<Record<string, never>, {}, "">;
2
- type InstalledView = ReturnType<typeof InstalledView>;
3
- export default InstalledView;