sh3-core 0.7.5 → 0.8.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.
- package/dist/api.d.ts +12 -2
- package/dist/api.js +13 -1
- package/dist/app/admin/SystemView.svelte +149 -11
- package/dist/app/store/StoreView.svelte +36 -7
- package/dist/app/store/storeShard.svelte.js +9 -3
- package/dist/app/store/verbs.js +8 -2
- package/dist/apps/lifecycle.d.ts +11 -0
- package/dist/apps/lifecycle.js +21 -1
- package/dist/apps/lifecycle.test.js +50 -1
- package/dist/apps/types.d.ts +7 -2
- package/dist/createShell.d.ts +2 -0
- package/dist/createShell.js +9 -7
- package/dist/documents/backends.d.ts +8 -0
- package/dist/documents/backends.js +87 -0
- package/dist/documents/backends.test.d.ts +1 -0
- package/dist/documents/backends.test.js +33 -0
- package/dist/documents/browse.d.ts +12 -0
- package/dist/documents/browse.js +19 -0
- package/dist/documents/browse.test.d.ts +1 -0
- package/dist/documents/browse.test.js +41 -0
- package/dist/documents/handle.js +5 -0
- package/dist/documents/http-backend.d.ts +4 -0
- package/dist/documents/http-backend.js +14 -0
- package/dist/documents/index.d.ts +1 -0
- package/dist/documents/index.js +1 -0
- package/dist/documents/journal-hook.d.ts +6 -0
- package/dist/documents/journal-hook.js +16 -0
- package/dist/documents/sync/activate-integration.test.d.ts +1 -0
- package/dist/documents/sync/activate-integration.test.js +37 -0
- package/dist/documents/sync/components/DocumentSyncExplorer.svelte +99 -0
- package/dist/documents/sync/components/DocumentSyncExplorer.svelte.d.ts +15 -0
- package/dist/documents/sync/components/SyncGrantPicker.svelte +70 -0
- package/dist/documents/sync/components/SyncGrantPicker.svelte.d.ts +12 -0
- package/dist/documents/sync/conflicts.d.ts +30 -0
- package/dist/documents/sync/conflicts.js +77 -0
- package/dist/documents/sync/conflicts.test.d.ts +1 -0
- package/dist/documents/sync/conflicts.test.js +71 -0
- package/dist/documents/sync/engine.d.ts +19 -0
- package/dist/documents/sync/engine.js +188 -0
- package/dist/documents/sync/engine.test.d.ts +1 -0
- package/dist/documents/sync/engine.test.js +169 -0
- package/dist/documents/sync/handle.d.ts +11 -0
- package/dist/documents/sync/handle.js +79 -0
- package/dist/documents/sync/handle.test.d.ts +1 -0
- package/dist/documents/sync/handle.test.js +56 -0
- package/dist/documents/sync/hash.d.ts +1 -0
- package/dist/documents/sync/hash.js +13 -0
- package/dist/documents/sync/hash.test.d.ts +1 -0
- package/dist/documents/sync/hash.test.js +20 -0
- package/dist/documents/sync/index.d.ts +5 -0
- package/dist/documents/sync/index.js +10 -0
- package/dist/documents/sync/journal.d.ts +30 -0
- package/dist/documents/sync/journal.js +179 -0
- package/dist/documents/sync/journal.test.d.ts +1 -0
- package/dist/documents/sync/journal.test.js +87 -0
- package/dist/documents/sync/observer.d.ts +3 -0
- package/dist/documents/sync/observer.js +45 -0
- package/dist/documents/sync/registry.d.ts +13 -0
- package/dist/documents/sync/registry.js +73 -0
- package/dist/documents/sync/registry.test.d.ts +1 -0
- package/dist/documents/sync/registry.test.js +53 -0
- package/dist/documents/sync/serialization.d.ts +5 -0
- package/dist/documents/sync/serialization.js +24 -0
- package/dist/documents/sync/serialization.test.d.ts +1 -0
- package/dist/documents/sync/serialization.test.js +26 -0
- package/dist/documents/sync/singleton.d.ts +11 -0
- package/dist/documents/sync/singleton.js +26 -0
- package/dist/documents/sync/tombstones.d.ts +19 -0
- package/dist/documents/sync/tombstones.js +58 -0
- package/dist/documents/sync/tombstones.test.d.ts +1 -0
- package/dist/documents/sync/tombstones.test.js +37 -0
- package/dist/documents/sync/types.d.ts +116 -0
- package/dist/documents/sync/types.js +27 -0
- package/dist/documents/sync/write-hook.test.d.ts +1 -0
- package/dist/documents/sync/write-hook.test.js +36 -0
- package/dist/documents/types.d.ts +18 -0
- package/dist/documents/types.js +6 -1
- package/dist/env/client.d.ts +10 -5
- package/dist/env/client.js +12 -4
- package/dist/layout/inspection.d.ts +17 -0
- package/dist/layout/inspection.js +53 -0
- package/dist/registry/installer.d.ts +10 -7
- package/dist/registry/installer.js +39 -35
- package/dist/registry/register.d.ts +17 -0
- package/dist/registry/register.js +22 -0
- package/dist/registry/register.test.d.ts +1 -0
- package/dist/registry/register.test.js +28 -0
- package/dist/shards/activate-browse.test.d.ts +1 -0
- package/dist/shards/activate-browse.test.js +36 -0
- package/dist/shards/activate-sync-registry.test.d.ts +1 -0
- package/dist/shards/activate-sync-registry.test.js +42 -0
- package/dist/shards/activate-tenantid.test.d.ts +1 -0
- package/dist/shards/activate-tenantid.test.js +21 -0
- package/dist/shards/activate.svelte.d.ts +12 -0
- package/dist/shards/activate.svelte.js +53 -2
- package/dist/shards/types.d.ts +43 -1
- package/dist/shell-shard/Terminal.svelte +140 -33
- package/dist/shell-shard/Terminal.svelte.d.ts +3 -0
- package/dist/shell-shard/auto-relocate.d.ts +12 -0
- package/dist/shell-shard/auto-relocate.js +20 -0
- package/dist/shell-shard/auto-relocate.test.d.ts +1 -0
- package/dist/shell-shard/auto-relocate.test.js +35 -0
- package/dist/shell-shard/dispatch.d.ts +15 -0
- package/dist/shell-shard/dispatch.js +56 -0
- package/dist/shell-shard/manifest.js +1 -1
- package/dist/shell-shard/modes/builtin.d.ts +5 -0
- package/dist/shell-shard/modes/builtin.js +18 -0
- package/dist/shell-shard/modes/prefs.d.ts +5 -0
- package/dist/shell-shard/modes/prefs.js +31 -0
- package/dist/shell-shard/modes/prefs.test.d.ts +1 -0
- package/dist/shell-shard/modes/prefs.test.js +46 -0
- package/dist/shell-shard/modes/registry.d.ts +7 -0
- package/dist/shell-shard/modes/registry.js +27 -0
- package/dist/shell-shard/modes/registry.test.d.ts +1 -0
- package/dist/shell-shard/modes/registry.test.js +35 -0
- package/dist/shell-shard/modes/types.d.ts +8 -0
- package/dist/shell-shard/modes/types.js +1 -0
- package/dist/shell-shard/protocol.d.ts +6 -0
- package/dist/shell-shard/shellShard.svelte.js +57 -5
- package/dist/shell-shard/tenant-fs-client.d.ts +24 -0
- package/dist/shell-shard/tenant-fs-client.js +44 -0
- package/dist/shell-shard/tenant-fs-client.test.d.ts +1 -0
- package/dist/shell-shard/tenant-fs-client.test.js +49 -0
- package/dist/shell-shard/terminal-dispatch.test.d.ts +1 -0
- package/dist/shell-shard/terminal-dispatch.test.js +53 -0
- package/dist/shell-shard/toolbar/Toolbar.svelte +62 -0
- package/dist/shell-shard/toolbar/Toolbar.svelte.d.ts +11 -0
- package/dist/shell-shard/toolbar/slots/FocusLockSlot.svelte +28 -0
- package/dist/shell-shard/toolbar/slots/FocusLockSlot.svelte.d.ts +7 -0
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte +102 -0
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte.d.ts +11 -0
- package/dist/shell-shard/toolbar/slots/TargetShardSlot.svelte +17 -0
- package/dist/shell-shard/toolbar/slots/TargetShardSlot.svelte.d.ts +6 -0
- package/dist/shell-shard/toolbar/slots.d.ts +17 -0
- package/dist/shell-shard/toolbar/slots.js +26 -0
- package/dist/shell-shard/toolbar/slots.test.d.ts +1 -0
- package/dist/shell-shard/toolbar/slots.test.js +28 -0
- package/dist/shell-shard/verbs/cat.d.ts +2 -0
- package/dist/shell-shard/verbs/cat.js +34 -0
- package/dist/shell-shard/verbs/cd.test.d.ts +1 -0
- package/dist/shell-shard/verbs/cd.test.js +56 -0
- package/dist/shell-shard/verbs/env.d.ts +2 -0
- package/dist/shell-shard/verbs/env.js +14 -0
- package/dist/shell-shard/verbs/index.js +9 -2
- package/dist/shell-shard/verbs/ls.d.ts +2 -0
- package/dist/shell-shard/verbs/ls.js +29 -0
- package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
- package/dist/shell-shard/verbs/ls.test.js +49 -0
- package/dist/shell-shard/verbs/session.d.ts +0 -1
- package/dist/shell-shard/verbs/session.js +58 -26
- package/dist/shell-shard/verbs/views.d.ts +2 -0
- package/dist/shell-shard/verbs/views.js +103 -2
- package/dist/verbs/types.d.ts +21 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
locked: boolean;
|
|
4
|
+
onToggle: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let { locked, onToggle }: Props = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<button class="focus-lock-btn" onclick={onToggle} title={locked ? 'Unlock focus' : 'Lock focus'}>
|
|
11
|
+
{locked ? '🔒' : '🔓'}
|
|
12
|
+
</button>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.focus-lock-btn {
|
|
16
|
+
background: none;
|
|
17
|
+
border: 1px solid var(--shell-border, #444);
|
|
18
|
+
border-radius: 3px;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
padding: 2px 5px;
|
|
21
|
+
font-size: 0.9em;
|
|
22
|
+
line-height: 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.focus-lock-btn:hover {
|
|
26
|
+
background: var(--shell-hover, #222);
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ShellMode, ShellRole } from '../../modes/types';
|
|
3
|
+
import type { ShellModeRegistry } from '../../modes/registry';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
mode: ShellMode;
|
|
7
|
+
role: ShellRole;
|
|
8
|
+
registry: ShellModeRegistry;
|
|
9
|
+
onSelect: (id: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { mode, role, registry, onSelect }: Props = $props();
|
|
13
|
+
|
|
14
|
+
let open = $state(false);
|
|
15
|
+
|
|
16
|
+
function select(id: string) {
|
|
17
|
+
open = false;
|
|
18
|
+
onSelect(id);
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
{#if role === 'admin'}
|
|
23
|
+
<div class="mode-slot">
|
|
24
|
+
<button class="mode-btn" onclick={() => (open = !open)}>
|
|
25
|
+
{mode.label} ▾
|
|
26
|
+
</button>
|
|
27
|
+
{#if open}
|
|
28
|
+
<ul class="mode-menu" role="menu">
|
|
29
|
+
{#each registry.list(role) as m (m.id)}
|
|
30
|
+
<li role="menuitem">
|
|
31
|
+
<button
|
|
32
|
+
class="mode-option"
|
|
33
|
+
class:active={m.id === mode.id}
|
|
34
|
+
onclick={() => select(m.id)}
|
|
35
|
+
>
|
|
36
|
+
{m.label}
|
|
37
|
+
</button>
|
|
38
|
+
</li>
|
|
39
|
+
{/each}
|
|
40
|
+
</ul>
|
|
41
|
+
{/if}
|
|
42
|
+
</div>
|
|
43
|
+
{:else}
|
|
44
|
+
<span class="mode-label">{mode.label}</span>
|
|
45
|
+
{/if}
|
|
46
|
+
|
|
47
|
+
<style>
|
|
48
|
+
.mode-slot {
|
|
49
|
+
position: relative;
|
|
50
|
+
display: inline-block;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.mode-btn {
|
|
54
|
+
background: none;
|
|
55
|
+
border: 1px solid var(--shell-border, #444);
|
|
56
|
+
color: var(--shell-fg, #ddd);
|
|
57
|
+
padding: 2px 6px;
|
|
58
|
+
border-radius: 3px;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
font-size: 0.85em;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.mode-btn:hover {
|
|
64
|
+
background: var(--shell-hover, #222);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.mode-menu {
|
|
68
|
+
position: absolute;
|
|
69
|
+
top: 100%;
|
|
70
|
+
left: 0;
|
|
71
|
+
margin: 2px 0 0;
|
|
72
|
+
padding: 0;
|
|
73
|
+
list-style: none;
|
|
74
|
+
background: var(--shell-bg, #111);
|
|
75
|
+
border: 1px solid var(--shell-border, #444);
|
|
76
|
+
border-radius: 3px;
|
|
77
|
+
z-index: 100;
|
|
78
|
+
min-width: 100%;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.mode-option {
|
|
82
|
+
display: block;
|
|
83
|
+
width: 100%;
|
|
84
|
+
background: none;
|
|
85
|
+
border: none;
|
|
86
|
+
color: var(--shell-fg, #ddd);
|
|
87
|
+
padding: 4px 10px;
|
|
88
|
+
text-align: left;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
font-size: 0.85em;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.mode-option:hover,
|
|
94
|
+
.mode-option.active {
|
|
95
|
+
background: var(--shell-hover, #222);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.mode-label {
|
|
99
|
+
font-size: 0.85em;
|
|
100
|
+
color: var(--shell-fg-dim, #888);
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ShellMode, ShellRole } from '../../modes/types';
|
|
2
|
+
import type { ShellModeRegistry } from '../../modes/registry';
|
|
3
|
+
interface Props {
|
|
4
|
+
mode: ShellMode;
|
|
5
|
+
role: ShellRole;
|
|
6
|
+
registry: ShellModeRegistry;
|
|
7
|
+
onSelect: (id: string) => void;
|
|
8
|
+
}
|
|
9
|
+
declare const ModeSlot: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type ModeSlot = ReturnType<typeof ModeSlot>;
|
|
11
|
+
export default ModeSlot;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
target: string | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
let { target }: Props = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<span class="target-shard" title="Target shard">→ {target ?? '—'}</span>
|
|
10
|
+
|
|
11
|
+
<style>
|
|
12
|
+
.target-shard {
|
|
13
|
+
font-size: 0.85em;
|
|
14
|
+
color: var(--shell-fg-dim, #888);
|
|
15
|
+
font-family: monospace;
|
|
16
|
+
}
|
|
17
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type { ShellMode, ShellRole } from '../modes/types';
|
|
3
|
+
export interface ShellSlotCtx {
|
|
4
|
+
mode: ShellMode;
|
|
5
|
+
role: ShellRole;
|
|
6
|
+
}
|
|
7
|
+
export interface ToolbarSlot {
|
|
8
|
+
id: string;
|
|
9
|
+
order: number;
|
|
10
|
+
visible(ctx: ShellSlotCtx): boolean;
|
|
11
|
+
component: Component<any>;
|
|
12
|
+
}
|
|
13
|
+
export declare class ToolbarSlotRegistry {
|
|
14
|
+
#private;
|
|
15
|
+
register(slot: ToolbarSlot): void;
|
|
16
|
+
list(ctx: ShellSlotCtx): ToolbarSlot[];
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
+
};
|
|
6
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
7
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
10
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11
|
+
};
|
|
12
|
+
var _ToolbarSlotRegistry_slots;
|
|
13
|
+
export class ToolbarSlotRegistry {
|
|
14
|
+
constructor() {
|
|
15
|
+
_ToolbarSlotRegistry_slots.set(this, []);
|
|
16
|
+
}
|
|
17
|
+
register(slot) {
|
|
18
|
+
__classPrivateFieldSet(this, _ToolbarSlotRegistry_slots, __classPrivateFieldGet(this, _ToolbarSlotRegistry_slots, "f").filter((s) => s.id !== slot.id).concat(slot), "f");
|
|
19
|
+
}
|
|
20
|
+
list(ctx) {
|
|
21
|
+
return __classPrivateFieldGet(this, _ToolbarSlotRegistry_slots, "f")
|
|
22
|
+
.filter((s) => s.visible(ctx))
|
|
23
|
+
.sort((a, b) => a.order - b.order);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
_ToolbarSlotRegistry_slots = new WeakMap();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ToolbarSlotRegistry } from './slots';
|
|
3
|
+
const A = { id: 'a', order: 20, visible: () => true, component: {} };
|
|
4
|
+
const B = { id: 'b', order: 10, visible: () => true, component: {} };
|
|
5
|
+
const C = { id: 'c', order: 30, visible: (ctx) => ctx.mode.id === 'user', component: {} };
|
|
6
|
+
const userCtx = { mode: { id: 'user', label: 'User', transport: 'none', autoRelocate: true }, role: 'user' };
|
|
7
|
+
const devCtx = { mode: { id: 'dev', label: 'Dev', transport: 'ws', autoRelocate: false, requiresRole: 'admin' }, role: 'admin' };
|
|
8
|
+
describe('ToolbarSlotRegistry', () => {
|
|
9
|
+
it('lists visible slots in order', () => {
|
|
10
|
+
const r = new ToolbarSlotRegistry();
|
|
11
|
+
r.register(A);
|
|
12
|
+
r.register(B);
|
|
13
|
+
r.register(C);
|
|
14
|
+
expect(r.list(userCtx).map((s) => s.id)).toEqual(['b', 'a', 'c']);
|
|
15
|
+
});
|
|
16
|
+
it('filters by visible', () => {
|
|
17
|
+
const r = new ToolbarSlotRegistry();
|
|
18
|
+
r.register(A);
|
|
19
|
+
r.register(C);
|
|
20
|
+
expect(r.list(devCtx).map((s) => s.id)).toEqual(['a']);
|
|
21
|
+
});
|
|
22
|
+
it('re-registering by id replaces', () => {
|
|
23
|
+
const r = new ToolbarSlotRegistry();
|
|
24
|
+
r.register(A);
|
|
25
|
+
r.register(Object.assign(Object.assign({}, A), { order: 99 }));
|
|
26
|
+
expect(r.list(userCtx)[0].order).toBe(99);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const catVerb = {
|
|
2
|
+
name: 'cat',
|
|
3
|
+
summary: 'Read a file into the scrollback.',
|
|
4
|
+
async run(ctx, args) {
|
|
5
|
+
if (args.length === 0) {
|
|
6
|
+
ctx.scrollback.push({
|
|
7
|
+
kind: 'status',
|
|
8
|
+
text: 'cat: missing file argument',
|
|
9
|
+
level: 'error',
|
|
10
|
+
ts: Date.now(),
|
|
11
|
+
});
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const path = args[0];
|
|
15
|
+
const target = ctx.session.cwd ? `${ctx.session.cwd}/${path}` : path;
|
|
16
|
+
try {
|
|
17
|
+
const text = await ctx.fs.read(target);
|
|
18
|
+
ctx.scrollback.push({
|
|
19
|
+
kind: 'text',
|
|
20
|
+
stream: 'stdout',
|
|
21
|
+
chunks: [text],
|
|
22
|
+
ts: Date.now(),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
ctx.scrollback.push({
|
|
27
|
+
kind: 'status',
|
|
28
|
+
text: `cat: ${err.message}`,
|
|
29
|
+
level: 'error',
|
|
30
|
+
ts: Date.now(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { cdVerb } from './session';
|
|
3
|
+
function makeCtx(initialCwd, fsStat) {
|
|
4
|
+
const pushed = [];
|
|
5
|
+
const session = { cwd: initialCwd };
|
|
6
|
+
const ctx = {
|
|
7
|
+
shell: {},
|
|
8
|
+
scrollback: { push: (e) => pushed.push(e) },
|
|
9
|
+
session,
|
|
10
|
+
cwd: initialCwd,
|
|
11
|
+
fs: {
|
|
12
|
+
list: async () => [],
|
|
13
|
+
stat: fsStat,
|
|
14
|
+
read: async () => '',
|
|
15
|
+
},
|
|
16
|
+
dispatch: async () => { },
|
|
17
|
+
};
|
|
18
|
+
return { ctx, session, pushed };
|
|
19
|
+
}
|
|
20
|
+
describe('cd verb', () => {
|
|
21
|
+
it('updates session.cwd on successful dir stat', async () => {
|
|
22
|
+
const { ctx, session } = makeCtx('', async (_path) => ({
|
|
23
|
+
name: 'projects',
|
|
24
|
+
kind: 'dir',
|
|
25
|
+
size: 0,
|
|
26
|
+
mtime: 0,
|
|
27
|
+
}));
|
|
28
|
+
await cdVerb.run(ctx, ['projects']);
|
|
29
|
+
expect(session.cwd).toBe('projects');
|
|
30
|
+
});
|
|
31
|
+
it('pushes error when target is a file, not a directory', async () => {
|
|
32
|
+
const { ctx, session, pushed } = makeCtx('', async (_path) => ({
|
|
33
|
+
name: 'readme.txt',
|
|
34
|
+
kind: 'file',
|
|
35
|
+
size: 100,
|
|
36
|
+
mtime: 0,
|
|
37
|
+
}));
|
|
38
|
+
await cdVerb.run(ctx, ['readme.txt']);
|
|
39
|
+
expect(session.cwd).toBe('');
|
|
40
|
+
const entry = pushed[0];
|
|
41
|
+
expect(entry.kind).toBe('status');
|
|
42
|
+
expect(entry.level).toBe('error');
|
|
43
|
+
expect(entry.text).toMatch(/not a directory/);
|
|
44
|
+
});
|
|
45
|
+
it('pushes error when fs.stat throws', async () => {
|
|
46
|
+
const { ctx, session, pushed } = makeCtx('', async (_path) => {
|
|
47
|
+
throw new Error('no such file');
|
|
48
|
+
});
|
|
49
|
+
await cdVerb.run(ctx, ['nonexistent']);
|
|
50
|
+
expect(session.cwd).toBe('');
|
|
51
|
+
const entry = pushed[0];
|
|
52
|
+
expect(entry.kind).toBe('status');
|
|
53
|
+
expect(entry.level).toBe('error');
|
|
54
|
+
expect(entry.text).toMatch(/no such file/);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import EnvTable from '../rich/EnvTable.svelte';
|
|
2
|
+
export const envVerb = {
|
|
3
|
+
name: 'env',
|
|
4
|
+
summary: 'Show the session environment.',
|
|
5
|
+
async run(ctx) {
|
|
6
|
+
const env = ctx.session.env;
|
|
7
|
+
ctx.scrollback.push({
|
|
8
|
+
kind: 'rich',
|
|
9
|
+
component: EnvTable,
|
|
10
|
+
props: { data: { env } },
|
|
11
|
+
ts: Date.now(),
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -7,9 +7,12 @@ import { clearVerb } from './clear';
|
|
|
7
7
|
import { historyVerb } from './history';
|
|
8
8
|
import { appsVerb, appVerb } from './apps';
|
|
9
9
|
import { shardsVerb } from './shards';
|
|
10
|
-
import { viewsVerb, openVerb, closeVerb } from './views';
|
|
10
|
+
import { viewsVerb, openVerb, closeVerb, popoutVerb, dockVerb } from './views';
|
|
11
11
|
import { zonesVerb, zoneVerb } from './zones';
|
|
12
|
-
import { pwdVerb, cdVerb,
|
|
12
|
+
import { pwdVerb, cdVerb, whoamiVerb } from './session';
|
|
13
|
+
import { envVerb } from './env';
|
|
14
|
+
import { lsVerb } from './ls';
|
|
15
|
+
import { catVerb } from './cat';
|
|
13
16
|
export function registerV1Verbs(ctx) {
|
|
14
17
|
ctx.registerVerb(makeHelpVerb());
|
|
15
18
|
ctx.registerVerb(clearVerb);
|
|
@@ -20,10 +23,14 @@ export function registerV1Verbs(ctx) {
|
|
|
20
23
|
ctx.registerVerb(viewsVerb);
|
|
21
24
|
ctx.registerVerb(openVerb);
|
|
22
25
|
ctx.registerVerb(closeVerb);
|
|
26
|
+
ctx.registerVerb(popoutVerb);
|
|
27
|
+
ctx.registerVerb(dockVerb);
|
|
23
28
|
ctx.registerVerb(zonesVerb);
|
|
24
29
|
ctx.registerVerb(zoneVerb);
|
|
25
30
|
ctx.registerVerb(pwdVerb);
|
|
26
31
|
ctx.registerVerb(cdVerb);
|
|
27
32
|
ctx.registerVerb(envVerb);
|
|
28
33
|
ctx.registerVerb(whoamiVerb);
|
|
34
|
+
ctx.registerVerb(lsVerb);
|
|
35
|
+
ctx.registerVerb(catVerb);
|
|
29
36
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const lsVerb = {
|
|
2
|
+
name: 'ls',
|
|
3
|
+
summary: 'List the current directory.',
|
|
4
|
+
async run(ctx) {
|
|
5
|
+
let entries;
|
|
6
|
+
try {
|
|
7
|
+
entries = await ctx.fs.list(ctx.session.cwd);
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
ctx.scrollback.push({
|
|
11
|
+
kind: 'status',
|
|
12
|
+
text: `ls: ${err.message}`,
|
|
13
|
+
level: 'error',
|
|
14
|
+
ts: Date.now(),
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const lines = entries
|
|
19
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
20
|
+
.map((e) => (e.kind === 'dir' ? `${e.name}/` : e.name))
|
|
21
|
+
.join('\n');
|
|
22
|
+
ctx.scrollback.push({
|
|
23
|
+
kind: 'text',
|
|
24
|
+
stream: 'stdout',
|
|
25
|
+
chunks: [lines || '(empty)'],
|
|
26
|
+
ts: Date.now(),
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { lsVerb } from './ls';
|
|
3
|
+
function makeCtx(cwd, fsList) {
|
|
4
|
+
const pushed = [];
|
|
5
|
+
const ctx = {
|
|
6
|
+
shell: {},
|
|
7
|
+
scrollback: { push: (e) => pushed.push(e) },
|
|
8
|
+
session: { cwd },
|
|
9
|
+
cwd,
|
|
10
|
+
fs: {
|
|
11
|
+
list: fsList,
|
|
12
|
+
stat: async () => ({ kind: 'dir', name: '', size: 0, mtime: 0 }),
|
|
13
|
+
read: async () => '',
|
|
14
|
+
},
|
|
15
|
+
dispatch: async () => { },
|
|
16
|
+
};
|
|
17
|
+
return { ctx, pushed };
|
|
18
|
+
}
|
|
19
|
+
describe('ls verb', () => {
|
|
20
|
+
it('lists entries sorted, dirs with trailing slash', async () => {
|
|
21
|
+
const { ctx, pushed } = makeCtx('', async () => [
|
|
22
|
+
{ name: 'zebra.txt', kind: 'file', size: 1, mtime: 0 },
|
|
23
|
+
{ name: 'apple', kind: 'dir', size: 0, mtime: 0 },
|
|
24
|
+
{ name: 'mango.txt', kind: 'file', size: 2, mtime: 0 },
|
|
25
|
+
]);
|
|
26
|
+
await lsVerb.run(ctx, []);
|
|
27
|
+
expect(pushed).toHaveLength(1);
|
|
28
|
+
const entry = pushed[0];
|
|
29
|
+
expect(entry.kind).toBe('text');
|
|
30
|
+
expect(entry.chunks[0]).toBe('apple/\nmango.txt\nzebra.txt');
|
|
31
|
+
});
|
|
32
|
+
it('shows (empty) for empty directory', async () => {
|
|
33
|
+
const { ctx, pushed } = makeCtx('', async () => []);
|
|
34
|
+
await lsVerb.run(ctx, []);
|
|
35
|
+
const entry = pushed[0];
|
|
36
|
+
expect(entry.kind).toBe('text');
|
|
37
|
+
expect(entry.chunks[0]).toBe('(empty)');
|
|
38
|
+
});
|
|
39
|
+
it('pushes status error when fs.list throws', async () => {
|
|
40
|
+
const { ctx, pushed } = makeCtx('', async () => {
|
|
41
|
+
throw new Error('permission denied');
|
|
42
|
+
});
|
|
43
|
+
await lsVerb.run(ctx, []);
|
|
44
|
+
const entry = pushed[0];
|
|
45
|
+
expect(entry.kind).toBe('status');
|
|
46
|
+
expect(entry.level).toBe('error');
|
|
47
|
+
expect(entry.text).toMatch(/permission denied/);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -1,18 +1,47 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Session verbs: pwd, cd,
|
|
2
|
+
* Session verbs: pwd, cd, whoami.
|
|
3
3
|
*
|
|
4
|
-
* cd
|
|
5
|
-
*
|
|
4
|
+
* cd validates the target path via ctx.fs.stat() then updates session.cwd
|
|
5
|
+
* locally. The server still owns the authoritative cwd for forwarded
|
|
6
|
+
* commands, but local verbs track it via session.cwd.
|
|
7
|
+
*
|
|
8
|
+
* envVerb lives in env.ts (separate Svelte import).
|
|
6
9
|
*/
|
|
7
|
-
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Path helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function joinRel(cwd, input) {
|
|
14
|
+
if (input.startsWith('/'))
|
|
15
|
+
return normalizeRel(input.slice(1));
|
|
16
|
+
const parts = [...cwd.split('/').filter(Boolean), ...input.split('/')];
|
|
17
|
+
const stack = [];
|
|
18
|
+
for (const p of parts) {
|
|
19
|
+
if (p === '' || p === '.')
|
|
20
|
+
continue;
|
|
21
|
+
if (p === '..') {
|
|
22
|
+
stack.pop();
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
stack.push(p);
|
|
26
|
+
}
|
|
27
|
+
return stack.join('/');
|
|
28
|
+
}
|
|
29
|
+
function normalizeRel(s) {
|
|
30
|
+
return s.split('/').filter((p) => p && p !== '.').join('/');
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Verbs
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
8
35
|
export const pwdVerb = {
|
|
9
36
|
name: 'pwd',
|
|
10
|
-
summary: 'Show the
|
|
37
|
+
summary: 'Show the current working directory.',
|
|
11
38
|
async run(ctx) {
|
|
39
|
+
const rel = ctx.session.cwd || '';
|
|
40
|
+
const display = rel.startsWith('/') ? rel : `/${rel}`;
|
|
12
41
|
ctx.scrollback.push({
|
|
13
42
|
kind: 'text',
|
|
14
43
|
stream: 'stdout',
|
|
15
|
-
chunks: [
|
|
44
|
+
chunks: [display + '\n'],
|
|
16
45
|
ts: Date.now(),
|
|
17
46
|
});
|
|
18
47
|
},
|
|
@@ -22,32 +51,35 @@ export const cdVerb = {
|
|
|
22
51
|
summary: 'Change the session working directory.',
|
|
23
52
|
async run(ctx, args) {
|
|
24
53
|
var _a;
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
54
|
+
const target = (_a = args[0]) !== null && _a !== void 0 ? _a : '';
|
|
55
|
+
if (target === '') {
|
|
56
|
+
ctx.session.cwd = '';
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const base = ctx.session.cwd;
|
|
60
|
+
const candidate = joinRel(base, target);
|
|
61
|
+
try {
|
|
62
|
+
const s = await ctx.fs.stat(candidate);
|
|
63
|
+
if (s.kind !== 'dir') {
|
|
64
|
+
ctx.scrollback.push({
|
|
65
|
+
kind: 'status',
|
|
66
|
+
text: `cd: not a directory: ${target}`,
|
|
67
|
+
level: 'error',
|
|
68
|
+
ts: Date.now(),
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
27
74
|
ctx.scrollback.push({
|
|
28
75
|
kind: 'status',
|
|
29
|
-
text:
|
|
30
|
-
level: '
|
|
76
|
+
text: `cd: ${err.message}`,
|
|
77
|
+
level: 'error',
|
|
31
78
|
ts: Date.now(),
|
|
32
79
|
});
|
|
33
80
|
return;
|
|
34
81
|
}
|
|
35
|
-
|
|
36
|
-
// dispatcher can intercept it and update session.cwd (see ws.ts).
|
|
37
|
-
ctx.session.send({ t: 'submit', line: `cd ${path}` });
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
export const envVerb = {
|
|
41
|
-
name: 'env',
|
|
42
|
-
summary: 'Show the session environment.',
|
|
43
|
-
async run(ctx) {
|
|
44
|
-
const env = ctx.session.env;
|
|
45
|
-
ctx.scrollback.push({
|
|
46
|
-
kind: 'rich',
|
|
47
|
-
component: EnvTable,
|
|
48
|
-
props: { data: { env } },
|
|
49
|
-
ts: Date.now(),
|
|
50
|
-
});
|
|
82
|
+
ctx.session.cwd = candidate;
|
|
51
83
|
},
|
|
52
84
|
};
|
|
53
85
|
export const whoamiVerb = {
|