@flowdular/sdk 0.1.0 → 0.2.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/.ai/skills/test-hardening/SKILL.md +1 -1
- package/README.md +69 -12
- package/assets/flowdular-banner.png +0 -0
- package/modules/agents/package.json +1 -1
- package/modules/auth/package.json +1 -1
- package/modules/auth/src/client/AuthenticationCore.tsrx +3 -1
- package/modules/auth/src/server/endpoints.ts +3 -1
- package/modules/auth/src/server/runtime.ts +4 -0
- package/modules/automations/package.json +1 -1
- package/modules/automations-workflows-integration/package.json +1 -1
- package/modules/profile/package.json +1 -1
- package/modules/sandbox/package.json +1 -1
- package/modules/system/package.json +1 -1
- package/modules/users/package.json +1 -1
- package/modules/workflows/package.json +1 -1
- package/package.json +5 -13
- package/packages/ai-provider/package.json +1 -1
- package/packages/cli-protocol/package.json +1 -1
- package/packages/client/package.json +1 -1
- package/packages/client/src/routing.ts +39 -2
- package/packages/client/src/shell/BrandMark.tsrx +5 -1
- package/packages/client/src/shell/navigation.ts +3 -1
- package/packages/client/src/state.ts +6 -2
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/schemas/project.schema.json +16 -42
- package/packages/database/package.json +1 -1
- package/packages/database-pglite/package.json +2 -2
- package/packages/database-pglite/src/driver.ts +21 -3
- package/packages/database-testing/package.json +1 -1
- package/packages/dev-console/package.json +1 -1
- package/packages/harness/package.json +1 -1
- package/packages/kernel/package.json +1 -1
- package/packages/server/package.json +1 -1
- package/packages/server/src/application-routes.ts +80 -8
- package/packages/server/src/index.ts +4 -1
- package/packages/server/src/web-html.ts +9 -5
- package/packages/server/src/web.ts +59 -7
- package/packages/ui/package.json +1 -1
- package/packages/coding-agent/package.json +0 -44
- package/packages/coding-agent/src/drivers/byok.ts +0 -322
- package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
- package/packages/coding-agent/src/drivers/codex.ts +0 -281
- package/packages/coding-agent/src/index.ts +0 -56
- package/packages/coding-agent/src/registry.ts +0 -119
- package/packages/coding-agent/src/roles/contract.ts +0 -60
- package/packages/coding-agent/src/roles/defaults.ts +0 -134
- package/packages/coding-agent/src/roles/handoff.ts +0 -41
- package/packages/coding-agent/src/roles/registry.ts +0 -156
- package/packages/coding-agent/src/roles/skills.ts +0 -72
- package/packages/coding-agent/src/roles/sync.ts +0 -88
- package/packages/coding-agent/src/types.ts +0 -102
- package/packages/coding-agent/src/workspace.ts +0 -275
- package/packages/sandbox/README.md +0 -691
- package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
- package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
- package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
- package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
- package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
- package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
- package/packages/sandbox/dist/client/favicon.svg +0 -1
- package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
- package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
- package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
- package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
- package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
- package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
- package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
- package/packages/sandbox/dist/server/entry.js +0 -91671
- package/packages/sandbox/dist/server/favicon.svg +0 -1
- package/packages/sandbox/dist/server/index.html +0 -161
- package/packages/sandbox/index.html +0 -159
- package/packages/sandbox/octane.config.ts +0 -36
- package/packages/sandbox/package.json +0 -83
- package/packages/sandbox/public/favicon.svg +0 -1
- package/packages/sandbox/public/og.png +0 -0
- package/packages/sandbox/src/App.tsrx +0 -924
- package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
- package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
- package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
- package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
- package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
- package/packages/sandbox/src/client/GateResults.tsrx +0 -60
- package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
- package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
- package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
- package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
- package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
- package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
- package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
- package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
- package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
- package/packages/sandbox/src/client/api.ts +0 -797
- package/packages/sandbox/src/client/attachments.ts +0 -73
- package/packages/sandbox/src/client/dashboard.ts +0 -46
- package/packages/sandbox/src/client/github-settings.ts +0 -48
- package/packages/sandbox/src/client/highlight.ts +0 -105
- package/packages/sandbox/src/client/i18n.ts +0 -32
- package/packages/sandbox/src/client/locales/en.json +0 -466
- package/packages/sandbox/src/client/locales/pl.json +0 -466
- package/packages/sandbox/src/client/session-labels.ts +0 -77
- package/packages/sandbox/src/client/state.ts +0 -108
- package/packages/sandbox/src/index.ts +0 -10
- package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
- package/packages/sandbox/src/preview/load-module.ts +0 -16
- package/packages/sandbox/src/server/attachments.ts +0 -308
- package/packages/sandbox/src/server/auto-review.ts +0 -181
- package/packages/sandbox/src/server/checkpoints.ts +0 -90
- package/packages/sandbox/src/server/config.ts +0 -460
- package/packages/sandbox/src/server/dashboard.ts +0 -245
- package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
- package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
- package/packages/sandbox/src/server/delivery/index.ts +0 -83
- package/packages/sandbox/src/server/delivery/local.ts +0 -151
- package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
- package/packages/sandbox/src/server/delivery/plan.ts +0 -218
- package/packages/sandbox/src/server/delivery/policies.ts +0 -239
- package/packages/sandbox/src/server/delivery/record.ts +0 -41
- package/packages/sandbox/src/server/delivery/steps.ts +0 -464
- package/packages/sandbox/src/server/delivery/types.ts +0 -158
- package/packages/sandbox/src/server/dependencies.ts +0 -88
- package/packages/sandbox/src/server/diff.ts +0 -230
- package/packages/sandbox/src/server/gates.ts +0 -388
- package/packages/sandbox/src/server/index.ts +0 -139
- package/packages/sandbox/src/server/path-guard.ts +0 -282
- package/packages/sandbox/src/server/planning.ts +0 -749
- package/packages/sandbox/src/server/platform-client.ts +0 -189
- package/packages/sandbox/src/server/preview-database-host.ts +0 -340
- package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
- package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
- package/packages/sandbox/src/server/preview-database.ts +0 -109
- package/packages/sandbox/src/server/preview-ipc.ts +0 -15
- package/packages/sandbox/src/server/preview-modules.ts +0 -83
- package/packages/sandbox/src/server/preview-revision.ts +0 -44
- package/packages/sandbox/src/server/preview-runtime.ts +0 -467
- package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
- package/packages/sandbox/src/server/preview-worker.ts +0 -168
- package/packages/sandbox/src/server/reference.ts +0 -251
- package/packages/sandbox/src/server/routes.ts +0 -2258
- package/packages/sandbox/src/server/runtime.ts +0 -188
- package/packages/sandbox/src/server/session-owner.ts +0 -23
- package/packages/sandbox/src/server/sessions.ts +0 -849
- package/packages/sandbox/src/server/spec.ts +0 -471
- package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
- package/packages/sandbox/src/server/turns.ts +0 -993
- package/packages/sandbox/src/server/workspace-install.ts +0 -266
- package/packages/sandbox/src/server/workspace-root.ts +0 -73
- package/packages/sandbox/src/styles.css +0 -1573
- package/packages/sandbox/vite.config.ts +0 -133
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo } from 'octane';
|
|
2
|
-
import {
|
|
3
|
-
ApplicationShell,
|
|
4
|
-
type ModuleClientContribution,
|
|
5
|
-
} from '@flowdular/sdk/client';
|
|
6
|
-
import { Alert, EmptyState, Tag } from '@flowdular/sdk/ui';
|
|
7
|
-
import { cell, createStore, useValue } from 'segment-state';
|
|
8
|
-
import { Head } from '@octanejs/seo';
|
|
9
|
-
import type { RenderRouteProps } from '@octanejs/vite-plugin';
|
|
10
|
-
import type { SessionView } from '../client/api.ts';
|
|
11
|
-
import { signalReady } from '../client/state.ts';
|
|
12
|
-
import { importDraftModule } from './load-module.ts';
|
|
13
|
-
import {
|
|
14
|
-
registerSandboxTranslations,
|
|
15
|
-
restoreSandboxLocale,
|
|
16
|
-
t,
|
|
17
|
-
useTranslation,
|
|
18
|
-
} from '../client/i18n.ts';
|
|
19
|
-
import '@flowdular/sdk/ui/styles';
|
|
20
|
-
import '../styles.css';
|
|
21
|
-
|
|
22
|
-
registerSandboxTranslations();
|
|
23
|
-
|
|
24
|
-
interface PreviewIdentity {
|
|
25
|
-
readonly accountId: string;
|
|
26
|
-
readonly email: string;
|
|
27
|
-
readonly displayName: string;
|
|
28
|
-
readonly role: string;
|
|
29
|
-
readonly scopes: readonly string[];
|
|
30
|
-
readonly tenantId: string;
|
|
31
|
-
readonly tenants: readonly {
|
|
32
|
-
readonly tenantId: string;
|
|
33
|
-
readonly name: string;
|
|
34
|
-
readonly slug: string;
|
|
35
|
-
readonly role: string;
|
|
36
|
-
}[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface PreviewPrincipal {
|
|
40
|
-
readonly accountId: string;
|
|
41
|
-
readonly email: string;
|
|
42
|
-
readonly displayName: string;
|
|
43
|
-
readonly role: string;
|
|
44
|
-
readonly scopes: readonly string[];
|
|
45
|
-
readonly tenantId: string;
|
|
46
|
-
readonly tenants: readonly {
|
|
47
|
-
readonly tenantId: string;
|
|
48
|
-
readonly name: string;
|
|
49
|
-
readonly slug: string;
|
|
50
|
-
readonly role: string;
|
|
51
|
-
}[];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function previewState() {
|
|
55
|
-
const store = createStore({
|
|
56
|
-
contributions: cell<readonly ModuleClientContribution[]>([]),
|
|
57
|
-
principal: cell<PreviewPrincipal | null>(null),
|
|
58
|
-
status: cell<'loading' | 'ready' | 'empty' | 'failed'>('loading'),
|
|
59
|
-
message: '',
|
|
60
|
-
moduleId: '',
|
|
61
|
-
/* The view the shell opens on: the first navigation entry of the module
|
|
62
|
-
the URL asked for, or of the first module that contributed one. */
|
|
63
|
-
initialView: '',
|
|
64
|
-
});
|
|
65
|
-
return { store, state: store.state };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/* The preview runs against the session's own authentication runtime, so the
|
|
69
|
-
draft module sees a real principal, real scopes, and a real CSRF token
|
|
70
|
-
instead of a stub. The credential belongs to one ephemeral database. */
|
|
71
|
-
interface PreviewModuleInfo {
|
|
72
|
-
readonly id: string;
|
|
73
|
-
readonly directory: string;
|
|
74
|
-
readonly hasClient: boolean;
|
|
75
|
-
readonly support?: boolean;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function signInPreview(sessionId: string): Promise<{
|
|
79
|
-
readonly principal: PreviewPrincipal;
|
|
80
|
-
readonly csrfToken: string;
|
|
81
|
-
readonly scopes: readonly string[];
|
|
82
|
-
readonly modules: readonly PreviewModuleInfo[];
|
|
83
|
-
readonly error: string | null;
|
|
84
|
-
}> {
|
|
85
|
-
document.cookie = `coreloom_preview=${sessionId}; path=/; samesite=strict`;
|
|
86
|
-
const info = await (await fetch(
|
|
87
|
-
'/sandbox/api/sessions/' + encodeURIComponent(sessionId) + '/preview',
|
|
88
|
-
{
|
|
89
|
-
headers: { accept: 'application/json' },
|
|
90
|
-
},
|
|
91
|
-
)).json() as {
|
|
92
|
-
credentials?: { email: string; password: string };
|
|
93
|
-
scopes?: readonly string[];
|
|
94
|
-
modules?: readonly PreviewModuleInfo[];
|
|
95
|
-
error?: string | { message?: string };
|
|
96
|
-
};
|
|
97
|
-
const credentials = info.credentials;
|
|
98
|
-
if (!credentials) {
|
|
99
|
-
throw new Error(typeof info.error === 'string'
|
|
100
|
-
? info.error
|
|
101
|
-
: info.error?.message ?? t('sandbox.preview.sessionUnavailable'));
|
|
102
|
-
}
|
|
103
|
-
const response = await fetch('/api/auth/sign-in', {
|
|
104
|
-
method: 'POST',
|
|
105
|
-
headers: { 'content-type': 'application/json' },
|
|
106
|
-
credentials: 'same-origin',
|
|
107
|
-
body: JSON.stringify(credentials),
|
|
108
|
-
});
|
|
109
|
-
const session = await response.json() as {
|
|
110
|
-
principal?: PreviewPrincipal;
|
|
111
|
-
csrfToken?: string;
|
|
112
|
-
error?: { message?: string };
|
|
113
|
-
};
|
|
114
|
-
if (!response.ok || !session.principal || !session.csrfToken) {
|
|
115
|
-
throw new Error(session.error?.message ??
|
|
116
|
-
t('sandbox.preview.identityFailed'));
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
principal: session.principal,
|
|
120
|
-
csrfToken: session.csrfToken,
|
|
121
|
-
scopes: info.scopes ?? [],
|
|
122
|
-
modules: info.modules ?? [],
|
|
123
|
-
error: typeof info.error === 'string' ? info.error : null,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/* Draft sources are served through a sandbox route that names the session and
|
|
128
|
-
the module directory, never through a host file system path. */
|
|
129
|
-
function clientEntry(
|
|
130
|
-
sessionId: string,
|
|
131
|
-
directory: string,
|
|
132
|
-
support = false,
|
|
133
|
-
): string {
|
|
134
|
-
return `/${
|
|
135
|
-
support ? 'preview-support' : 'preview-module'
|
|
136
|
-
}/${encodeURIComponent(sessionId)}/${encodeURIComponent(
|
|
137
|
-
directory,
|
|
138
|
-
)}/src/client/index.ts`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function sessionIdFromLocation(url: string): string {
|
|
142
|
-
const segments = new URL(url, 'http://sandbox.local').pathname.split(
|
|
143
|
-
'/',
|
|
144
|
-
).filter(Boolean);
|
|
145
|
-
return segments[0] === 'preview' ? segments[1] ?? '' : '';
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function identityFor(
|
|
149
|
-
principal: PreviewPrincipal | null,
|
|
150
|
-
contributions: readonly ModuleClientContribution[],
|
|
151
|
-
moduleId: string,
|
|
152
|
-
): PreviewIdentity {
|
|
153
|
-
/* The signed-in preview principal decides authority. Its scopes are widened
|
|
154
|
-
only with the surfaces the draft modules declare, so every screen they
|
|
155
|
-
contribute is reachable and nothing else appears. */
|
|
156
|
-
const scopes = new Set<string>([
|
|
157
|
-
'system.workspace.access',
|
|
158
|
-
...(principal?.scopes ?? []),
|
|
159
|
-
]);
|
|
160
|
-
for (const contribution of contributions) {
|
|
161
|
-
for (const item of contribution.navigation ?? []) scopes.add(item.scope);
|
|
162
|
-
for (const widget of contribution.widgets ?? []) scopes.add(widget.scope);
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
accountId: principal?.accountId ?? 'preview-account',
|
|
166
|
-
email: principal?.email ?? 'preview@sandbox.local',
|
|
167
|
-
displayName: principal?.displayName ?? t('sandbox.preview.identityName'),
|
|
168
|
-
role: principal?.role ?? 'owner',
|
|
169
|
-
scopes: [...scopes],
|
|
170
|
-
tenantId: principal?.tenantId ?? 'preview-tenant',
|
|
171
|
-
tenants: principal?.tenants && principal.tenants.length > 0
|
|
172
|
-
? principal.tenants
|
|
173
|
-
: [
|
|
174
|
-
{
|
|
175
|
-
tenantId: 'preview-tenant',
|
|
176
|
-
name: moduleId || t('sandbox.preview.workspace'),
|
|
177
|
-
slug: 'preview',
|
|
178
|
-
role: 'owner',
|
|
179
|
-
},
|
|
180
|
-
],
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/* Element selection: the parent turns it on, the preview reports what was
|
|
185
|
-
clicked, and the chat attaches it to the next message. */
|
|
186
|
-
function installSelection(enabled: boolean): () => void {
|
|
187
|
-
if (!enabled || typeof document === 'undefined') return () => undefined;
|
|
188
|
-
const describe = (element: Element): string => {
|
|
189
|
-
const parts: string[] = [];
|
|
190
|
-
let current: Element | null = element;
|
|
191
|
-
for (let depth = 0; current && depth < 4; depth += 1) {
|
|
192
|
-
const classes = (current.getAttribute('class') ?? '').split(/\s+/).filter(
|
|
193
|
-
Boolean,
|
|
194
|
-
).slice(0, 2).map((value) => '.' + value).join('');
|
|
195
|
-
parts.unshift(current.tagName.toLowerCase() + classes);
|
|
196
|
-
current = current.parentElement;
|
|
197
|
-
}
|
|
198
|
-
return parts.join(' > ');
|
|
199
|
-
};
|
|
200
|
-
const onClick = (event: MouseEvent) => {
|
|
201
|
-
const target = event.target as Element | null;
|
|
202
|
-
if (!target) return;
|
|
203
|
-
event.preventDefault();
|
|
204
|
-
event.stopPropagation();
|
|
205
|
-
const view =
|
|
206
|
-
target.closest('[data-flowdular-view]')?.getAttribute(
|
|
207
|
-
'data-flowdular-view',
|
|
208
|
-
) ??
|
|
209
|
-
'';
|
|
210
|
-
window.parent.postMessage(
|
|
211
|
-
{
|
|
212
|
-
type: 'flowdular:selection',
|
|
213
|
-
label: (target.textContent ?? '').trim().slice(0, 80) ||
|
|
214
|
-
target.tagName.toLowerCase(),
|
|
215
|
-
selector: describe(target),
|
|
216
|
-
view,
|
|
217
|
-
text: (target.textContent ?? '').trim().slice(0, 400),
|
|
218
|
-
},
|
|
219
|
-
'*',
|
|
220
|
-
);
|
|
221
|
-
};
|
|
222
|
-
const onOver = (event: MouseEvent) => {
|
|
223
|
-
const target = event.target as HTMLElement | null;
|
|
224
|
-
if (target) target.style.outline = '2px solid #2563eb';
|
|
225
|
-
};
|
|
226
|
-
const onOut = (event: MouseEvent) => {
|
|
227
|
-
const target = event.target as HTMLElement | null;
|
|
228
|
-
if (target) target.style.outline = '';
|
|
229
|
-
};
|
|
230
|
-
document.addEventListener('click', onClick, true);
|
|
231
|
-
document.addEventListener('mouseover', onOver, true);
|
|
232
|
-
document.addEventListener('mouseout', onOut, true);
|
|
233
|
-
return () => {
|
|
234
|
-
document.removeEventListener('click', onClick, true);
|
|
235
|
-
document.removeEventListener('mouseover', onOver, true);
|
|
236
|
-
document.removeEventListener('mouseout', onOut, true);
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export function PreviewHost(props?: RenderRouteProps) @{
|
|
241
|
-
const { t } = useTranslation();
|
|
242
|
-
const preview = useMemo(() => previewState(), []);
|
|
243
|
-
const [contributions] = useValue(preview.state.contributions);
|
|
244
|
-
const [principal] = useValue(preview.state.principal);
|
|
245
|
-
const [status, setStatus] = useValue(preview.state.status);
|
|
246
|
-
const [message, setMessage] = useValue(preview.state.message);
|
|
247
|
-
const [moduleId, setModuleId] = useValue(preview.state.moduleId);
|
|
248
|
-
const [initialView] = useValue(preview.state.initialView);
|
|
249
|
-
|
|
250
|
-
useEffect(() => {
|
|
251
|
-
restoreSandboxLocale();
|
|
252
|
-
const url = props?.url ?? window.location.href;
|
|
253
|
-
const sessionId = sessionIdFromLocation(url);
|
|
254
|
-
const parameters = new URL(url, 'http://sandbox.local').searchParams;
|
|
255
|
-
const selecting = parameters.has('select');
|
|
256
|
-
/* The workbench opens the preview on one module of the session; every
|
|
257
|
-
module still composes, so this only picks the first screen. */
|
|
258
|
-
const openOn = parameters.get('module') ?? '';
|
|
259
|
-
const teardown = installSelection(selecting);
|
|
260
|
-
|
|
261
|
-
void (async () => {
|
|
262
|
-
try {
|
|
263
|
-
const response = await fetch(
|
|
264
|
-
'/sandbox/api/sessions/' + encodeURIComponent(sessionId),
|
|
265
|
-
{ headers: { accept: 'application/json' } },
|
|
266
|
-
);
|
|
267
|
-
const view = await response.json() as
|
|
268
|
-
SessionView & {
|
|
269
|
-
error?: { message?: string };
|
|
270
|
-
};
|
|
271
|
-
if (!response.ok) {
|
|
272
|
-
setMessage(
|
|
273
|
-
view.error?.message ?? t('sandbox.preview.sessionUnavailable'),
|
|
274
|
-
);
|
|
275
|
-
setStatus('failed');
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
setModuleId(view.session.moduleId);
|
|
279
|
-
const identity = await signInPreview(sessionId);
|
|
280
|
-
/* Every draft module with a client entry contributes to the same
|
|
281
|
-
shell, exactly as the platform composes them. */
|
|
282
|
-
const created: ModuleClientContribution[] = [];
|
|
283
|
-
const missing: string[] = [];
|
|
284
|
-
let opening = '';
|
|
285
|
-
for (const module of identity.modules) {
|
|
286
|
-
if (!module.hasClient) continue;
|
|
287
|
-
const loaded = await importDraftModule(
|
|
288
|
-
clientEntry(sessionId, module.directory, module.support),
|
|
289
|
-
);
|
|
290
|
-
if (!loaded.createClientContribution) {
|
|
291
|
-
missing.push(module.id);
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
const contribution = loaded.createClientContribution({
|
|
295
|
-
csrfToken: identity.csrfToken,
|
|
296
|
-
scopes: [...identity.principal.scopes, ...identity.scopes],
|
|
297
|
-
});
|
|
298
|
-
created.push(contribution);
|
|
299
|
-
const entry = contribution.navigation?.[0]?.viewId ?? '';
|
|
300
|
-
if (
|
|
301
|
-
entry &&
|
|
302
|
-
(!opening || !module.support && !openOn ||
|
|
303
|
-
module.directory === openOn)
|
|
304
|
-
) {
|
|
305
|
-
opening = entry;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
if (created.length === 0) {
|
|
309
|
-
setMessage(
|
|
310
|
-
missing.length > 0
|
|
311
|
-
? missing.join(', ') + t('sandbox.preview.missingExport', {
|
|
312
|
-
modules: missing.join(', '),
|
|
313
|
-
})
|
|
314
|
-
: t('sandbox.preview.noClient'),
|
|
315
|
-
);
|
|
316
|
-
setStatus('empty');
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
preview.store.act((transaction) => {
|
|
320
|
-
transaction.set(preview.state.contributions, created);
|
|
321
|
-
transaction.set(preview.state.principal, identity.principal);
|
|
322
|
-
transaction.set(preview.state.initialView, opening);
|
|
323
|
-
}, 'preview/loaded');
|
|
324
|
-
setStatus('ready');
|
|
325
|
-
} catch (error) {
|
|
326
|
-
setMessage(
|
|
327
|
-
error instanceof Error
|
|
328
|
-
? error.message
|
|
329
|
-
: t('sandbox.preview.loadFailed'),
|
|
330
|
-
);
|
|
331
|
-
setStatus('empty');
|
|
332
|
-
} finally {
|
|
333
|
-
signalReady();
|
|
334
|
-
}
|
|
335
|
-
})();
|
|
336
|
-
|
|
337
|
-
return teardown;
|
|
338
|
-
}, []);
|
|
339
|
-
|
|
340
|
-
/* The shell registers document metadata, so the preview provides the same
|
|
341
|
-
Head boundary the platform gives it. */
|
|
342
|
-
<Head>
|
|
343
|
-
@if (status === 'ready' && contributions.length > 0) {
|
|
344
|
-
<ApplicationShell
|
|
345
|
-
contributions={contributions}
|
|
346
|
-
identity={identityFor(principal, contributions, moduleId)}
|
|
347
|
-
initialView={initialView || contributions[0]!.navigation?.[0]?.viewId ||
|
|
348
|
-
'overview'}
|
|
349
|
-
onSignOut={() => undefined}
|
|
350
|
-
onSwitchTenant={() => undefined}
|
|
351
|
-
/>
|
|
352
|
-
} @else if (status === 'loading') {
|
|
353
|
-
<div class="ui-view" style="padding: 24px">
|
|
354
|
-
<p class="ui-help">{t('sandbox.preview.loading')}</p>
|
|
355
|
-
</div>
|
|
356
|
-
} @else {
|
|
357
|
-
<div class="ui-view" style="padding: 24px">
|
|
358
|
-
<EmptyState
|
|
359
|
-
icon="flask"
|
|
360
|
-
title={t('sandbox.preview.empty.title')}
|
|
361
|
-
code="src/client/index.ts"
|
|
362
|
-
>
|
|
363
|
-
{message || t('sandbox.preview.empty.description')}
|
|
364
|
-
</EmptyState>
|
|
365
|
-
@if (status === 'failed') {
|
|
366
|
-
<Alert>{message}</Alert>
|
|
367
|
-
} @else {
|
|
368
|
-
<Tag tone="info">{moduleId || t('sandbox.preview.draftModule')}</Tag>
|
|
369
|
-
}
|
|
370
|
-
</div>
|
|
371
|
-
}
|
|
372
|
-
</Head>
|
|
373
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { ModuleClientContribution } from '@flowdular/sdk/client';
|
|
2
|
-
|
|
3
|
-
export interface DraftModuleEntry {
|
|
4
|
-
readonly createClientContribution?: (context: {
|
|
5
|
-
readonly csrfToken: string;
|
|
6
|
-
readonly scopes: readonly string[];
|
|
7
|
-
}) => ModuleClientContribution;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/* The draft module lives in a session workspace outside this app, so its entry
|
|
11
|
-
is resolved at runtime. The ignore comment stays in this plain module: the
|
|
12
|
-
TSRX compiler drops comments from call arguments, which would make Vite warn
|
|
13
|
-
about an unanalyzable import on every preview load. */
|
|
14
|
-
export function importDraftModule(entry: string): Promise<DraftModuleEntry> {
|
|
15
|
-
return import(/* @vite-ignore */ entry) as Promise<DraftModuleEntry>;
|
|
16
|
-
}
|
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
readSession,
|
|
6
|
-
sessionPaths,
|
|
7
|
-
updateSession,
|
|
8
|
-
type SandboxSession,
|
|
9
|
-
type SessionAttachment,
|
|
10
|
-
} from './sessions.ts';
|
|
11
|
-
import { SandboxSetupError } from './workspace-root.ts';
|
|
12
|
-
|
|
13
|
-
export const MAX_ATTACHMENTS = 10;
|
|
14
|
-
export const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
15
|
-
export const MAX_ATTACHMENT_NAME = 128;
|
|
16
|
-
|
|
17
|
-
interface AttachmentType {
|
|
18
|
-
readonly kind: SessionAttachment['kind'];
|
|
19
|
-
readonly contentType: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/* The only extensions accepted. Images are verified by magic bytes; the
|
|
23
|
-
concept/text formats are accepted by extension, with a NUL-byte guard that
|
|
24
|
-
rejects a binary wearing a text extension. */
|
|
25
|
-
const ATTACHMENT_TYPES: Readonly<Record<string, AttachmentType>> = {
|
|
26
|
-
png: { kind: 'image', contentType: 'image/png' },
|
|
27
|
-
jpg: { kind: 'image', contentType: 'image/jpeg' },
|
|
28
|
-
jpeg: { kind: 'image', contentType: 'image/jpeg' },
|
|
29
|
-
gif: { kind: 'image', contentType: 'image/gif' },
|
|
30
|
-
webp: { kind: 'image', contentType: 'image/webp' },
|
|
31
|
-
md: { kind: 'file', contentType: 'text/markdown; charset=utf-8' },
|
|
32
|
-
txt: { kind: 'file', contentType: 'text/plain; charset=utf-8' },
|
|
33
|
-
json: { kind: 'file', contentType: 'application/json; charset=utf-8' },
|
|
34
|
-
csv: { kind: 'file', contentType: 'text/csv; charset=utf-8' },
|
|
35
|
-
pdf: { kind: 'file', contentType: 'application/pdf' },
|
|
36
|
-
svg: { kind: 'file', contentType: 'image/svg+xml' },
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const ATTACHMENT_ID =
|
|
40
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
41
|
-
|
|
42
|
-
/* Attachment ids are the only client-chosen segment that reaches a file path,
|
|
43
|
-
so anything that is not an id the sandbox minted is refused before a path is
|
|
44
|
-
built from it. */
|
|
45
|
-
export function assertAttachmentId(value: string): string {
|
|
46
|
-
if (!ATTACHMENT_ID.test(value)) {
|
|
47
|
-
throw new SandboxSetupError(
|
|
48
|
-
'INVALID_ATTACHMENT_ID',
|
|
49
|
-
'The attachment id is not a sandbox attachment identifier.',
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
return value;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function extensionOf(name: string): string {
|
|
56
|
-
const dot = name.lastIndexOf('.');
|
|
57
|
-
return dot > 0 ? name.slice(dot + 1).toLowerCase() : '';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/* The original filename reduced to a safe basename: no path segments, only
|
|
61
|
-
[A-Za-z0-9._-], no `..` traversal, no leading dot, and bounded length with
|
|
62
|
-
the extension preserved. */
|
|
63
|
-
function safeAttachmentName(raw: string): string {
|
|
64
|
-
const base = raw.split(/[/\\]/).pop() ?? '';
|
|
65
|
-
let safe = base.replace(/[^A-Za-z0-9._-]/g, '_').replace(/\.{2,}/g, '.');
|
|
66
|
-
safe = safe.replace(/^\.+/, '');
|
|
67
|
-
if (safe.length > MAX_ATTACHMENT_NAME) {
|
|
68
|
-
const dot = safe.lastIndexOf('.');
|
|
69
|
-
const ext = dot > 0 ? safe.slice(dot) : '';
|
|
70
|
-
safe = safe.slice(0, Math.max(1, MAX_ATTACHMENT_NAME - ext.length)) + ext;
|
|
71
|
-
}
|
|
72
|
-
if (!safe || safe === '.' || safe.includes('..')) {
|
|
73
|
-
throw new SandboxSetupError(
|
|
74
|
-
'ATTACHMENT_NAME_INVALID',
|
|
75
|
-
'The attachment filename is not a valid name.',
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
return safe;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/* Images must prove their format; the pdf and svg formats carry a recognisable
|
|
82
|
-
header too. Text formats are accepted by extension. */
|
|
83
|
-
function contentMatches(ext: string, bytes: Buffer): boolean {
|
|
84
|
-
switch (ext) {
|
|
85
|
-
case 'png':
|
|
86
|
-
return (
|
|
87
|
-
bytes.length >= 8 &&
|
|
88
|
-
bytes[0] === 0x89 &&
|
|
89
|
-
bytes[1] === 0x50 &&
|
|
90
|
-
bytes[2] === 0x4e &&
|
|
91
|
-
bytes[3] === 0x47
|
|
92
|
-
);
|
|
93
|
-
case 'jpg':
|
|
94
|
-
case 'jpeg':
|
|
95
|
-
return (
|
|
96
|
-
bytes.length >= 3 &&
|
|
97
|
-
bytes[0] === 0xff &&
|
|
98
|
-
bytes[1] === 0xd8 &&
|
|
99
|
-
bytes[2] === 0xff
|
|
100
|
-
);
|
|
101
|
-
case 'gif': {
|
|
102
|
-
const head = bytes.subarray(0, 6).toString('latin1');
|
|
103
|
-
return head === 'GIF87a' || head === 'GIF89a';
|
|
104
|
-
}
|
|
105
|
-
case 'webp':
|
|
106
|
-
return (
|
|
107
|
-
bytes.length >= 12 &&
|
|
108
|
-
bytes.subarray(0, 4).toString('latin1') === 'RIFF' &&
|
|
109
|
-
bytes.subarray(8, 12).toString('latin1') === 'WEBP'
|
|
110
|
-
);
|
|
111
|
-
case 'pdf':
|
|
112
|
-
return bytes.subarray(0, 5).toString('latin1') === '%PDF-';
|
|
113
|
-
case 'svg': {
|
|
114
|
-
const head = bytes.subarray(0, 4096).toString('utf8').trimStart();
|
|
115
|
-
return head.startsWith('<') && /<svg[\s>]/i.test(head);
|
|
116
|
-
}
|
|
117
|
-
default:
|
|
118
|
-
/* Text formats: reject a binary payload carrying a text extension. */
|
|
119
|
-
return !bytes.subarray(0, 8192).includes(0);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function resolveType(name: string, bytes: Buffer): AttachmentType {
|
|
124
|
-
const ext = extensionOf(name);
|
|
125
|
-
const type = ATTACHMENT_TYPES[ext];
|
|
126
|
-
if (!type) {
|
|
127
|
-
throw new SandboxSetupError(
|
|
128
|
-
'ATTACHMENT_TYPE_REJECTED',
|
|
129
|
-
`Attachments of type .${ext || '(none)'} are not allowed. Allowed: ${Object.keys(
|
|
130
|
-
ATTACHMENT_TYPES,
|
|
131
|
-
).join(', ')}.`,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
if (!contentMatches(ext, bytes)) {
|
|
135
|
-
throw new SandboxSetupError(
|
|
136
|
-
'ATTACHMENT_TYPE_REJECTED',
|
|
137
|
-
`The file contents do not match a .${ext} file.`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
return type;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function attachmentContentType(name: string): string {
|
|
144
|
-
return (
|
|
145
|
-
ATTACHMENT_TYPES[extensionOf(name)]?.contentType ??
|
|
146
|
-
'application/octet-stream'
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/* Two attachments must never share a workspace filename, or one would overwrite
|
|
151
|
-
the other in reference/attachments/. */
|
|
152
|
-
function uniqueName(
|
|
153
|
-
name: string,
|
|
154
|
-
existing: readonly SessionAttachment[],
|
|
155
|
-
): string {
|
|
156
|
-
const taken = new Set(existing.map((item) => item.name));
|
|
157
|
-
if (!taken.has(name)) return name;
|
|
158
|
-
const dot = name.lastIndexOf('.');
|
|
159
|
-
const stem = dot > 0 ? name.slice(0, dot) : name;
|
|
160
|
-
const ext = dot > 0 ? name.slice(dot) : '';
|
|
161
|
-
for (let index = 2; index < 1000; index += 1) {
|
|
162
|
-
const candidate = `${stem}-${index}${ext}`;
|
|
163
|
-
if (!taken.has(candidate)) return candidate;
|
|
164
|
-
}
|
|
165
|
-
return `${randomUUID().slice(0, 8)}-${name}`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function assertInside(root: string, path: string): string {
|
|
169
|
-
const inside = relative(resolve(root), resolve(path));
|
|
170
|
-
if (!inside || inside.startsWith('..') || isAbsolute(inside)) {
|
|
171
|
-
throw new SandboxSetupError(
|
|
172
|
-
'ATTACHMENT_NAME_INVALID',
|
|
173
|
-
'The attachment path escapes the session directory.',
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
return path;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function storedPath(
|
|
180
|
-
paths: ReturnType<typeof sessionPaths>,
|
|
181
|
-
meta: SessionAttachment,
|
|
182
|
-
): string {
|
|
183
|
-
return assertInside(
|
|
184
|
-
paths.attachments,
|
|
185
|
-
join(paths.attachments, `${meta.id}-${meta.name}`),
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function workspacePath(
|
|
190
|
-
paths: ReturnType<typeof sessionPaths>,
|
|
191
|
-
meta: SessionAttachment,
|
|
192
|
-
): string {
|
|
193
|
-
return assertInside(
|
|
194
|
-
paths.workspaceAttachments,
|
|
195
|
-
join(paths.workspaceAttachments, meta.name),
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export async function addAttachment(
|
|
200
|
-
workspaceRoot: string,
|
|
201
|
-
session: SandboxSession,
|
|
202
|
-
input: { readonly name: string; readonly bytes: Buffer },
|
|
203
|
-
): Promise<SessionAttachment> {
|
|
204
|
-
if (input.bytes.byteLength === 0) {
|
|
205
|
-
throw new SandboxSetupError('ATTACHMENT_EMPTY', 'The attachment is empty.');
|
|
206
|
-
}
|
|
207
|
-
if (input.bytes.byteLength > MAX_ATTACHMENT_BYTES) {
|
|
208
|
-
throw new SandboxSetupError(
|
|
209
|
-
'ATTACHMENT_TOO_LARGE',
|
|
210
|
-
`Attachments are limited to ${MAX_ATTACHMENT_BYTES / (1024 * 1024)} MB.`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
const current = await readSession(workspaceRoot, session.id);
|
|
214
|
-
if (current.attachments.length >= MAX_ATTACHMENTS) {
|
|
215
|
-
throw new SandboxSetupError(
|
|
216
|
-
'ATTACHMENT_LIMIT_REACHED',
|
|
217
|
-
`A session may hold at most ${MAX_ATTACHMENTS} attachments.`,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
const type = resolveType(input.name, input.bytes);
|
|
221
|
-
const meta: SessionAttachment = {
|
|
222
|
-
id: randomUUID(),
|
|
223
|
-
name: uniqueName(safeAttachmentName(input.name), current.attachments),
|
|
224
|
-
kind: type.kind,
|
|
225
|
-
size: input.bytes.byteLength,
|
|
226
|
-
addedAt: Date.now(),
|
|
227
|
-
};
|
|
228
|
-
const paths = sessionPaths(workspaceRoot, current.id, current.moduleSuffix);
|
|
229
|
-
await mkdir(paths.attachments, { recursive: true });
|
|
230
|
-
await mkdir(paths.workspaceAttachments, { recursive: true });
|
|
231
|
-
await writeFile(storedPath(paths, meta), input.bytes);
|
|
232
|
-
await writeFile(workspacePath(paths, meta), input.bytes);
|
|
233
|
-
await updateSession(workspaceRoot, current.id, {
|
|
234
|
-
attachments: [...current.attachments, meta],
|
|
235
|
-
});
|
|
236
|
-
return meta;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export async function listAttachments(
|
|
240
|
-
workspaceRoot: string,
|
|
241
|
-
session: SandboxSession,
|
|
242
|
-
): Promise<readonly SessionAttachment[]> {
|
|
243
|
-
return (await readSession(workspaceRoot, session.id)).attachments;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function findAttachment(
|
|
247
|
-
attachments: readonly SessionAttachment[],
|
|
248
|
-
attachmentId: string,
|
|
249
|
-
): SessionAttachment {
|
|
250
|
-
const meta = attachments.find((item) => item.id === attachmentId);
|
|
251
|
-
if (!meta) {
|
|
252
|
-
throw new SandboxSetupError(
|
|
253
|
-
'ATTACHMENT_NOT_FOUND',
|
|
254
|
-
'No such attachment exists in this session.',
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
return meta;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export async function removeAttachment(
|
|
261
|
-
workspaceRoot: string,
|
|
262
|
-
session: SandboxSession,
|
|
263
|
-
attachmentId: string,
|
|
264
|
-
): Promise<void> {
|
|
265
|
-
assertAttachmentId(attachmentId);
|
|
266
|
-
const current = await readSession(workspaceRoot, session.id);
|
|
267
|
-
const meta = findAttachment(current.attachments, attachmentId);
|
|
268
|
-
const paths = sessionPaths(workspaceRoot, current.id, current.moduleSuffix);
|
|
269
|
-
await rm(storedPath(paths, meta), { force: true });
|
|
270
|
-
await rm(workspacePath(paths, meta), { force: true });
|
|
271
|
-
await updateSession(workspaceRoot, current.id, {
|
|
272
|
-
attachments: current.attachments.filter((item) => item.id !== attachmentId),
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export async function readAttachment(
|
|
277
|
-
workspaceRoot: string,
|
|
278
|
-
session: SandboxSession,
|
|
279
|
-
attachmentId: string,
|
|
280
|
-
): Promise<{
|
|
281
|
-
readonly bytes: Buffer;
|
|
282
|
-
readonly contentType: string;
|
|
283
|
-
readonly name: string;
|
|
284
|
-
}> {
|
|
285
|
-
assertAttachmentId(attachmentId);
|
|
286
|
-
const current = await readSession(workspaceRoot, session.id);
|
|
287
|
-
const meta = findAttachment(current.attachments, attachmentId);
|
|
288
|
-
const paths = sessionPaths(workspaceRoot, current.id, current.moduleSuffix);
|
|
289
|
-
const bytes = await readFile(storedPath(paths, meta));
|
|
290
|
-
return {
|
|
291
|
-
bytes,
|
|
292
|
-
contentType: attachmentContentType(meta.name),
|
|
293
|
-
name: meta.name,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/* The note prepended to a turn's instruction when the session has attachments.
|
|
298
|
-
The files are already in reference/attachments/, so the agent opens them with
|
|
299
|
-
its normal file tools; the path plus this note is the whole contract. */
|
|
300
|
-
export function attachmentInstruction(
|
|
301
|
-
attachments: readonly SessionAttachment[],
|
|
302
|
-
): string | null {
|
|
303
|
-
if (attachments.length === 0) return null;
|
|
304
|
-
const list = attachments
|
|
305
|
-
.map((item) => `${item.name} (${item.kind})`)
|
|
306
|
-
.join(', ');
|
|
307
|
-
return `Attachments the operator provided (in reference/attachments/): ${list}. Screenshots show the desired change; read the files before implementing.`;
|
|
308
|
-
}
|