sh3-server 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/app/assets/index-C3rCTpjL.js +17 -0
- package/app/assets/index-C3rCTpjL.js.map +1 -0
- package/app/assets/index-GfhVhkjD.css +1 -0
- package/app/index.html +2 -2
- package/dist/cli.js +9 -7
- package/dist/index.js +13 -3
- package/dist/packages.d.ts +20 -2
- package/dist/packages.js +64 -3
- package/dist/routes/docs.d.ts +2 -0
- package/dist/routes/docs.js +30 -0
- package/dist/settings.d.ts +13 -0
- package/dist/settings.js +33 -0
- package/dist/shell-shard/index.d.ts +2 -0
- package/dist/shell-shard/index.js +3 -1
- package/dist/shell-shard/session-manager.d.ts +2 -1
- package/dist/shell-shard/session-manager.js +15 -2
- package/dist/shell-shard/ws.js +14 -14
- package/dist/tenant-fs/http.d.ts +15 -0
- package/dist/tenant-fs/http.js +109 -0
- package/dist/tenant-fs/index.d.ts +4 -0
- package/dist/tenant-fs/index.js +4 -0
- package/dist/tenant-fs/paths.d.ts +23 -0
- package/dist/tenant-fs/paths.js +51 -0
- package/dist/tenant-fs/resolve.d.ts +16 -0
- package/dist/tenant-fs/resolve.js +48 -0
- package/dist/tenant-fs/session-required.d.ts +11 -0
- package/dist/tenant-fs/session-required.js +19 -0
- package/package.json +2 -2
- package/app/assets/index-25fXNyG3.js +0 -12
- package/app/assets/index-25fXNyG3.js.map +0 -1
- package/app/assets/index-BcQ1cruS.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.modal-frame.svelte-2tcvcm{position:absolute;inset:0;display:grid;place-items:center;pointer-events:auto}.modal-box.svelte-2tcvcm{background:var(--shell-grad-bg-elevated, var(--shell-bg-elevated));color:var(--shell-fg);border:1px solid var(--shell-border-strong);border-radius:var(--shell-radius);min-width:320px;max-width:min(640px,90vw);max-height:90vh;overflow:auto;box-shadow:0 20px 48px #00000080;outline:none}.popup-frame.svelte-mp81cl{position:absolute;background:var(--shell-grad-bg-elevated, var(--shell-bg-elevated));color:var(--shell-fg);border:1px solid var(--shell-border-strong);border-radius:var(--shell-radius-sm);box-shadow:0 8px 24px #0006;min-width:120px;outline:none;pointer-events:auto}.toast.svelte-12gwnj0{pointer-events:auto;display:flex;align-items:center;gap:var(--shell-pad-md);padding:var(--shell-pad-sm) var(--shell-pad-md);background:var(--shell-grad-bg-elevated, var(--shell-bg-elevated));color:var(--shell-fg);border:1px solid var(--shell-border-strong);border-left-width:3px;border-radius:var(--shell-radius-sm);box-shadow:0 8px 20px #0006;font-size:12px;min-width:220px;max-width:360px;cursor:pointer;animation:svelte-12gwnj0-toast-in .16s ease-out both}.toast-level.svelte-12gwnj0{text-transform:uppercase;font-family:var(--shell-font-mono);font-size:10px;letter-spacing:.5px;color:var(--shell-fg-muted)}.toast-message.svelte-12gwnj0{flex:1}.toast-info.svelte-12gwnj0{border-left-color:var(--shell-accent)}.toast-success.svelte-12gwnj0{border-left-color:#5cb176}.toast-warn.svelte-12gwnj0{border-left-color:#d6a84a}.toast-error.svelte-12gwnj0{border-left-color:#d06060}@keyframes svelte-12gwnj0-toast-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.sync-grant-picker.svelte-106syj7{display:grid;gap:.75rem;padding:1rem;border:1px solid var(--sh3-border, #444);border-radius:6px}.error.svelte-106syj7{color:var(--sh3-error, #c00)}footer.svelte-106syj7{display:flex;gap:.5rem;justify-content:flex-end}.document-sync-explorer.svelte-wc9uuh{display:grid;gap:.75rem}ul.svelte-wc9uuh{list-style:none;padding:0;margin:0}li.svelte-wc9uuh{display:flex;gap:.5rem;align-items:center}.shell-title.svelte-1ifjvh5{display:inline-block;line-height:0;overflow:hidden}.shell-title.svelte-1ifjvh5 canvas{transform:scale(var(--shell-title-scale));transform-origin:top left;display:block}.shell-title-fallback.svelte-1ifjvh5{margin:0;font-size:42px;color:var(--shell-accent);letter-spacing:2px}.shell-home.svelte-cpn2x2{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;padding:48px 24px;overflow:auto;background:var(--shell-grad-bg, var(--shell-bg));color:var(--shell-fg);font-family:system-ui,sans-serif}.shell-home-header.svelte-cpn2x2{text-align:center;margin-bottom:32px;display:flex;flex-direction:column;align-items:center;gap:12px}.shell-home-title-row.svelte-cpn2x2{display:flex;align-items:baseline;gap:6px}.shell-home-credit.svelte-cpn2x2{font-size:11px;color:var(--shell-fg-muted);letter-spacing:.04em;margin-top:-4px}.shell-home-credit.svelte-cpn2x2 a:where(.svelte-cpn2x2){color:var(--shell-fg-subtle);text-decoration:none;border-bottom:1px dotted var(--shell-fg-muted)}.shell-home-credit.svelte-cpn2x2 a:where(.svelte-cpn2x2):hover{color:var(--shell-accent);border-bottom-color:var(--shell-accent)}.shell-home-version.svelte-cpn2x2{font-size:14px;color:var(--shell-fg-subtle);letter-spacing:.04em}.shell-home-alpha.svelte-cpn2x2{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#fff;background:var(--shell-accent);padding:2px 8px;border-radius:8px;position:relative;top:-1px}.shell-home-empty.svelte-cpn2x2{color:var(--shell-fg-muted);font-style:italic}.shell-home-section.svelte-cpn2x2{width:100%;max-width:440px;margin-bottom:24px}.shell-home-section-title.svelte-cpn2x2{font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--shell-fg-subtle);margin:0 0 12px}.shell-home-list.svelte-cpn2x2{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}.shell-home-entry.svelte-cpn2x2{display:grid;grid-template-columns:1fr auto;grid-template-rows:auto auto;gap:4px 16px;align-items:center;padding:14px 18px;background:var(--shell-grad-bg-elevated, var(--shell-bg-elevated));border:1px solid var(--shell-border);border-radius:var(--shell-radius-md)}.shell-home-entry-label.svelte-cpn2x2{grid-column:1;grid-row:1;font-weight:600}.shell-home-entry-meta.svelte-cpn2x2{grid-column:1;grid-row:2;font-size:11px;color:var(--shell-fg-subtle)}.shell-home-launch.svelte-cpn2x2{grid-column:2;grid-row:1 / span 2;padding:8px 16px;font-weight:600}.shell-text.svelte-ypr8s{margin:0;padding:0 8px;font-family:var(--shell-font-mono, monospace);white-space:pre-wrap;word-break:break-word;color:var(--shell-fg, #ddd)}.shell-text.stderr.svelte-ypr8s{color:var(--shell-fg-error, #f88)}.shell-prompt.svelte-u0gb59{padding:4px 8px 0;font-family:var(--shell-font-mono, monospace);display:flex;gap:8px}.shell-prompt-cwd.svelte-u0gb59{color:var(--shell-fg-muted, #888)}.shell-prompt-arrow.svelte-u0gb59{color:var(--shell-accent, #6cf)}.shell-prompt-line.svelte-u0gb59{color:var(--shell-fg, #ddd)}.shell-status.svelte-nfxdpt{padding:2px 8px;font-family:var(--shell-font-mono, monospace);font-style:italic}.shell-status.info.svelte-nfxdpt{color:var(--shell-fg-muted, #888)}.shell-status.warn.svelte-nfxdpt{color:var(--shell-fg-warn, #fc6)}.shell-status.error.svelte-nfxdpt{color:var(--shell-fg-error, #f88)}.shell-rich.svelte-1rnhl05{padding:4px 8px}.shell-scrollback.svelte-isy3jt{flex:1 1 auto;overflow-y:auto;background:var(--shell-bg, #111);color:var(--shell-fg, #ddd)}.shell-input.svelte-1dfv2gk{display:flex;gap:8px;padding:4px 8px;border-top:1px solid var(--shell-border, #333);font-family:var(--shell-font-mono, monospace)}.shell-input-cwd.svelte-1dfv2gk{color:var(--shell-fg-muted, #888)}.shell-input-arrow.svelte-1dfv2gk{color:var(--shell-accent, #6cf)}.shell-input-field.svelte-1dfv2gk{flex:1 1 auto;background:transparent;border:0;outline:0;color:var(--shell-fg, #ddd);font:inherit}.shell-input.locked.svelte-1dfv2gk .shell-input-field:where(.svelte-1dfv2gk){opacity:.5;cursor:default}.toolbar.svelte-gnowwz{display:flex;align-items:center;gap:6px;padding:2px 6px;background:var(--shell-toolbar-bg, #1a1a1a);border-bottom:1px solid var(--shell-border, #333);flex-shrink:0;min-height:24px}.toolbar-toggle.svelte-gnowwz{background:none;border:none;color:var(--shell-fg-dim, #888);cursor:pointer;font-size:.7em;padding:0 2px;line-height:1}.toolbar-toggle.svelte-gnowwz:hover{color:var(--shell-fg, #ddd)}.toolbar-slots.svelte-gnowwz{display:flex;align-items:center;gap:8px}.mode-slot.svelte-1i4cqlj{position:relative;display:inline-block}.mode-btn.svelte-1i4cqlj{background:none;border:1px solid var(--shell-border, #444);color:var(--shell-fg, #ddd);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:.85em}.mode-btn.svelte-1i4cqlj:hover{background:var(--shell-hover, #222)}.mode-menu.svelte-1i4cqlj{position:absolute;top:100%;left:0;margin:2px 0 0;padding:0;list-style:none;background:var(--shell-bg, #111);border:1px solid var(--shell-border, #444);border-radius:3px;z-index:100;min-width:100%}.mode-option.svelte-1i4cqlj{display:block;width:100%;background:none;border:none;color:var(--shell-fg, #ddd);padding:4px 10px;text-align:left;cursor:pointer;font-size:.85em}.mode-option.svelte-1i4cqlj:hover,.mode-option.active.svelte-1i4cqlj{background:var(--shell-hover, #222)}.mode-label.svelte-1i4cqlj{font-size:.85em;color:var(--shell-fg-dim, #888)}.focus-lock-btn.svelte-1q6gbap{background:none;border:1px solid var(--shell-border, #444);border-radius:3px;cursor:pointer;padding:2px 5px;font-size:.9em;line-height:1}.focus-lock-btn.svelte-1q6gbap:hover{background:var(--shell-hover, #222)}.target-shard.svelte-1nn2m6x{font-size:.85em;color:var(--shell-fg-dim, #888);font-family:monospace}.shell-terminal.svelte-13komre{display:flex;flex-direction:column;height:100%;background:var(--shell-bg, #111);color:var(--shell-fg, #ddd)}.shell-rich-help.svelte-m248jy table:where(.svelte-m248jy){border-collapse:collapse;width:100%}.shell-rich-help.svelte-m248jy th:where(.svelte-m248jy),.shell-rich-help.svelte-m248jy td:where(.svelte-m248jy){padding:2px 8px;text-align:left}.shell-rich-help.svelte-m248jy button:where(.svelte-m248jy){background:none;border:0;color:var(--shell-link, #6cf);cursor:pointer;padding:0;font:inherit}.shell-rich-help.svelte-m248jy button:where(.svelte-m248jy):hover{text-decoration:underline}.shell-rich-history.svelte-o8m437 ol:where(.svelte-o8m437){list-style-position:inside;margin:0;padding:0}.shell-rich-history.svelte-o8m437 li:where(.svelte-o8m437){padding:2px 0}.shell-rich-history.svelte-o8m437 button:where(.svelte-o8m437){background:none;border:0;color:var(--shell-link, #6cf);cursor:pointer;padding:0;font:inherit;text-align:left}.shell-rich-history.svelte-o8m437 button:where(.svelte-o8m437):hover{text-decoration:underline}.shell-rich-apps.svelte-1u5krjz table:where(.svelte-1u5krjz){border-collapse:collapse;width:100%}.shell-rich-apps.svelte-1u5krjz th:where(.svelte-1u5krjz),.shell-rich-apps.svelte-1u5krjz td:where(.svelte-1u5krjz){padding:2px 8px;text-align:left}.shell-rich-apps.svelte-1u5krjz button:where(.svelte-1u5krjz){background:none;border:0;color:var(--shell-link, #6cf);cursor:pointer;padding:0;font:inherit}.shell-rich-apps.svelte-1u5krjz button:where(.svelte-1u5krjz):hover{text-decoration:underline}.shell-rich-appcard.svelte-npcp8i{padding:8px;border:1px solid var(--shell-border, #444)}.shell-rich-appcard.svelte-npcp8i h3:where(.svelte-npcp8i){margin:0 0 8px}.shell-rich-appcard.svelte-npcp8i p:where(.svelte-npcp8i){margin:4px 0}.shell-rich-shards.svelte-sc3ux4 table:where(.svelte-sc3ux4){border-collapse:collapse;width:100%}.shell-rich-shards.svelte-sc3ux4 th:where(.svelte-sc3ux4),.shell-rich-shards.svelte-sc3ux4 td:where(.svelte-sc3ux4){padding:2px 8px;text-align:left}.shell-rich-views.svelte-15elbr1 table:where(.svelte-15elbr1){border-collapse:collapse;width:100%}.shell-rich-views.svelte-15elbr1 th:where(.svelte-15elbr1),.shell-rich-views.svelte-15elbr1 td:where(.svelte-15elbr1){padding:2px 8px;text-align:left}.shell-rich-views.svelte-15elbr1 button:where(.svelte-15elbr1){background:none;border:0;color:var(--shell-link, #6cf);cursor:pointer;padding:0;font:inherit}.shell-rich-views.svelte-15elbr1 button:where(.svelte-15elbr1):hover{text-decoration:underline}.shell-rich-zones.svelte-1w7onag table:where(.svelte-1w7onag){border-collapse:collapse;width:100%}.shell-rich-zones.svelte-1w7onag th:where(.svelte-1w7onag),.shell-rich-zones.svelte-1w7onag td:where(.svelte-1w7onag){padding:2px 8px;text-align:left}.shell-rich-zonetree.svelte-13iiyr1 pre:where(.svelte-13iiyr1){overflow:auto;padding:8px;background:var(--shell-bg, #000);color:var(--shell-fg, #fff)}.shell-rich-env.svelte-1ox8vog table:where(.svelte-1ox8vog){border-collapse:collapse;width:100%}.shell-rich-env.svelte-1ox8vog th:where(.svelte-1ox8vog),.shell-rich-env.svelte-1ox8vog td:where(.svelte-1ox8vog){padding:2px 8px;text-align:left}.store-view.svelte-1aafzt0{font-family:var(--shell-font-ui);color:var(--shell-fg, #e0e0e0);background:var(--shell-bg, #1e1e1e);padding:16px;height:100%;overflow-y:auto;box-sizing:border-box}.store-header.svelte-1aafzt0{margin-bottom:16px}.store-header.svelte-1aafzt0 h2:where(.svelte-1aafzt0){margin:0 0 8px;font-size:1.25rem;font-weight:600}.store-controls.svelte-1aafzt0{display:flex;gap:8px;flex-wrap:wrap}.store-search.svelte-1aafzt0{flex:1;min-width:160px;padding:6px 10px;background:var(--shell-input-bg, #2a2a2a);color:var(--shell-fg, #e0e0e0);border:1px solid var(--shell-border, #444);border-radius:var(--shell-radius);font-family:inherit;font-size:.875rem}.store-search.svelte-1aafzt0::placeholder{color:var(--shell-fg-muted, #888)}.store-filter.svelte-1aafzt0{padding:6px 10px;background:var(--shell-input-bg, #2a2a2a);color:var(--shell-fg, #e0e0e0);border:1px solid var(--shell-border, #444);border-radius:var(--shell-radius);font-family:inherit;font-size:.875rem}.store-refresh.svelte-1aafzt0:disabled{opacity:.6;cursor:not-allowed}.store-error.svelte-1aafzt0{padding:8px 12px;margin-bottom:12px;background:color-mix(in srgb,var(--shell-error, #d32f2f) 15%,transparent);color:var(--shell-error, #d32f2f);border:1px solid var(--shell-error, #d32f2f);border-radius:var(--shell-radius);font-size:.8125rem}.store-grid.svelte-1aafzt0{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.store-card.svelte-1aafzt0{background:var(--shell-input-bg, #2a2a2a);border:1px solid var(--shell-border, #444);border-radius:var(--shell-radius-md);padding:14px;display:flex;flex-direction:column;gap:8px}.store-card.svelte-1aafzt0:hover{border-color:var(--shell-accent, #007acc)}.store-card-header.svelte-1aafzt0{display:flex;align-items:center;gap:10px}.store-card-icon.svelte-1aafzt0{width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center}.store-icon-img.svelte-1aafzt0{width:36px;height:36px;border-radius:var(--shell-radius);object-fit:cover}.store-icon-placeholder.svelte-1aafzt0{width:36px;height:36px;display:flex;align-items:center;justify-content:center;background:var(--shell-accent, #007acc);color:#fff;border-radius:var(--shell-radius);font-weight:700;font-size:1rem}.store-card-title.svelte-1aafzt0{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.store-card-label.svelte-1aafzt0{font-weight:600;font-size:.9375rem}.store-card-badge.svelte-1aafzt0{font-size:.6875rem;padding:1px 6px;border-radius:var(--shell-radius-sm);text-transform:uppercase;font-weight:600;letter-spacing:.04em}.badge-shard.svelte-1aafzt0{background:color-mix(in srgb,var(--shell-accent, #007acc) 25%,transparent);color:var(--shell-accent, #007acc)}.badge-app.svelte-1aafzt0{background:color-mix(in srgb,var(--shell-success, #4caf50) 25%,transparent);color:var(--shell-success, #4caf50)}.store-card-version.svelte-1aafzt0{font-size:.75rem;color:var(--shell-fg-muted, #888)}.store-card-desc.svelte-1aafzt0{margin:0;font-size:.8125rem;color:var(--shell-fg-muted, #888);line-height:1.4}.store-card-author.svelte-1aafzt0{font-size:.75rem;color:var(--shell-fg-muted, #888)}.store-card-warning.svelte-1aafzt0{font-size:.75rem;color:var(--shell-warning, #ff9800);padding:4px 8px;background:color-mix(in srgb,var(--shell-warning, #ff9800) 10%,transparent);border-radius:var(--shell-radius-sm)}.store-card-actions.svelte-1aafzt0{margin-top:auto;display:flex;justify-content:flex-end}.store-install-btn.svelte-1aafzt0{padding:5px 14px;font-size:.8125rem}.store-install-btn.svelte-1aafzt0:disabled{opacity:.6;cursor:not-allowed}.store-installed-label.svelte-1aafzt0{font-size:.8125rem;color:var(--shell-success, #4caf50);font-weight:600}.store-install-wrap.svelte-1aafzt0{display:flex;flex-direction:column;align-items:flex-end;gap:4px}.store-card-missing.svelte-1aafzt0{font-size:.75rem;color:var(--shell-warning, #ff9800)}.store-update-btn.svelte-1aafzt0{padding:5px 14px;background:var(--shell-warning, #ff9800);font-size:.8125rem}.store-update-btn.svelte-1aafzt0:hover:not(:disabled){filter:brightness(1.1)}.store-update-btn.svelte-1aafzt0:disabled{opacity:.6;cursor:not-allowed}.store-empty.svelte-1aafzt0{text-align:center;padding:32px 16px;color:var(--shell-fg-muted, #888);font-size:.875rem}.store-registries.svelte-1aafzt0{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}.store-registry-entry.svelte-1aafzt0{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:var(--shell-input-bg, #2a2a2a);border:1px solid var(--shell-border, #444);border-radius:var(--shell-radius);font-size:.8125rem}.store-registry-url.svelte-1aafzt0{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--shell-fg-muted, #888)}.store-registry-remove.svelte-1aafzt0{padding:2px 8px;background:transparent;color:var(--shell-error, #d32f2f);border:1px solid var(--shell-error, #d32f2f);border-radius:var(--shell-radius-sm);font-size:.75rem;flex-shrink:0;margin-left:8px}.store-add-registry.svelte-1aafzt0{display:flex;gap:8px;margin-bottom:12px}.store-registry-input.svelte-1aafzt0{flex:1;padding:6px 10px;background:var(--shell-input-bg, #2a2a2a);color:var(--shell-fg, #e0e0e0);border:1px solid var(--shell-border, #444);border-radius:var(--shell-radius);font-family:inherit;font-size:.8125rem}.store-registry-input.svelte-1aafzt0::placeholder{color:var(--shell-fg-muted, #888)}.store-add-btn.svelte-1aafzt0{font-size:.8125rem;white-space:nowrap}.store-add-btn.svelte-1aafzt0:disabled{opacity:.6;cursor:not-allowed}.installed-view.svelte-1hqclkp{font-family:var(--shell-font-ui);color:var(--shell-fg, #e0e0e0);background:var(--shell-bg, #1e1e1e);padding:16px;height:100%;overflow-y:auto;box-sizing:border-box}.installed-header.svelte-1hqclkp{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.installed-header.svelte-1hqclkp h2:where(.svelte-1hqclkp){margin:0;font-size:1.25rem;font-weight:600}.installed-empty.svelte-1hqclkp{text-align:center;padding:32px 16px;color:var(--shell-fg-muted, #888);font-size:.875rem}.installed-list.svelte-1hqclkp{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.installed-item.svelte-1hqclkp{background:var(--shell-input-bg, #2a2a2a);border:1px solid var(--shell-border, #444);border-radius:var(--shell-radius-md);padding:12px 14px;display:flex;flex-direction:column;gap:6px}.installed-item-main.svelte-1hqclkp{display:flex;align-items:center;gap:8px}.installed-item-id.svelte-1hqclkp{font-weight:600;font-size:.9375rem}.installed-item-badge.svelte-1hqclkp{font-size:.6875rem;padding:1px 6px;border-radius:var(--shell-radius-sm);text-transform:uppercase;font-weight:600;letter-spacing:.04em}.badge-shard.svelte-1hqclkp{background:color-mix(in srgb,var(--shell-accent, #007acc) 25%,transparent);color:var(--shell-accent, #007acc)}.badge-app.svelte-1hqclkp{background:color-mix(in srgb,var(--shell-success, #4caf50) 25%,transparent);color:var(--shell-success, #4caf50)}.installed-item-version.svelte-1hqclkp{font-size:.75rem;color:var(--shell-fg-muted, #888)}.installed-item-meta.svelte-1hqclkp{display:flex;gap:16px;flex-wrap:wrap;font-size:.75rem;color:var(--shell-fg-muted, #888)}.installed-item-actions.svelte-1hqclkp{display:flex;justify-content:flex-end;gap:8px}.installed-uninstall-btn.svelte-1hqclkp{padding:4px 12px;background:transparent;color:var(--shell-error, #d32f2f);border:1px solid var(--shell-error, #d32f2f);font-size:.8125rem}.installed-uninstall-btn.svelte-1hqclkp:hover:not(:disabled){background:color-mix(in srgb,var(--shell-error, #d32f2f) 15%,transparent)}.installed-uninstall-btn.svelte-1hqclkp:disabled{opacity:.6;cursor:not-allowed}.installed-update-btn.svelte-1hqclkp{padding:4px 12px;background:var(--shell-warning, #ff9800);font-size:.8125rem}.installed-update-btn.svelte-1hqclkp:hover:not(:disabled){filter:brightness(1.1)}.installed-update-btn.svelte-1hqclkp:disabled{opacity:.6;cursor:not-allowed}.installed-error.svelte-1hqclkp{padding:8px 12px;margin-bottom:12px;background:color-mix(in srgb,var(--shell-error, #d32f2f) 15%,transparent);color:var(--shell-error, #d32f2f);border:1px solid var(--shell-error, #d32f2f);border-radius:var(--shell-radius);font-size:.8125rem}.admin-users.svelte-4q792t{padding:24px;font-family:system-ui,sans-serif;color:var(--shell-fg)}.admin-users-header.svelte-4q792t{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.admin-users-header.svelte-4q792t h2:where(.svelte-4q792t){margin:0;font-size:18px}.admin-create-form.svelte-4q792t,.admin-edit-form.svelte-4q792t{display:flex;flex-direction:column;gap:8px;margin-bottom:16px;max-width:400px}.admin-input.svelte-4q792t{padding:8px 12px;background:var(--shell-bg, #1a1a2e);color:var(--shell-fg);border:1px solid var(--shell-border, #3a3a5c);border-radius:var(--shell-radius, 6px);font-size:13px}.admin-btn.svelte-4q792t{font-weight:600;font-size:13px}.admin-btn.svelte-4q792t:disabled{opacity:.6;cursor:not-allowed}.admin-btn-secondary.svelte-4q792t{background:transparent;color:var(--shell-fg-subtle);border:1px solid var(--shell-border);font-size:12px}.admin-btn-danger.svelte-4q792t{background:transparent;color:var(--shell-error, #d32f2f);border:1px solid var(--shell-error, #d32f2f);font-size:12px}.admin-user-list.svelte-4q792t{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.admin-user-item.svelte-4q792t{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:var(--shell-bg-elevated, #252540);border:1px solid var(--shell-border, #3a3a5c);border-radius:var(--shell-radius, 6px)}.admin-user-info.svelte-4q792t{display:flex;flex-direction:column;gap:2px}.admin-user-name.svelte-4q792t{font-weight:600}.admin-user-meta.svelte-4q792t{font-size:11px;color:var(--shell-fg-subtle)}.admin-user-actions.svelte-4q792t,.admin-edit-actions.svelte-4q792t{display:flex;gap:6px}.admin-error.svelte-4q792t{color:var(--shell-error, #d32f2f);font-size:13px}.admin-muted.svelte-4q792t{color:var(--shell-fg-muted);font-style:italic}.admin-auth.svelte-1hsgdb4{padding:24px;font-family:system-ui,sans-serif;color:var(--shell-fg)}.admin-auth.svelte-1hsgdb4 h2:where(.svelte-1hsgdb4){margin:0 0 16px;font-size:18px}.admin-auth-fields.svelte-1hsgdb4{display:flex;flex-direction:column;gap:16px;max-width:480px}.admin-toggle.svelte-1hsgdb4{display:flex;flex-wrap:wrap;align-items:center;gap:8px;cursor:pointer}.admin-toggle.svelte-1hsgdb4 input:where(.svelte-1hsgdb4){accent-color:var(--shell-accent, #7c7cf0)}.admin-hint.svelte-1hsgdb4{flex-basis:100%;font-size:11px;color:var(--shell-fg-muted);margin-left:24px}.admin-field.svelte-1hsgdb4{display:flex;flex-direction:column;gap:4px}.admin-field.svelte-1hsgdb4 span:where(.svelte-1hsgdb4){font-size:13px}.admin-input.svelte-1hsgdb4{padding:8px 12px;background:var(--shell-bg);color:var(--shell-fg);border:1px solid var(--shell-border);border-radius:var(--shell-radius, 6px);font-size:13px}.admin-input-sm.svelte-1hsgdb4{max-width:120px}.admin-btn.svelte-1hsgdb4{padding:8px 16px;font-weight:600;align-self:flex-start}.admin-btn.svelte-1hsgdb4:disabled{opacity:.6;cursor:not-allowed}.admin-error.svelte-1hsgdb4{margin-top:8px;color:var(--shell-error, #d32f2f);font-size:13px}.admin-muted.svelte-1hsgdb4{color:var(--shell-fg-muted);font-style:italic}.admin-system.svelte-1gkiloq{padding:24px;font-family:system-ui,sans-serif;color:var(--shell-fg)}.admin-system.svelte-1gkiloq h2:where(.svelte-1gkiloq){margin:0 0 16px;font-size:18px}.admin-system.svelte-1gkiloq h3:where(.svelte-1gkiloq){margin:0 0 8px;font-size:14px;text-transform:uppercase;letter-spacing:.05em;color:var(--shell-fg-subtle)}.admin-system-info.svelte-1gkiloq{margin-bottom:24px}.admin-system-row.svelte-1gkiloq{display:flex;gap:12px;padding:8px 0;border-bottom:1px solid var(--shell-border, #3a3a5c);font-size:13px}.admin-system-label.svelte-1gkiloq{color:var(--shell-fg-subtle);min-width:140px}.admin-system-section.svelte-1gkiloq{margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--shell-border, #3a3a5c)}.admin-system-section.svelte-1gkiloq:last-child{border-bottom:none}.admin-system-hint.svelte-1gkiloq{font-size:13px;color:var(--shell-fg-subtle);margin:0 0 12px}.admin-system-readout.svelte-1gkiloq{font-size:13px;margin:8px 0 12px}.admin-system-snaps.svelte-1gkiloq{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}.admin-snap.svelte-1gkiloq{padding:4px 10px;font-size:12px;background:transparent;border:1px solid var(--shell-border, #3a3a5c);color:var(--shell-fg);cursor:pointer;border-radius:var(--shell-radius-sm, 3px)}.admin-snap.active.svelte-1gkiloq{background:var(--shell-accent, #4a7bd4);color:var(--shell-bg);border-color:var(--shell-accent, #4a7bd4)}.admin-snap.svelte-1gkiloq:disabled{opacity:.5;cursor:not-allowed}.admin-system-actions.svelte-1gkiloq{display:flex;flex-direction:row;gap:8px;align-items:center;flex-wrap:wrap}.admin-btn.svelte-1gkiloq{padding:8px 16px;background:var(--shell-accent, #4a7bd4);color:var(--shell-bg);border:1px solid var(--shell-accent, #4a7bd4);font-weight:600;cursor:pointer;border-radius:var(--shell-radius-sm, 3px)}.admin-btn.svelte-1gkiloq:disabled{opacity:.5;cursor:not-allowed}.admin-btn-ghost.svelte-1gkiloq{padding:8px 16px;background:transparent;color:var(--shell-fg);border:1px solid var(--shell-border, #3a3a5c);cursor:pointer;border-radius:var(--shell-radius-sm, 3px)}.admin-btn-ghost.svelte-1gkiloq:disabled{opacity:.5;cursor:not-allowed}.admin-btn-danger.svelte-1gkiloq{padding:8px 16px;background:transparent;color:var(--shell-error, #d32f2f);border:1px solid var(--shell-error, #d32f2f);font-weight:600;cursor:pointer;border-radius:var(--shell-radius-sm, 3px)}.admin-btn-danger.svelte-1gkiloq:disabled{opacity:.6;cursor:not-allowed}.admin-error.svelte-1gkiloq{color:var(--shell-error, #d32f2f);font-size:13px}input[type=range].svelte-1gkiloq{width:100%;max-width:480px}.admin-keys.svelte-1afg5z5{padding:24px;font-family:system-ui,sans-serif;color:var(--shell-fg)}.admin-keys-header.svelte-1afg5z5{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.admin-keys-header.svelte-1afg5z5 h2:where(.svelte-1afg5z5){margin:0;font-size:18px}.admin-create-form.svelte-1afg5z5{display:flex;flex-direction:column;gap:8px;margin-bottom:16px;max-width:400px}.admin-input.svelte-1afg5z5{padding:8px 12px;background:var(--shell-bg, #1a1a2e);color:var(--shell-fg);border:1px solid var(--shell-border, #3a3a5c);border-radius:var(--shell-radius, 6px);font-size:13px}.admin-btn.svelte-1afg5z5{font-weight:600;font-size:13px}.admin-btn.svelte-1afg5z5:disabled{opacity:.6;cursor:not-allowed}.admin-btn-secondary.svelte-1afg5z5{background:transparent;color:var(--shell-fg-subtle);border:1px solid var(--shell-border);font-size:12px}.admin-btn-danger.svelte-1afg5z5{background:transparent;color:var(--shell-error, #d32f2f);border:1px solid var(--shell-error, #d32f2f);font-size:12px}.admin-key-list.svelte-1afg5z5{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.admin-key-item.svelte-1afg5z5{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:var(--shell-bg-elevated, #252540);border:1px solid var(--shell-border, #3a3a5c);border-radius:var(--shell-radius, 6px)}.admin-key-info.svelte-1afg5z5{display:flex;flex-direction:column;gap:2px}.admin-key-label.svelte-1afg5z5{font-weight:600}.admin-key-meta.svelte-1afg5z5{font-size:11px;color:var(--shell-fg-subtle)}.admin-key-value.svelte-1afg5z5{font-size:12px;color:var(--shell-fg-muted);background:var(--shell-bg, #1a1a2e);padding:2px 6px;border-radius:3px;margin-top:2px;word-break:break-all}.admin-key-actions.svelte-1afg5z5{display:flex;gap:6px;flex-shrink:0}.admin-muted.svelte-1afg5z5{color:var(--shell-fg-muted);font-style:italic}.admin-error.svelte-1afg5z5{color:var(--shell-error, #d32f2f);font-size:13px}button,input[type=button],input[type=submit],input[type=reset],.shell-base-button{padding:6px 14px;background:var(--shell-accent, #6ea8fe);color:#fff;border:none;border-radius:var(--shell-radius);cursor:pointer;font-family:inherit;font-size:.875rem;line-height:var(--shell-line)}button:hover,input[type=button]:hover,input[type=submit]:hover,input[type=reset]:hover,.shell-base-button:hover{filter:brightness(1.12)}button:active,input[type=button]:active,input[type=submit]:active,input[type=reset]:active,.shell-base-button:active{filter:brightness(.92)}.splitter.svelte-1adl4o1{display:flex;width:100%;height:100%;min-width:0;min-height:0}.splitter.horizontal.svelte-1adl4o1{flex-direction:row}.splitter.vertical.svelte-1adl4o1{flex-direction:column}.splitter-pane.svelte-1adl4o1{position:relative;min-width:0;min-height:0;overflow:hidden;display:flex}.horizontal.svelte-1adl4o1>.splitter-pane:where(.svelte-1adl4o1){flex-direction:row}.vertical.svelte-1adl4o1>.splitter-pane:where(.svelte-1adl4o1){flex-direction:column}.splitter-pane.collapsed.svelte-1adl4o1{overflow:visible}.pane-content.svelte-1adl4o1{flex:1 1 0;position:relative;min-width:0;min-height:0;overflow:hidden}.collapse-header.svelte-1adl4o1{appearance:none;flex:0 0 auto;display:flex;align-items:center;justify-content:center;background:var(--shell-grad-bg-elevated, var(--shell-bg-elevated));border:none;color:var(--shell-fg-muted);cursor:pointer;padding:0;font-size:10px}.collapse-header.svelte-1adl4o1:hover{color:var(--shell-fg);background:var(--shell-accent-muted)}body[data-dragging] .collapse-header.svelte-1adl4o1{pointer-events:none}.collapse-header.horizontal.svelte-1adl4o1{width:100%;height:100%;writing-mode:vertical-rl}.collapse-header.vertical.svelte-1adl4o1{width:100%;height:100%}.collapse-header.expanded.horizontal.svelte-1adl4o1{width:16px;height:100%;border-right:1px solid var(--shell-border)}.collapse-header.expanded.vertical.svelte-1adl4o1{width:100%;height:16px;border-bottom:1px solid var(--shell-border)}.splitter-handle.svelte-1adl4o1{flex:0 0 auto;background:var(--shell-border);transition:background-color .12s ease;touch-action:none}.splitter-handle.svelte-1adl4o1:hover,.splitter-handle.dragging.svelte-1adl4o1{background:var(--shell-accent)}body[data-dragging] .splitter-handle.svelte-1adl4o1{pointer-events:none}.splitter-handle.disabled.svelte-1adl4o1{cursor:default;pointer-events:none}.horizontal.svelte-1adl4o1>.splitter-handle:where(.svelte-1adl4o1){width:4px;cursor:col-resize}.vertical.svelte-1adl4o1>.splitter-handle:where(.svelte-1adl4o1){height:4px;cursor:row-resize}.tabbed-panel.svelte-1kxcyhf{display:flex;flex-direction:column;width:100%;height:100%;min-width:0;min-height:0;background:var(--shell-grad-bg, var(--shell-bg))}.tab-strip.svelte-1kxcyhf{position:relative;flex:0 0 auto;display:flex;gap:1px;background:var(--shell-grad-bg-sunken, var(--shell-bg-sunken));border-bottom:1px solid var(--shell-border);padding:0 var(--shell-pad-sm);user-select:none}.tab.svelte-1kxcyhf{appearance:none;background:transparent;border:none;color:var(--shell-fg-muted);font:inherit;font-size:12px;padding:var(--shell-pad-sm) var(--shell-pad-md);margin-top:2px;display:inline-flex;align-items:center;gap:var(--shell-pad-sm);cursor:pointer;border-top:2px solid transparent;border-radius:2px 2px 0 0;touch-action:none}.tab.svelte-1kxcyhf:hover{color:var(--shell-fg);background:var(--shell-grad-bg-elevated, var(--shell-bg-elevated))}.tab.active.svelte-1kxcyhf{color:var(--shell-fg);background:var(--shell-grad-bg, var(--shell-bg));border-top-color:var(--shell-accent)}.tab-icon.svelte-1kxcyhf{font-size:11px}.tab-label.svelte-1kxcyhf{white-space:nowrap}.tab-dirty.svelte-1kxcyhf{width:8px;height:8px;border-radius:50%;background:var(--shell-accent);flex-shrink:0}.tab-close.svelte-1kxcyhf{display:inline-flex;font-size:10px;line-height:1;padding:2px;border-radius:var(--shell-radius-sm);color:var(--shell-fg-muted);cursor:pointer;flex-shrink:0;margin-left:auto}.tab-close.svelte-1kxcyhf:hover{color:var(--shell-fg);background:var(--shell-bg-sunken)}.drop-indicator.svelte-1kxcyhf{position:absolute;width:2px;background:var(--shell-accent);box-shadow:0 0 6px var(--shell-accent);pointer-events:none;border-radius:1px}.tab-body.svelte-1kxcyhf{flex:1 1 auto;position:relative;min-width:0;min-height:0;overflow:hidden}.tab-body-pane.svelte-1kxcyhf{position:absolute;inset:0;min-width:0;min-height:0;display:none}.tab-body-pane.active.svelte-1kxcyhf{display:block}.slot.svelte-1czj0s8{position:relative;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden}.slot-placeholder.svelte-1czj0s8{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--shell-pad-sm);color:var(--shell-fg-muted);font-size:12px;text-align:center;padding:var(--shell-pad-md);background:repeating-linear-gradient(45deg,var(--shell-bg) 0 10px,var(--shell-bg-elevated) 10px 20px);border:1px dashed var(--shell-border-strong);pointer-events:none}.slot-id.svelte-1czj0s8{color:var(--shell-fg);font-family:var(--shell-font-mono);font-size:13px}.slot-meta.svelte-1czj0s8 code:where(.svelte-1czj0s8){font-family:var(--shell-font-mono);color:var(--shell-accent)}.slot-dims.svelte-1czj0s8{font-family:var(--shell-font-mono);color:var(--shell-fg-subtle);font-size:11px}.slot-drop-zone.svelte-re71zu{position:absolute;inset:0;pointer-events:none}.slot-drop-zone.active.svelte-re71zu{pointer-events:auto}.quad-highlight.svelte-re71zu{position:absolute;background:var(--shell-accent);opacity:.18;border:1px dashed var(--shell-accent);pointer-events:none;transition:inset 80ms ease}.quad-highlight.quad-left.svelte-re71zu{inset:0 50% 0 0}.quad-highlight.quad-right.svelte-re71zu{inset:0 0 0 50%}.quad-highlight.quad-top.svelte-re71zu{inset:0 0 50%}.quad-highlight.quad-bottom.svelte-re71zu{inset:50% 0 0}.quad-target.svelte-re71zu{position:absolute;pointer-events:none}.quad-target.quad-left.svelte-re71zu{inset:0 50% 0 0}.quad-target.quad-right.svelte-re71zu{inset:0 0 0 50%}.quad-target.quad-top.svelte-re71zu{inset:0 0 50%}.quad-target.quad-bottom.svelte-re71zu{inset:50% 0 0}.tab-slot-wrapper.svelte-1gw9g38,.leaf-slot-wrapper.svelte-1gw9g38{position:absolute;inset:0;min-width:0;min-height:0}.empty-tabs-placeholder.svelte-1gw9g38{width:100%;height:100%;min-width:0;min-height:0;position:relative}.empty-tabs-default.svelte-1gw9g38{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--shell-fg-muted);font-size:12px;background:repeating-linear-gradient(45deg,var(--shell-bg) 0 10px,var(--shell-bg-elevated) 10px 20px);border:1px dashed var(--shell-border-strong)}.empty-tabs-custom.svelte-1gw9g38{position:absolute;inset:0}.empty-tabs-drop.svelte-1gw9g38{position:absolute;inset:0;pointer-events:auto}.drag-preview.svelte-133fiip{position:absolute;display:inline-flex;align-items:center;gap:var(--shell-pad-sm);padding:var(--shell-pad-sm) var(--shell-pad-md);background:var(--shell-grad-bg-elevated, var(--shell-bg-elevated));color:var(--shell-fg);border:1px solid var(--shell-accent);border-radius:var(--shell-radius-sm);box-shadow:0 8px 24px #00000080;font-size:12px;font-family:var(--shell-font-ui);pointer-events:none;opacity:.9}.drag-preview-icon.svelte-133fiip{font-size:11px}.drag-preview-label.svelte-133fiip{white-space:nowrap}.sh3-float-frame.svelte-1p734al{position:absolute;display:flex;flex-direction:column;background:var(--shell-grad-bg-elevated, var(--shell-bg-elevated, #1e1e1e));color:var(--shell-fg);border:1px solid var(--shell-border-strong);border-radius:var(--shell-radius);box-shadow:0 8px 24px #0006;pointer-events:auto}.sh3-float-header.svelte-1p734al{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:var(--shell-bg, #111);cursor:move;user-select:none;border-bottom:1px solid var(--shell-border-strong);border-top-left-radius:var(--shell-radius);border-top-right-radius:var(--shell-radius);flex-shrink:0}.sh3-float-title.svelte-1p734al{font-size:12px;color:var(--shell-fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sh3-float-close.svelte-1p734al{background:transparent;border:none;color:var(--shell-fg);font-size:16px;line-height:1;cursor:pointer;padding:0 4px;flex-shrink:0}.sh3-float-body.svelte-1p734al{flex:1;position:relative;overflow:hidden;min-height:0}.sh3-float-layer.svelte-1rzcset{position:absolute;inset:0;pointer-events:none}.guest-banner.svelte-pymkh{display:flex;align-items:center;justify-content:center;gap:12px;padding:6px var(--shell-pad-md, 12px);background:color-mix(in srgb,var(--shell-accent, #7c7cf0) 15%,transparent);border-bottom:1px solid var(--shell-border, #3a3a5c);font-size:12px;color:var(--shell-fg, #e0e0e0)}.guest-banner-action.svelte-pymkh{padding:3px 10px;color:var(--shell-bg, #1a1a2e);font-size:11px;font-weight:600}.guest-signin-overlay.svelte-pymkh{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:#00000080;z-index:9999}.guest-signin-card.svelte-pymkh{display:flex;flex-direction:column;gap:12px;padding:32px;background:var(--shell-bg-elevated, #252540);border:1px solid var(--shell-border, #3a3a5c);border-radius:var(--shell-radius-lg, 12px);min-width:300px}.guest-signin-form.svelte-pymkh{display:flex;flex-direction:column;gap:10px}.guest-signin-input.svelte-pymkh{padding:8px 12px;background:var(--shell-bg, #1a1a2e);color:var(--shell-fg, #e0e0e0);border:1px solid var(--shell-border, #3a3a5c);border-radius:var(--shell-radius, 6px);font-size:13px}.guest-signin-input.svelte-pymkh::placeholder{color:var(--shell-fg-muted, #888)}.guest-signin-actions.svelte-pymkh{display:flex;gap:8px}.guest-signin-btn.svelte-pymkh{flex:1;padding:8px;color:var(--shell-bg, #1a1a2e);font-weight:600}.guest-signin-btn.svelte-pymkh:disabled{opacity:.6;cursor:not-allowed}.guest-signin-cancel.svelte-pymkh{padding:8px 12px;background:transparent;color:var(--shell-fg-subtle, #aaa);border:1px solid var(--shell-border, #3a3a5c)}.guest-signin-error.svelte-pymkh{padding:6px 10px;font-size:12px;color:var(--shell-error, #d32f2f);background:color-mix(in srgb,var(--shell-error, #d32f2f) 10%,transparent);border-radius:var(--shell-radius, 6px)}.shell.svelte-187fra4{display:grid;grid-template-rows:var(--shell-tabbar-height) auto 1fr var(--shell-statusbar-height);height:100%;width:100%;position:relative;background:var(--shell-grad-bg, var(--shell-bg));color:var(--shell-fg)}.shell-tabbar.svelte-187fra4{display:flex;align-items:center;gap:var(--shell-pad-md);padding:0 var(--shell-pad-md);background:var(--shell-grad-bg-elevated, var(--shell-bg-elevated));border-bottom:1px solid var(--shell-border);user-select:none}.shell-tabbar-brand.svelte-187fra4{font-weight:600;color:var(--shell-accent);letter-spacing:.5px}.shell-content.svelte-187fra4{position:relative;overflow:hidden;background:var(--shell-grad-bg, var(--shell-bg));min-width:0;min-height:0}.shell-statusbar.svelte-187fra4{display:flex;align-items:center;justify-content:space-between;padding:0 var(--shell-pad-md);background:var(--shell-grad-bg-sunken, var(--shell-bg-sunken));border-top:1px solid var(--shell-border);color:var(--shell-fg-muted);font-size:11px;user-select:none}.shell-overlays.svelte-187fra4,.shell-overlay-root.svelte-187fra4{position:absolute;inset:0;pointer-events:none}.shell-tabbar-home-button.svelte-187fra4{display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;background:transparent;color:var(--shell-fg-muted);border:1px solid var(--shell-border)}.shell-tabbar-home-button.svelte-187fra4:hover:not(:disabled){color:var(--shell-fg);border-color:var(--shell-fg-muted)}.shell-tabbar-home-button.svelte-187fra4:disabled{color:var(--shell-fg-subtle);border-color:var(--shell-border);cursor:default}.shell-tabbar-home-icon.svelte-187fra4{width:14px;height:14px}.shell-tabbar-user.svelte-187fra4{display:flex;align-items:center;gap:6px;margin-left:auto}.shell-tabbar-user-name.svelte-187fra4{font-size:12px;color:var(--shell-fg-subtle)}.shell-tabbar-tag.svelte-187fra4{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#fff;background:var(--shell-accent);padding:1px 6px;border-radius:6px}.shell-tabbar-signout.svelte-187fra4{display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;background:transparent;color:var(--shell-fg-muted);border:1px solid var(--shell-border)}.shell-tabbar-signout.svelte-187fra4:hover{color:var(--shell-fg);border-color:var(--shell-fg-muted)}.shell-tabbar-signout-icon.svelte-187fra4{width:14px;height:14px}.signin-wall.svelte-lh4k15{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:var(--shell-grad-bg, var(--shell-bg, #1a1a2e));color:var(--shell-fg, #e0e0e0);font-family:system-ui,sans-serif}.signin-card.svelte-lh4k15{display:flex;flex-direction:column;align-items:center;gap:16px;padding:48px 40px;background:var(--shell-grad-bg-elevated, var(--shell-bg-elevated, #252540));border:1px solid var(--shell-border, #3a3a5c);border-radius:var(--shell-radius-lg, 12px);min-width:320px}.signin-brand.svelte-lh4k15{margin:0 0 8px;font-size:42px;color:var(--shell-accent, #7c7cf0);letter-spacing:2px}.signin-form.svelte-lh4k15{display:flex;flex-direction:column;gap:12px;width:100%}.signin-input.svelte-lh4k15{padding:10px 14px;background:var(--shell-bg, #1a1a2e);color:var(--shell-fg, #e0e0e0);border:1px solid var(--shell-border, #3a3a5c);border-radius:var(--shell-radius, 6px);font-size:14px}.signin-input.svelte-lh4k15::placeholder{color:var(--shell-fg-muted, #888)}.signin-btn.svelte-lh4k15{padding:10px 16px;color:var(--shell-bg, #1a1a2e);font-weight:600;font-size:14px}.signin-btn.svelte-lh4k15:disabled{opacity:.6;cursor:not-allowed}.signin-link.svelte-lh4k15{background:none;color:var(--shell-accent, #7c7cf0);font-size:13px;padding:0}.signin-link.svelte-lh4k15:hover{text-decoration:underline}.signin-error.svelte-lh4k15{padding:8px 12px;font-size:13px;color:var(--shell-error, #d32f2f);background:color-mix(in srgb,var(--shell-error, #d32f2f) 10%,transparent);border-radius:var(--shell-radius, 6px);width:100%;text-align:center}:root{--shell-bg: #1a1b1e;--shell-bg-elevated: #22232a;--shell-bg-sunken: #141518;--shell-border: #2e3038;--shell-border-strong: #3c3f4a;--shell-fg: #e4e6eb;--shell-fg-muted: #9aa0aa;--shell-fg-subtle: #6b7280;--shell-accent: #6ea8fe;--shell-accent-muted: #3a5580;--shell-input-bg: #2a2a2a;--shell-error: #f87171;--shell-warning: #fbbf24;--shell-success: #34d399;--shell-font-ui: system-ui, -apple-system, "Segoe UI", sans-serif;--shell-font-mono: ui-monospace, "Cascadia Code", "Consolas", monospace;--shell-font-size: 13px;--shell-line: 1.45;--shell-radius-sm: 3px;--shell-radius: 4px;--shell-radius-md: 6px;--shell-radius-lg: 8px;--shell-pad-xs: 2px;--shell-pad-sm: 4px;--shell-pad-md: 8px;--shell-pad-lg: 12px;--shell-tabbar-height: 32px;--shell-statusbar-height: 22px;--shell-z-layer-0: 0;--shell-z-layer-1: 100;--shell-z-layer-2: 200;--shell-z-layer-3: 300;--shell-z-layer-4: 400;--shell-z-layer-5: 500;--shell-z-layer-6: 600}*,*:before,*:after{box-sizing:border-box}html,body{margin:0;padding:0;height:100%;overflow:hidden;background:var(--shell-grad-bg, var(--shell-bg));color:var(--shell-fg);font-family:var(--shell-font-ui);font-size:var(--shell-font-size);line-height:var(--shell-line);-webkit-font-smoothing:antialiased}#app{height:100%}
|
package/app/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>SH3</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-C3rCTpjL.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-GfhVhkjD.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="app"></div>
|
package/dist/cli.js
CHANGED
|
@@ -21,10 +21,12 @@ const { values } = parseArgs({
|
|
|
21
21
|
'no-auth': { type: 'boolean', default: false },
|
|
22
22
|
},
|
|
23
23
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
(async () => {
|
|
25
|
+
const server = await createServer({
|
|
26
|
+
port: parseInt(values.port, 10),
|
|
27
|
+
dataDir: values.data,
|
|
28
|
+
distDir: values.dist,
|
|
29
|
+
noAuth: values['no-auth'],
|
|
30
|
+
});
|
|
31
|
+
await server.start();
|
|
32
|
+
})();
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { createDocsRouter } from './routes/docs.js';
|
|
|
26
26
|
import { createEnvStateRouter } from './routes/env-state.js';
|
|
27
27
|
import { loadPackages, scanPackages, servePackageBundles, createPackageManagementRoutes } from './packages.js';
|
|
28
28
|
import { ShardRouter, adminOnly } from './shard-router.js';
|
|
29
|
+
import { registerTenantFsRoutes } from './tenant-fs/index.js';
|
|
29
30
|
import shellShardServer from './shell-shard/index.js';
|
|
30
31
|
export async function createServer(options = {}) {
|
|
31
32
|
const port = options.port ?? 3000;
|
|
@@ -166,9 +167,10 @@ export async function createServer(options = {}) {
|
|
|
166
167
|
// Package management (install/uninstall) — admin-gated
|
|
167
168
|
app.use('/api/packages/install', adminAuth(sessions, keys, settings));
|
|
168
169
|
app.use('/api/packages/uninstall', adminAuth(sessions, keys, settings));
|
|
169
|
-
|
|
170
|
+
const frameworkShardIds = ['__sh3core__', 'shell', 'sh3-store', 'sh3-admin'];
|
|
171
|
+
app.route('/api/packages', createPackageManagementRoutes(shardRouter, dataDir, keys, settings, wsRegister, frameworkShardIds));
|
|
170
172
|
// Serve client bundles from discovered packages
|
|
171
|
-
servePackageBundles(app, dataDir);
|
|
173
|
+
servePackageBundles(app, dataDir, settings);
|
|
172
174
|
// Framework built-in: shell-shard server routes.
|
|
173
175
|
//
|
|
174
176
|
// Mounted directly on the top-level Hono app (not via shardRouter) so
|
|
@@ -193,8 +195,16 @@ export async function createServer(options = {}) {
|
|
|
193
195
|
});
|
|
194
196
|
app.route('/api/shell', shellSubApp);
|
|
195
197
|
}
|
|
198
|
+
// Framework-level tenant filesystem API — read-only, jailed to each user's documents.
|
|
199
|
+
// Mounted before the dynamic shard router so /api/fs/* is claimed first.
|
|
200
|
+
registerTenantFsRoutes(app, {
|
|
201
|
+
dataDir,
|
|
202
|
+
rootBase: settings.get().tenants?.rootBase ?? '',
|
|
203
|
+
settings,
|
|
204
|
+
maxReadBytes: 10 * 1024 * 1024,
|
|
205
|
+
});
|
|
196
206
|
// Dynamic shard routes (packages). The catch-all comes after static
|
|
197
|
-
// framework mounts above so /api/shell/*
|
|
207
|
+
// framework mounts above so /api/shell/* and /api/fs/* are claimed first.
|
|
198
208
|
app.all('/api/:shardId/*', shardRouter.handler());
|
|
199
209
|
return {
|
|
200
210
|
app,
|
package/dist/packages.d.ts
CHANGED
|
@@ -28,11 +28,29 @@ export declare function loadPackages(shardRouter: ShardRouter, dataDir: string,
|
|
|
28
28
|
export declare function scanPackages(dataDir: string): DiscoveredPackage[];
|
|
29
29
|
/**
|
|
30
30
|
* Register `GET /packages/:id/client.js` to serve client bundles from disk.
|
|
31
|
+
* The `Cache-Control` header is read from settings on every request:
|
|
32
|
+
* - cacheMaxAge === 0 → `no-store`
|
|
33
|
+
* - otherwise → `public, max-age=<n>` (never `immutable`)
|
|
31
34
|
*/
|
|
32
|
-
export declare function servePackageBundles(app: Hono, dataDir: string): void;
|
|
35
|
+
export declare function servePackageBundles(app: Hono, dataDir: string, settings: SettingsStore): void;
|
|
36
|
+
export interface MissingShardsResult {
|
|
37
|
+
missing: Array<{
|
|
38
|
+
id: string;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check an uploaded manifest's `requiredShards` against the set of shard ids
|
|
43
|
+
* already known to the server (framework shards + installed shard/combo
|
|
44
|
+
* packages + shards present in the current upload for combo bundles).
|
|
45
|
+
*
|
|
46
|
+
* Returns `{ missing: [] }` for shard-only packages or when all requirements
|
|
47
|
+
* are satisfied. The shape of `missing` entries is deliberately extensible
|
|
48
|
+
* (future version-mismatch entries can add a `versionMismatch: true` flag).
|
|
49
|
+
*/
|
|
50
|
+
export declare function validateRequiredShards(manifest: Record<string, unknown>, knownShards: Set<string>): MissingShardsResult;
|
|
33
51
|
/**
|
|
34
52
|
* Returns a Hono router with POST /install and POST /uninstall.
|
|
35
53
|
* Protected by the blanket `/api/*` auth middleware already applied upstream.
|
|
36
54
|
*/
|
|
37
|
-
export declare function createPackageManagementRoutes(shardRouter: ShardRouter, dataDir: string, keys: KeyStore, settings: SettingsStore, wsRegister: WsRegister): Hono;
|
|
55
|
+
export declare function createPackageManagementRoutes(shardRouter: ShardRouter, dataDir: string, keys: KeyStore, settings: SettingsStore, wsRegister: WsRegister, frameworkShardIds?: string[]): Hono;
|
|
38
56
|
export {};
|
package/dist/packages.js
CHANGED
|
@@ -138,8 +138,11 @@ export function scanPackages(dataDir) {
|
|
|
138
138
|
// ---------------------------------------------------------------------------
|
|
139
139
|
/**
|
|
140
140
|
* Register `GET /packages/:id/client.js` to serve client bundles from disk.
|
|
141
|
+
* The `Cache-Control` header is read from settings on every request:
|
|
142
|
+
* - cacheMaxAge === 0 → `no-store`
|
|
143
|
+
* - otherwise → `public, max-age=<n>` (never `immutable`)
|
|
141
144
|
*/
|
|
142
|
-
export function servePackageBundles(app, dataDir) {
|
|
145
|
+
export function servePackageBundles(app, dataDir, settings) {
|
|
143
146
|
app.get('/packages/:id/client.js', (c) => {
|
|
144
147
|
const id = c.req.param('id');
|
|
145
148
|
if (!isValidId(id)) {
|
|
@@ -149,13 +152,40 @@ export function servePackageBundles(app, dataDir) {
|
|
|
149
152
|
if (!existsSync(filePath)) {
|
|
150
153
|
return c.json({ error: 'Client bundle not found' }, 404);
|
|
151
154
|
}
|
|
155
|
+
const maxAge = settings.get().packages.cacheMaxAge;
|
|
156
|
+
const cacheControl = maxAge === 0 ? 'no-store' : `public, max-age=${maxAge}`;
|
|
152
157
|
const content = readFileSync(filePath, 'utf-8');
|
|
153
158
|
return c.body(content, 200, {
|
|
154
159
|
'Content-Type': 'application/javascript',
|
|
155
|
-
'Cache-Control':
|
|
160
|
+
'Cache-Control': cacheControl,
|
|
156
161
|
});
|
|
157
162
|
});
|
|
158
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Check an uploaded manifest's `requiredShards` against the set of shard ids
|
|
166
|
+
* already known to the server (framework shards + installed shard/combo
|
|
167
|
+
* packages + shards present in the current upload for combo bundles).
|
|
168
|
+
*
|
|
169
|
+
* Returns `{ missing: [] }` for shard-only packages or when all requirements
|
|
170
|
+
* are satisfied. The shape of `missing` entries is deliberately extensible
|
|
171
|
+
* (future version-mismatch entries can add a `versionMismatch: true` flag).
|
|
172
|
+
*/
|
|
173
|
+
export function validateRequiredShards(manifest, knownShards) {
|
|
174
|
+
const type = manifest.type;
|
|
175
|
+
if (type !== 'app' && type !== 'combo')
|
|
176
|
+
return { missing: [] };
|
|
177
|
+
const required = manifest.requiredShards;
|
|
178
|
+
if (!Array.isArray(required))
|
|
179
|
+
return { missing: [] };
|
|
180
|
+
const missing = [];
|
|
181
|
+
for (const id of required) {
|
|
182
|
+
if (typeof id !== 'string')
|
|
183
|
+
continue;
|
|
184
|
+
if (!knownShards.has(id))
|
|
185
|
+
missing.push({ id });
|
|
186
|
+
}
|
|
187
|
+
return { missing };
|
|
188
|
+
}
|
|
159
189
|
// ---------------------------------------------------------------------------
|
|
160
190
|
// Package management routes (install / uninstall)
|
|
161
191
|
// ---------------------------------------------------------------------------
|
|
@@ -163,7 +193,7 @@ export function servePackageBundles(app, dataDir) {
|
|
|
163
193
|
* Returns a Hono router with POST /install and POST /uninstall.
|
|
164
194
|
* Protected by the blanket `/api/*` auth middleware already applied upstream.
|
|
165
195
|
*/
|
|
166
|
-
export function createPackageManagementRoutes(shardRouter, dataDir, keys, settings, wsRegister) {
|
|
196
|
+
export function createPackageManagementRoutes(shardRouter, dataDir, keys, settings, wsRegister, frameworkShardIds = []) {
|
|
167
197
|
const router = new Hono();
|
|
168
198
|
router.post('/install', async (c) => {
|
|
169
199
|
const form = await c.req.formData();
|
|
@@ -189,6 +219,37 @@ export function createPackageManagementRoutes(shardRouter, dataDir, keys, settin
|
|
|
189
219
|
if (typeof manifest.version !== 'string' || !manifest.version) {
|
|
190
220
|
return c.json({ error: 'Missing "version" in manifest' }, 400);
|
|
191
221
|
}
|
|
222
|
+
// requiredShards guard — reject apps whose shard deps are not on the server.
|
|
223
|
+
const installedShardIds = [];
|
|
224
|
+
const pkgsDir = join(dataDir, 'packages');
|
|
225
|
+
if (existsSync(pkgsDir)) {
|
|
226
|
+
for (const entry of readdirSync(pkgsDir, { withFileTypes: true })) {
|
|
227
|
+
if (!entry.isDirectory())
|
|
228
|
+
continue;
|
|
229
|
+
const otherManifestPath = join(pkgsDir, entry.name, 'manifest.json');
|
|
230
|
+
if (!existsSync(otherManifestPath))
|
|
231
|
+
continue;
|
|
232
|
+
try {
|
|
233
|
+
const other = JSON.parse(readFileSync(otherManifestPath, 'utf-8'));
|
|
234
|
+
if ((other.type === 'shard' || other.type === 'combo') && typeof other.id === 'string') {
|
|
235
|
+
installedShardIds.push(other.id);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch { /* skip malformed manifest */ }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const knownShards = new Set([
|
|
242
|
+
...frameworkShardIds,
|
|
243
|
+
...installedShardIds,
|
|
244
|
+
]);
|
|
245
|
+
// Combo uploads contribute their own shard to the known set (the shard id equals the combo id).
|
|
246
|
+
if (manifest.type === 'combo' && typeof manifest.id === 'string') {
|
|
247
|
+
knownShards.add(manifest.id);
|
|
248
|
+
}
|
|
249
|
+
const { missing } = validateRequiredShards(manifest, knownShards);
|
|
250
|
+
if (missing.length > 0) {
|
|
251
|
+
return c.json({ code: 'missing-shards', missing }, 409);
|
|
252
|
+
}
|
|
192
253
|
const pkgDir = join(dataDir, 'packages', id);
|
|
193
254
|
mkdirSync(pkgDir, { recursive: true });
|
|
194
255
|
// Write manifest
|
package/dist/routes/docs.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Maps the DocumentBackend interface to HTTP endpoints backed by the
|
|
5
5
|
* local filesystem. Files are stored at {dataDir}/docs/{tenant}/{shard}/{path}.
|
|
6
6
|
*
|
|
7
|
+
* GET /api/docs/:tenant/_shards → listAllShards
|
|
8
|
+
* GET /api/docs/:tenant/_all → listAllDocuments
|
|
7
9
|
* GET /api/docs/:tenant/:shard → list
|
|
8
10
|
* GET /api/docs/:tenant/:shard/*path → read
|
|
9
11
|
* HEAD /api/docs/:tenant/:shard/*path → exists
|
package/dist/routes/docs.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Maps the DocumentBackend interface to HTTP endpoints backed by the
|
|
5
5
|
* local filesystem. Files are stored at {dataDir}/docs/{tenant}/{shard}/{path}.
|
|
6
6
|
*
|
|
7
|
+
* GET /api/docs/:tenant/_shards → listAllShards
|
|
8
|
+
* GET /api/docs/:tenant/_all → listAllDocuments
|
|
7
9
|
* GET /api/docs/:tenant/:shard → list
|
|
8
10
|
* GET /api/docs/:tenant/:shard/*path → read
|
|
9
11
|
* HEAD /api/docs/:tenant/:shard/*path → exists
|
|
@@ -45,6 +47,34 @@ export function createDocsRouter(dataDir) {
|
|
|
45
47
|
}
|
|
46
48
|
return results;
|
|
47
49
|
}
|
|
50
|
+
// List all shard ids that have content for a tenant.
|
|
51
|
+
router.get('/:tenant/_shards', (c) => {
|
|
52
|
+
const { tenant } = c.req.param();
|
|
53
|
+
const tenantDir = join(docsDir, tenant);
|
|
54
|
+
if (!existsSync(tenantDir))
|
|
55
|
+
return c.json([]);
|
|
56
|
+
const entries = readdirSync(tenantDir, { withFileTypes: true });
|
|
57
|
+
const shards = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
58
|
+
return c.json(shards);
|
|
59
|
+
});
|
|
60
|
+
// Tenant-wide document list with shardId attached on each entry.
|
|
61
|
+
router.get('/:tenant/_all', (c) => {
|
|
62
|
+
const { tenant } = c.req.param();
|
|
63
|
+
const tenantDir = join(docsDir, tenant);
|
|
64
|
+
if (!existsSync(tenantDir))
|
|
65
|
+
return c.json([]);
|
|
66
|
+
const entries = readdirSync(tenantDir, { withFileTypes: true });
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (!entry.isDirectory())
|
|
70
|
+
continue;
|
|
71
|
+
const shardDir = join(tenantDir, entry.name);
|
|
72
|
+
for (const f of collectFiles(shardDir, shardDir)) {
|
|
73
|
+
out.push({ ...f, shardId: entry.name });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return c.json(out);
|
|
77
|
+
});
|
|
48
78
|
// List documents for a tenant/shard
|
|
49
79
|
router.get('/:tenant/:shard', (c) => {
|
|
50
80
|
const { tenant, shard } = c.req.param();
|
package/dist/settings.d.ts
CHANGED
|
@@ -9,6 +9,17 @@ export interface GlobalSettings {
|
|
|
9
9
|
sessionTTL: number;
|
|
10
10
|
selfRegistration: boolean;
|
|
11
11
|
};
|
|
12
|
+
tenants: {
|
|
13
|
+
/** Absolute or dataDir-relative base; each user gets `<base>/<userId>/documents/`.
|
|
14
|
+
* Empty string means "<dataDir>/users" at resolve time. */
|
|
15
|
+
rootBase: string;
|
|
16
|
+
};
|
|
17
|
+
packages: {
|
|
18
|
+
/** Max-age in seconds for `GET /packages/:id/client.js` responses.
|
|
19
|
+
* Clamped to [0, 31536000]. 0 emits `Cache-Control: no-store`.
|
|
20
|
+
* Any other value emits `public, max-age=N` (never `immutable`). */
|
|
21
|
+
cacheMaxAge: number;
|
|
22
|
+
};
|
|
12
23
|
}
|
|
13
24
|
export declare class SettingsStore {
|
|
14
25
|
#private;
|
|
@@ -18,5 +29,7 @@ export declare class SettingsStore {
|
|
|
18
29
|
/** Patch settings. Only provided fields are updated. */
|
|
19
30
|
update(patch: {
|
|
20
31
|
auth?: Partial<GlobalSettings['auth']>;
|
|
32
|
+
tenants?: Partial<GlobalSettings['tenants']>;
|
|
33
|
+
packages?: Partial<GlobalSettings['packages']>;
|
|
21
34
|
}): GlobalSettings;
|
|
22
35
|
}
|
package/dist/settings.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
6
6
|
import { dirname } from 'node:path';
|
|
7
|
+
const MAX_PACKAGE_CACHE_AGE = 31536000; // 1 year
|
|
7
8
|
const DEFAULTS = {
|
|
8
9
|
auth: {
|
|
9
10
|
required: true,
|
|
@@ -11,7 +12,24 @@ const DEFAULTS = {
|
|
|
11
12
|
sessionTTL: 24,
|
|
12
13
|
selfRegistration: false,
|
|
13
14
|
},
|
|
15
|
+
tenants: {
|
|
16
|
+
rootBase: '',
|
|
17
|
+
},
|
|
18
|
+
packages: {
|
|
19
|
+
cacheMaxAge: MAX_PACKAGE_CACHE_AGE,
|
|
20
|
+
},
|
|
14
21
|
};
|
|
22
|
+
function clampCacheMaxAge(value) {
|
|
23
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
24
|
+
return DEFAULTS.packages.cacheMaxAge;
|
|
25
|
+
}
|
|
26
|
+
const floored = Math.floor(value);
|
|
27
|
+
if (floored < 0)
|
|
28
|
+
return 0;
|
|
29
|
+
if (floored > MAX_PACKAGE_CACHE_AGE)
|
|
30
|
+
return MAX_PACKAGE_CACHE_AGE;
|
|
31
|
+
return floored;
|
|
32
|
+
}
|
|
15
33
|
export class SettingsStore {
|
|
16
34
|
#path;
|
|
17
35
|
#settings;
|
|
@@ -31,6 +49,12 @@ export class SettingsStore {
|
|
|
31
49
|
sessionTTL: raw.auth?.sessionTTL ?? DEFAULTS.auth.sessionTTL,
|
|
32
50
|
selfRegistration: raw.auth?.selfRegistration ?? DEFAULTS.auth.selfRegistration,
|
|
33
51
|
},
|
|
52
|
+
tenants: {
|
|
53
|
+
rootBase: raw.tenants?.rootBase ?? DEFAULTS.tenants.rootBase,
|
|
54
|
+
},
|
|
55
|
+
packages: {
|
|
56
|
+
cacheMaxAge: clampCacheMaxAge(raw.packages?.cacheMaxAge),
|
|
57
|
+
},
|
|
34
58
|
};
|
|
35
59
|
}
|
|
36
60
|
catch {
|
|
@@ -57,6 +81,15 @@ export class SettingsStore {
|
|
|
57
81
|
if (patch.auth.selfRegistration !== undefined)
|
|
58
82
|
this.#settings.auth.selfRegistration = patch.auth.selfRegistration;
|
|
59
83
|
}
|
|
84
|
+
if (patch.tenants) {
|
|
85
|
+
if (patch.tenants.rootBase !== undefined)
|
|
86
|
+
this.#settings.tenants.rootBase = patch.tenants.rootBase;
|
|
87
|
+
}
|
|
88
|
+
if (patch.packages) {
|
|
89
|
+
if (patch.packages.cacheMaxAge !== undefined) {
|
|
90
|
+
this.#settings.packages.cacheMaxAge = clampCacheMaxAge(patch.packages.cacheMaxAge);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
60
93
|
this.#save();
|
|
61
94
|
return this.get();
|
|
62
95
|
}
|
|
@@ -4,6 +4,8 @@ import type { WsLike } from './session-manager.js';
|
|
|
4
4
|
export interface ShellServerContext {
|
|
5
5
|
shardId: string;
|
|
6
6
|
dataDir: string;
|
|
7
|
+
/** Base for per-user document roots; empty string → <dataDir>/users. */
|
|
8
|
+
tenantRootBase?: string;
|
|
7
9
|
adminOnly: any;
|
|
8
10
|
wsRegister: (onConnect: (ws: WsLike, c: Context) => void) => any;
|
|
9
11
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { LocalRunner } from './runner.js';
|
|
11
11
|
import { SessionManager } from './session-manager.js';
|
|
12
12
|
import { handleClientMessage } from './ws.js';
|
|
13
|
+
import { shardDocumentsPath } from '../tenant-fs/paths.js';
|
|
13
14
|
function sessionUser(c) {
|
|
14
15
|
const session = c.get('session') ?? c.env?.session;
|
|
15
16
|
return session?.userId ?? 'admin';
|
|
@@ -20,7 +21,8 @@ export default {
|
|
|
20
21
|
// Default config — overridable via shard env state in a future pass.
|
|
21
22
|
const cfg = { ringSize: 500, historyMaxLines: 10_000, defaultCwd: '' };
|
|
22
23
|
const runner = new LocalRunner();
|
|
23
|
-
const
|
|
24
|
+
const userCwd = (userId) => shardDocumentsPath(ctx.dataDir, userId, 'shell', ctx.tenantRootBase ?? '');
|
|
25
|
+
const manager = new SessionManager(ctx.dataDir, runner, cfg, userCwd);
|
|
24
26
|
// NOTE: the JSON /history endpoint is convenience — the authoritative
|
|
25
27
|
// delivery of history is via the `history` server message sent on WS
|
|
26
28
|
// attach. Clients can skip the REST endpoint entirely. Kept for
|
|
@@ -58,6 +58,7 @@ export declare class SessionManager {
|
|
|
58
58
|
private readonly dataDir;
|
|
59
59
|
private readonly runner;
|
|
60
60
|
private readonly cfg;
|
|
61
|
-
|
|
61
|
+
private readonly userCwd;
|
|
62
|
+
constructor(dataDir: string, runner: Runner, cfg: SessionConfig, userCwd: (userId: string) => string);
|
|
62
63
|
getOrCreate(userId: string): ShellSession;
|
|
63
64
|
}
|
|
@@ -160,16 +160,29 @@ export class SessionManager {
|
|
|
160
160
|
dataDir;
|
|
161
161
|
runner;
|
|
162
162
|
cfg;
|
|
163
|
-
|
|
163
|
+
userCwd;
|
|
164
|
+
constructor(dataDir, runner, cfg, userCwd) {
|
|
164
165
|
this.dataDir = dataDir;
|
|
165
166
|
this.runner = runner;
|
|
166
167
|
this.cfg = cfg;
|
|
168
|
+
this.userCwd = userCwd;
|
|
167
169
|
}
|
|
168
170
|
getOrCreate(userId) {
|
|
169
171
|
let session = this.sessions.get(userId);
|
|
170
172
|
if (!session) {
|
|
171
173
|
const history = new HistoryStore(this.dataDir, userId, this.cfg.historyMaxLines);
|
|
172
|
-
|
|
174
|
+
// Resolve user cwd; on disk failure fall back to cfg.defaultCwd so
|
|
175
|
+
// a sick data dir doesn't block login. The warning surfaces in the
|
|
176
|
+
// server log so ops can notice.
|
|
177
|
+
let cwd = this.cfg.defaultCwd;
|
|
178
|
+
try {
|
|
179
|
+
cwd = this.userCwd(userId);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
console.warn(`[shell-shard] userCwd failed for ${userId}: ${err.message} — falling back to ${cwd || 'process.cwd()'}`);
|
|
183
|
+
}
|
|
184
|
+
const cfg = { ...this.cfg, defaultCwd: cwd };
|
|
185
|
+
session = new ShellSession(userId, this.runner, history, cfg);
|
|
173
186
|
this.sessions.set(userId, session);
|
|
174
187
|
}
|
|
175
188
|
return session;
|
package/dist/shell-shard/ws.js
CHANGED
|
@@ -15,21 +15,16 @@ export function handleClientMessage(session, ws, raw) {
|
|
|
15
15
|
msg = JSON.parse(raw);
|
|
16
16
|
}
|
|
17
17
|
catch {
|
|
18
|
-
// Malformed frame — ignore, do not crash the session.
|
|
19
18
|
return;
|
|
20
19
|
}
|
|
21
20
|
switch (msg.t) {
|
|
22
21
|
case 'hello':
|
|
23
|
-
// attach() already sent welcome+replay+history at connection time.
|
|
24
|
-
// The only meaningful thing here is a late hello with replayFrom,
|
|
25
|
-
// which v1 ignores — the client can resync by reconnecting.
|
|
26
22
|
return;
|
|
27
23
|
case 'submit': {
|
|
28
24
|
const trimmed = msg.line.trim();
|
|
29
25
|
if (trimmed.startsWith('cd ') || trimmed === 'cd') {
|
|
30
|
-
// Server-managed cd — don't spawn, update session cwd directly.
|
|
31
26
|
const target = trimmed === 'cd' ? '' : trimmed.slice(3).trim();
|
|
32
|
-
|
|
27
|
+
applyCwdChange(session, target, 'cd');
|
|
33
28
|
return;
|
|
34
29
|
}
|
|
35
30
|
void session.submit(msg.line, ws);
|
|
@@ -42,12 +37,12 @@ export function handleClientMessage(session, ws, raw) {
|
|
|
42
37
|
session.historyLog(msg.line);
|
|
43
38
|
return;
|
|
44
39
|
case 'cwd-query':
|
|
45
|
-
// Re-emit a cwd update message (reuses setCwd() broadcast path).
|
|
46
40
|
session.setCwd(session.cwd);
|
|
47
41
|
return;
|
|
42
|
+
case 'setCwd':
|
|
43
|
+
applyCwdChange(session, msg.path, 'setCwd');
|
|
44
|
+
return;
|
|
48
45
|
default: {
|
|
49
|
-
// Exhaustiveness check. If a new ClientMessage variant is added to
|
|
50
|
-
// the protocol without a handler here, TypeScript flags this line.
|
|
51
46
|
const _exhaustive = msg;
|
|
52
47
|
void _exhaustive;
|
|
53
48
|
return;
|
|
@@ -55,11 +50,16 @@ export function handleClientMessage(session, ws, raw) {
|
|
|
55
50
|
}
|
|
56
51
|
}
|
|
57
52
|
/**
|
|
58
|
-
*
|
|
59
|
-
* exists and is a directory, then update session.cwd via setCwd()
|
|
60
|
-
*
|
|
53
|
+
* Resolve a cwd-change request against the session's current cwd, validate
|
|
54
|
+
* it exists and is a directory, then update session.cwd via setCwd(). Used
|
|
55
|
+
* for both interactive `cd` (from submit) and programmatic `setCwd` (from
|
|
56
|
+
* the docs tree / file explorer).
|
|
57
|
+
*
|
|
58
|
+
* Stderr wording keeps the familiar `cd: no such directory` for shell users
|
|
59
|
+
* and `setCwd: no such directory` for programmatic callers, so the source
|
|
60
|
+
* of a bad path is obvious in the terminal log.
|
|
61
61
|
*/
|
|
62
|
-
function
|
|
62
|
+
function applyCwdChange(session, target, source) {
|
|
63
63
|
const dest = target === '' || target === '~'
|
|
64
64
|
? homedir()
|
|
65
65
|
: isAbsolute(target)
|
|
@@ -69,7 +69,7 @@ function handleCd(session, target) {
|
|
|
69
69
|
session.broadcast({
|
|
70
70
|
seq: 0,
|
|
71
71
|
kind: 'stderr',
|
|
72
|
-
data:
|
|
72
|
+
data: `${source}: no such directory: ${target}\n`,
|
|
73
73
|
ts: Date.now(),
|
|
74
74
|
});
|
|
75
75
|
return;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant FS HTTP API — mounts /api/fs/list, /api/fs/stat, /api/fs/read.
|
|
3
|
+
*
|
|
4
|
+
* Gated by sessionRequired; scope is the caller's own documentsRoot.
|
|
5
|
+
* Read-only. Writes are out of scope for this iteration.
|
|
6
|
+
*/
|
|
7
|
+
import type { Hono } from 'hono';
|
|
8
|
+
import type { SettingsStore } from '../settings.js';
|
|
9
|
+
export interface TenantFsRouteContext {
|
|
10
|
+
dataDir: string;
|
|
11
|
+
rootBase: string;
|
|
12
|
+
settings: SettingsStore;
|
|
13
|
+
maxReadBytes: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function registerTenantFsRoutes(app: Hono, ctx: TenantFsRouteContext): void;
|