@somewhere-tech/cli 0.27.4 → 0.27.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/advisor.js +5 -2
- package/dist/commands/advisor.js.map +1 -1
- package/dist/commands/init.js +15 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mcp.js +117 -35
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/typecheck.js +1 -1
- package/dist/commands/typecheck.js.map +1 -1
- package/dist/lib/auth.js +2 -0
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/client.js +1 -1
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/config.js +7 -2
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/device-login.js +2 -0
- package/dist/lib/device-login.js.map +1 -1
- package/dist/lib/init-scaffold.js +52 -0
- package/dist/lib/init-scaffold.js.map +1 -0
- package/dist/lib/init-template.js +442 -0
- package/dist/lib/init-template.js.map +1 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/dist/lib/client-hint.js +0 -30
- package/dist/lib/client-hint.js.map +0 -1
- package/dist/lib/pinned-release-retry.js +0 -36
- package/dist/lib/pinned-release-retry.js.map +0 -1
- package/dist/lib/update-check.js +0 -106
- package/dist/lib/update-check.js.map +0 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
3
|
+
const INIT_METADATA = new Set([
|
|
4
|
+
'.DS_Store',
|
|
5
|
+
'.git',
|
|
6
|
+
'.mcp.json',
|
|
7
|
+
'.somewhere.json',
|
|
8
|
+
]);
|
|
9
|
+
/**
|
|
10
|
+
* Rule-9 boundary for `somewhere init`: a new/empty directory receives the
|
|
11
|
+
* starter, while a directory containing application source keeps the original
|
|
12
|
+
* link-only behavior. Dotfiles count as source unless they are metadata the
|
|
13
|
+
* init command itself owns.
|
|
14
|
+
*/
|
|
15
|
+
export function canWriteInitScaffold(dir) {
|
|
16
|
+
if (!existsSync(dir))
|
|
17
|
+
return true;
|
|
18
|
+
return readdirSync(dir).every((name) => INIT_METADATA.has(name));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Write a complete template only after every path passes confinement and
|
|
22
|
+
* overwrite checks. No partial scaffold is written on a preflight failure.
|
|
23
|
+
*/
|
|
24
|
+
export function writeInitScaffold(dir, files) {
|
|
25
|
+
const root = resolve(dir);
|
|
26
|
+
const targets = new Map();
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const normalizedPath = file.path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
29
|
+
if (!normalizedPath || normalizedPath.endsWith('/')) {
|
|
30
|
+
throw new Error(`Invalid scaffold file path: ${file.path}`);
|
|
31
|
+
}
|
|
32
|
+
const target = resolve(root, normalizedPath);
|
|
33
|
+
const rel = relative(root, target);
|
|
34
|
+
if (rel.startsWith('..') || rel === '') {
|
|
35
|
+
throw new Error(`Scaffold file escapes the project directory: ${file.path}`);
|
|
36
|
+
}
|
|
37
|
+
if (targets.has(target)) {
|
|
38
|
+
throw new Error(`Duplicate scaffold file path: ${normalizedPath}`);
|
|
39
|
+
}
|
|
40
|
+
if (existsSync(target)) {
|
|
41
|
+
const kind = lstatSync(target).isDirectory() ? 'directory' : 'file';
|
|
42
|
+
throw new Error(`Refusing to overwrite existing ${kind}: ${normalizedPath}`);
|
|
43
|
+
}
|
|
44
|
+
targets.set(target, { path: normalizedPath, content: file.content });
|
|
45
|
+
}
|
|
46
|
+
for (const [target, file] of targets) {
|
|
47
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
48
|
+
writeFileSync(target, file.content, { encoding: 'utf8', flag: 'wx' });
|
|
49
|
+
}
|
|
50
|
+
return { created: [...targets.values()].map(({ path }) => path) };
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=init-scaffold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init-scaffold.js","sourceRoot":"","sources":["../../src/lib/init-scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,SAAS,EACT,SAAS,EACT,WAAW,EACX,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWvD,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IAC5B,WAAW;IACX,MAAM;IACN,WAAW;IACX,iBAAiB;CAClB,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AACnE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,GAAW,EACX,KAAkC;IAElC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1B,MAAM,OAAO,GAAG,IAAI,GAAG,EAA6C,CAAC;IAErE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACzE,IAAI,CAAC,cAAc,IAAI,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CAAC,+BAA+B,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9D,CAAC;QAED,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;QAC7C,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACnC,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;YACvC,MAAM,IAAI,KAAK,CAAC,gDAAgD,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/E,CAAC;QACD,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,iCAAiC,cAAc,EAAE,CAAC,CAAC;QACrE,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC;YACpE,MAAM,IAAI,KAAK,CAAC,kCAAkC,IAAI,KAAK,cAAc,EAAE,CAAC,CAAC;QAC/E,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,OAAO,EAAE,CAAC;QACrC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;AACpE,CAAC"}
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
const PACKAGE_JSON = `{
|
|
2
|
+
"name": "somewhere-happy-path",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@somewhere-tech/sdk": "^0.7.2",
|
|
12
|
+
"react": "^19.2.0",
|
|
13
|
+
"react-dom": "^19.2.0",
|
|
14
|
+
"react-router-dom": "^7.9.5"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/react": "^19.2.2",
|
|
18
|
+
"@types/react-dom": "^19.2.2",
|
|
19
|
+
"@vitejs/plugin-react": "^5.1.0",
|
|
20
|
+
"typescript": "^5.9.3",
|
|
21
|
+
"vite": "^7.2.2"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
const TSCONFIG = `{
|
|
26
|
+
"compilerOptions": {
|
|
27
|
+
"target": "ES2022",
|
|
28
|
+
"useDefineForClassFields": true,
|
|
29
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
30
|
+
"allowJs": false,
|
|
31
|
+
"skipLibCheck": true,
|
|
32
|
+
"esModuleInterop": true,
|
|
33
|
+
"allowSyntheticDefaultImports": true,
|
|
34
|
+
"strict": true,
|
|
35
|
+
"forceConsistentCasingInFileNames": true,
|
|
36
|
+
"module": "ESNext",
|
|
37
|
+
"moduleResolution": "Bundler",
|
|
38
|
+
"resolveJsonModule": true,
|
|
39
|
+
"isolatedModules": true,
|
|
40
|
+
"noEmit": true,
|
|
41
|
+
"jsx": "react-jsx"
|
|
42
|
+
},
|
|
43
|
+
"include": ["src", "api", "vite.config.ts"]
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
const RUNTIME_TYPES = `export interface AppUser {
|
|
47
|
+
id: string;
|
|
48
|
+
email?: string | null;
|
|
49
|
+
display_name?: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Runtime {
|
|
53
|
+
auth: {
|
|
54
|
+
fromRequest(req: Request): Promise<AppUser | null>;
|
|
55
|
+
};
|
|
56
|
+
db: {
|
|
57
|
+
query<T>(
|
|
58
|
+
sql: string,
|
|
59
|
+
params?: unknown[],
|
|
60
|
+
options?: { user?: AppUser },
|
|
61
|
+
): Promise<{ data: T[]; count: number; changes: number }>;
|
|
62
|
+
};
|
|
63
|
+
fs: {
|
|
64
|
+
uploadFromRequest(
|
|
65
|
+
req: Request,
|
|
66
|
+
options: {
|
|
67
|
+
path: string;
|
|
68
|
+
maxBytes?: number;
|
|
69
|
+
allowedTypes?: string[];
|
|
70
|
+
fieldName?: string;
|
|
71
|
+
public?: boolean;
|
|
72
|
+
},
|
|
73
|
+
): Promise<{
|
|
74
|
+
url: string;
|
|
75
|
+
path: string;
|
|
76
|
+
size: number;
|
|
77
|
+
contentType: string;
|
|
78
|
+
visibility: 'private' | 'public';
|
|
79
|
+
}>;
|
|
80
|
+
setOwner(path: string, user: AppUser): Promise<unknown>;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
const AUTH_FUNCTION = `import {
|
|
85
|
+
somewhereAuth,
|
|
86
|
+
type SwAuthNamespace,
|
|
87
|
+
} from '@somewhere-tech/sdk/server';
|
|
88
|
+
|
|
89
|
+
export default function auth(req: Request, sw: SwAuthNamespace): Promise<Response> {
|
|
90
|
+
return somewhereAuth(req, sw);
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
const DATA_FUNCTION = `import type { Runtime } from './_lib/runtime';
|
|
94
|
+
|
|
95
|
+
interface GreetingRow {
|
|
96
|
+
user_id: string;
|
|
97
|
+
message: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Source of truth: worker/src/runtime/auth.ts:728 (fromRequest) and
|
|
101
|
+
// worker/src/runtime/db.ts:1653 (query). Browser code never receives SQL or a
|
|
102
|
+
// database connection; it calls this same-origin function.
|
|
103
|
+
export default async function data(req: Request, sw: Runtime): Promise<Response> {
|
|
104
|
+
const user = await sw.auth.fromRequest(req);
|
|
105
|
+
if (!user) return Response.json({ error: 'UNAUTHORIZED' }, { status: 401 });
|
|
106
|
+
|
|
107
|
+
const result = await sw.db.query<GreetingRow>(
|
|
108
|
+
'SELECT ? AS user_id, ? AS message',
|
|
109
|
+
[user.id, 'Your database call ran server-side.'],
|
|
110
|
+
);
|
|
111
|
+
return Response.json({ greeting: result.data[0] ?? null });
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
const UPLOAD_FUNCTION = `import type { Runtime } from './_lib/runtime';
|
|
115
|
+
|
|
116
|
+
// Source of truth: worker/src/runtime/auth.ts:728 (fromRequest) and
|
|
117
|
+
// worker/src/runtime/fs.ts:207 (uploadFromRequest). Files are private by
|
|
118
|
+
// default; the returned URL is short-lived, and ownership is assigned here.
|
|
119
|
+
export default async function upload(req: Request, sw: Runtime): Promise<Response> {
|
|
120
|
+
const user = await sw.auth.fromRequest(req);
|
|
121
|
+
if (!user) return Response.json({ error: 'UNAUTHORIZED' }, { status: 401 });
|
|
122
|
+
if (req.method !== 'POST') {
|
|
123
|
+
return Response.json({ error: 'METHOD_NOT_ALLOWED' }, { status: 405 });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const uploaded = await sw.fs.uploadFromRequest(req, {
|
|
127
|
+
path: \`/uploads/\${user.id}/\${crypto.randomUUID()}\`,
|
|
128
|
+
fieldName: 'file',
|
|
129
|
+
maxBytes: 10 * 1024 * 1024,
|
|
130
|
+
allowedTypes: ['image/jpeg', 'image/png', 'application/pdf', 'text/plain'],
|
|
131
|
+
});
|
|
132
|
+
await sw.fs.setOwner(uploaded.path, user);
|
|
133
|
+
return Response.json({ file: uploaded });
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
const APP_TYPES = `export interface ServerGreeting {
|
|
137
|
+
user_id: string;
|
|
138
|
+
message: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface UploadResult {
|
|
142
|
+
url: string;
|
|
143
|
+
path: string;
|
|
144
|
+
size: number;
|
|
145
|
+
contentType: string;
|
|
146
|
+
visibility: 'private' | 'public';
|
|
147
|
+
}
|
|
148
|
+
`;
|
|
149
|
+
const AUTH_SERVICE = `import {
|
|
150
|
+
createSomewhereAuth,
|
|
151
|
+
type SomewhereAuth,
|
|
152
|
+
} from '@somewhere-tech/sdk/auth';
|
|
153
|
+
import type { ServerGreeting, UploadResult } from '../types/app';
|
|
154
|
+
|
|
155
|
+
export const authClient = createSomewhereAuth();
|
|
156
|
+
|
|
157
|
+
export function signIn(
|
|
158
|
+
auth: SomewhereAuth,
|
|
159
|
+
email: string,
|
|
160
|
+
password: string,
|
|
161
|
+
) {
|
|
162
|
+
return auth.signIn({ email, password });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function signUp(
|
|
166
|
+
auth: SomewhereAuth,
|
|
167
|
+
email: string,
|
|
168
|
+
password: string,
|
|
169
|
+
) {
|
|
170
|
+
return auth.signUp({ email, password });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function signOut(auth: SomewhereAuth) {
|
|
174
|
+
return auth.signOut();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function startGoogleSignIn(auth: SomewhereAuth): Promise<void> {
|
|
178
|
+
window.location.assign(await auth.googleSignInUrl({
|
|
179
|
+
redirectUri: window.location.origin + '/auth/callback',
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function requireOk(response: Response): Promise<Response> {
|
|
184
|
+
if (response.ok) return response;
|
|
185
|
+
const value = await response.json().catch(() => ({})) as { message?: string };
|
|
186
|
+
throw new Error(value.message ?? \`Request failed (\${response.status})\`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function loadServerGreeting(
|
|
190
|
+
auth: SomewhereAuth,
|
|
191
|
+
): Promise<ServerGreeting> {
|
|
192
|
+
const response = await requireOk(await auth.fetch('/api/data'));
|
|
193
|
+
const value = await response.json() as { greeting: ServerGreeting };
|
|
194
|
+
return value.greeting;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function uploadFile(
|
|
198
|
+
auth: SomewhereAuth,
|
|
199
|
+
file: File,
|
|
200
|
+
): Promise<UploadResult> {
|
|
201
|
+
const form = new FormData();
|
|
202
|
+
form.set('file', file);
|
|
203
|
+
const response = await requireOk(await auth.fetch('/api/upload', {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
body: form,
|
|
206
|
+
}));
|
|
207
|
+
const value = await response.json() as { file: UploadResult };
|
|
208
|
+
return value.file;
|
|
209
|
+
}
|
|
210
|
+
`;
|
|
211
|
+
const HOME_PAGE = `import {
|
|
212
|
+
useState,
|
|
213
|
+
type ChangeEvent,
|
|
214
|
+
type FormEvent,
|
|
215
|
+
} from 'react';
|
|
216
|
+
import {
|
|
217
|
+
SignedIn,
|
|
218
|
+
SignedOut,
|
|
219
|
+
useAuth,
|
|
220
|
+
useUser,
|
|
221
|
+
} from '@somewhere-tech/sdk/react';
|
|
222
|
+
import {
|
|
223
|
+
loadServerGreeting,
|
|
224
|
+
signIn,
|
|
225
|
+
signOut,
|
|
226
|
+
signUp,
|
|
227
|
+
startGoogleSignIn,
|
|
228
|
+
uploadFile,
|
|
229
|
+
} from '../services/app';
|
|
230
|
+
|
|
231
|
+
export function HomePage() {
|
|
232
|
+
return (
|
|
233
|
+
<main>
|
|
234
|
+
<header>
|
|
235
|
+
<p className="eyebrow">somewhere.tech happy path</p>
|
|
236
|
+
<h1>One secure app, already wired.</h1>
|
|
237
|
+
<p>SDK auth, server-side data, private file uploads, and raw-source deploy.</p>
|
|
238
|
+
</header>
|
|
239
|
+
<SignedOut><AuthForm /></SignedOut>
|
|
240
|
+
<SignedIn><Workspace /></SignedIn>
|
|
241
|
+
</main>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function AuthForm() {
|
|
246
|
+
const auth = useAuth();
|
|
247
|
+
const [email, setEmail] = useState('');
|
|
248
|
+
const [password, setPassword] = useState('');
|
|
249
|
+
const [message, setMessage] = useState('');
|
|
250
|
+
|
|
251
|
+
async function submit(create: boolean) {
|
|
252
|
+
setMessage('');
|
|
253
|
+
try {
|
|
254
|
+
await (create ? signUp(auth, email, password) : signIn(auth, email, password));
|
|
255
|
+
} catch (error) {
|
|
256
|
+
setMessage(error instanceof Error ? error.message : String(error));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<section className="panel">
|
|
262
|
+
<form onSubmit={(event: FormEvent<HTMLFormElement>) => {
|
|
263
|
+
event.preventDefault();
|
|
264
|
+
void submit(false);
|
|
265
|
+
}}>
|
|
266
|
+
<label>Email<input type="email" value={email} onChange={(event: ChangeEvent<HTMLInputElement>) => setEmail(event.target.value)} required /></label>
|
|
267
|
+
<label>Password<input type="password" value={password} onChange={(event: ChangeEvent<HTMLInputElement>) => setPassword(event.target.value)} minLength={8} required /></label>
|
|
268
|
+
<div className="actions">
|
|
269
|
+
<button type="submit">Sign in</button>
|
|
270
|
+
<button type="button" className="secondary" onClick={() => void submit(true)}>Create account</button>
|
|
271
|
+
</div>
|
|
272
|
+
</form>
|
|
273
|
+
<button className="google" onClick={() => void startGoogleSignIn(auth)}>Continue with Google</button>
|
|
274
|
+
{message && <p className="message">{message}</p>}
|
|
275
|
+
</section>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function Workspace() {
|
|
280
|
+
const auth = useAuth();
|
|
281
|
+
const user = useUser();
|
|
282
|
+
const [message, setMessage] = useState('Ready.');
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<section className="panel">
|
|
286
|
+
<p>Signed in as <strong>{user?.email ?? user?.id}</strong></p>
|
|
287
|
+
<div className="actions">
|
|
288
|
+
<button onClick={() => void loadServerGreeting(auth).then((value) => setMessage(value.message)).catch((error: unknown) => setMessage(String(error)))}>
|
|
289
|
+
Run server data example
|
|
290
|
+
</button>
|
|
291
|
+
<label className="upload">
|
|
292
|
+
Upload a private file
|
|
293
|
+
<input type="file" onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
|
294
|
+
const file = event.target.files?.[0];
|
|
295
|
+
if (file) void uploadFile(auth, file).then((value) => setMessage(\`Uploaded \${value.path}\`)).catch((error: unknown) => setMessage(String(error)));
|
|
296
|
+
}} />
|
|
297
|
+
</label>
|
|
298
|
+
<button className="secondary" onClick={() => void signOut(auth)}>Sign out</button>
|
|
299
|
+
</div>
|
|
300
|
+
<p className="message">{message}</p>
|
|
301
|
+
</section>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
`;
|
|
305
|
+
const APP = `import { AuthCallback } from '@somewhere-tech/sdk/react';
|
|
306
|
+
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
|
307
|
+
import { HomePage } from './pages/HomePage';
|
|
308
|
+
|
|
309
|
+
export function App() {
|
|
310
|
+
return (
|
|
311
|
+
<BrowserRouter>
|
|
312
|
+
<Routes>
|
|
313
|
+
<Route path="/auth/callback" element={<AuthCallback provider="google" />} />
|
|
314
|
+
<Route path="*" element={<HomePage />} />
|
|
315
|
+
</Routes>
|
|
316
|
+
</BrowserRouter>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
`;
|
|
320
|
+
const MAIN = `import { StrictMode } from 'react';
|
|
321
|
+
import { createRoot } from 'react-dom/client';
|
|
322
|
+
import { SomewhereAuthProvider } from '@somewhere-tech/sdk/react';
|
|
323
|
+
import { App } from './App';
|
|
324
|
+
import { authClient } from './services/app';
|
|
325
|
+
import './styles.css';
|
|
326
|
+
|
|
327
|
+
createRoot(document.getElementById('root')!).render(
|
|
328
|
+
<StrictMode>
|
|
329
|
+
<SomewhereAuthProvider client={authClient}>
|
|
330
|
+
<App />
|
|
331
|
+
</SomewhereAuthProvider>
|
|
332
|
+
</StrictMode>,
|
|
333
|
+
);
|
|
334
|
+
`;
|
|
335
|
+
const STYLES = `:root {
|
|
336
|
+
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
337
|
+
color: #15201c;
|
|
338
|
+
background: #edf4ef;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
* { box-sizing: border-box; }
|
|
342
|
+
body { margin: 0; min-width: 320px; }
|
|
343
|
+
button, input { font: inherit; }
|
|
344
|
+
main { width: min(760px, calc(100% - 32px)); margin: 12vh auto; }
|
|
345
|
+
header { margin-bottom: 32px; }
|
|
346
|
+
h1 { max-width: 620px; margin: 8px 0; font-size: clamp(2.5rem, 8vw, 5rem); line-height: .95; letter-spacing: -.06em; }
|
|
347
|
+
.eyebrow { color: #14734c; font-weight: 750; text-transform: uppercase; letter-spacing: .12em; }
|
|
348
|
+
.panel { padding: 28px; border: 1px solid #c6d8ce; border-radius: 22px; background: rgba(255,255,255,.82); box-shadow: 0 20px 60px rgba(21,32,28,.08); }
|
|
349
|
+
form, label { display: grid; gap: 8px; }
|
|
350
|
+
form { gap: 18px; }
|
|
351
|
+
input { width: 100%; padding: 12px 14px; border: 1px solid #b8c9c0; border-radius: 10px; background: white; }
|
|
352
|
+
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
|
|
353
|
+
button, .upload { padding: 12px 16px; border: 0; border-radius: 999px; background: #14734c; color: white; cursor: pointer; font-weight: 700; }
|
|
354
|
+
.secondary { background: #dce9e1; color: #18382b; }
|
|
355
|
+
.google { width: 100%; margin-top: 12px; background: #17231e; }
|
|
356
|
+
.upload input { display: none; }
|
|
357
|
+
.message { margin-top: 20px; color: #385548; }
|
|
358
|
+
`;
|
|
359
|
+
const AGENT_GUIDE = `# somewhere.tech project contract
|
|
360
|
+
|
|
361
|
+
This starter is intentionally the one happy path:
|
|
362
|
+
|
|
363
|
+
## Auth
|
|
364
|
+
|
|
365
|
+
Auth is handled by \`@somewhere-tech/sdk\` — use its client and hooks.
|
|
366
|
+
|
|
367
|
+
## Data, files, and deploy
|
|
368
|
+
|
|
369
|
+
- Browser data and file calls go to \`api/*\`. Only those server functions call
|
|
370
|
+
\`sw.db\` and \`sw.fs\`; do not move SQL or platform calls into \`src/\`.
|
|
371
|
+
- Deploy raw source from this directory with \`somewhere deploy\`. Do not run a
|
|
372
|
+
build first and never deploy \`dist/\` or \`build/\`; the platform compiles
|
|
373
|
+
TypeScript, TSX, CSS, and imports.
|
|
374
|
+
|
|
375
|
+
Safe loop:
|
|
376
|
+
|
|
377
|
+
\`\`\`sh
|
|
378
|
+
npm install
|
|
379
|
+
npm run typecheck
|
|
380
|
+
somewhere deploy-check
|
|
381
|
+
somewhere deploy
|
|
382
|
+
\`\`\`
|
|
383
|
+
|
|
384
|
+
Runtime citations for the server data and files examples:
|
|
385
|
+
|
|
386
|
+
- \`worker/src/runtime/db.ts:1653\` query
|
|
387
|
+
- \`worker/src/runtime/fs.ts:207\` uploadFromRequest
|
|
388
|
+
`;
|
|
389
|
+
const README = `# somewhere.tech starter
|
|
390
|
+
|
|
391
|
+
One narrow production architecture is already wired:
|
|
392
|
+
|
|
393
|
+
## Auth
|
|
394
|
+
|
|
395
|
+
Auth is handled by \`@somewhere-tech/sdk\` — use its client and hooks.
|
|
396
|
+
|
|
397
|
+
## Data and files
|
|
398
|
+
|
|
399
|
+
- authenticated data through a server function using \`sw.db\`;
|
|
400
|
+
- private file upload through a server function using \`sw.fs\`;
|
|
401
|
+
|
|
402
|
+
## Run
|
|
403
|
+
|
|
404
|
+
\`\`\`sh
|
|
405
|
+
npm install
|
|
406
|
+
npm run dev
|
|
407
|
+
npm run typecheck
|
|
408
|
+
somewhere deploy-check
|
|
409
|
+
somewhere deploy
|
|
410
|
+
\`\`\`
|
|
411
|
+
|
|
412
|
+
Deploy the raw source. There is intentionally no build script.
|
|
413
|
+
`;
|
|
414
|
+
export function createHappyPathTemplate() {
|
|
415
|
+
return [
|
|
416
|
+
{ path: '.gitignore', content: 'node_modules\\ndist\\nbuild\\n.env\\n' },
|
|
417
|
+
{ path: 'AGENTS.md', content: AGENT_GUIDE },
|
|
418
|
+
{ path: 'CLAUDE.md', content: AGENT_GUIDE },
|
|
419
|
+
{ path: 'README.md', content: README },
|
|
420
|
+
{ path: 'package.json', content: PACKAGE_JSON },
|
|
421
|
+
{ path: 'tsconfig.json', content: TSCONFIG },
|
|
422
|
+
{
|
|
423
|
+
path: 'vite.config.ts',
|
|
424
|
+
content: `import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\nexport default defineConfig({ plugins: [react()] });\n`,
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
path: 'index.html',
|
|
428
|
+
content: `<!doctype html>\n<html lang="en">\n <head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>somewhere.tech starter</title></head>\n <body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>\n</html>\n`,
|
|
429
|
+
},
|
|
430
|
+
{ path: 'api/_lib/runtime.ts', content: RUNTIME_TYPES },
|
|
431
|
+
{ path: 'api/auth/[...path].ts', content: AUTH_FUNCTION },
|
|
432
|
+
{ path: 'api/data.ts', content: DATA_FUNCTION },
|
|
433
|
+
{ path: 'api/upload.ts', content: UPLOAD_FUNCTION },
|
|
434
|
+
{ path: 'src/types/app.ts', content: APP_TYPES },
|
|
435
|
+
{ path: 'src/services/app.ts', content: AUTH_SERVICE },
|
|
436
|
+
{ path: 'src/pages/HomePage.tsx', content: HOME_PAGE },
|
|
437
|
+
{ path: 'src/App.tsx', content: APP },
|
|
438
|
+
{ path: 'src/main.tsx', content: MAIN },
|
|
439
|
+
{ path: 'src/styles.css', content: STYLES },
|
|
440
|
+
];
|
|
441
|
+
}
|
|
442
|
+
//# sourceMappingURL=init-template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init-template.js","sourceRoot":"","sources":["../../src/lib/init-template.ts"],"names":[],"mappings":"AAEA,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;CAuBpB,CAAC;AAEF,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;CAoBhB,CAAC;AAEF,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCrB,CAAC;AAEF,MAAM,aAAa,GAAG;;;;;;;;CAQrB,CAAC;AAEF,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;CAoBrB,CAAC;AAEF,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;CAqBvB,CAAC;AAEF,MAAM,SAAS,GAAG;;;;;;;;;;;;CAYjB,CAAC;AAEF,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6DpB,CAAC;AAEF,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6FjB,CAAC;AAEF,MAAM,GAAG,GAAG;;;;;;;;;;;;;;CAcX,CAAC;AAEF,MAAM,IAAI,GAAG;;;;;;;;;;;;;;CAcZ,CAAC;AAEF,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;CAuBd,CAAC;AAEF,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BnB,CAAC;AAEF,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;CAwBd,CAAC;AAEF,MAAM,UAAU,uBAAuB;IACrC,OAAO;QACL,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,uCAAuC,EAAE;QACxE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE;QAC3C,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE;QAC3C,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE;QACtC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE;QAC/C,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE;QAC5C;YACE,IAAI,EAAE,gBAAgB;YACtB,OAAO,EAAE,2IAA2I;SACrJ;QACD;YACE,IAAI,EAAE,YAAY;YAClB,OAAO,EAAE,6RAA6R;SACvS;QACD,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,aAAa,EAAE;QACvD,EAAE,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,aAAa,EAAE;QACzD,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE;QAC/C,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,eAAe,EAAE;QACnD,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE;QAChD,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,YAAY,EAAE;QACtD,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,SAAS,EAAE;QACtD,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,EAAE;QACrC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE;QACvC,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE;KAC5C,CAAC;AACJ,CAAC"}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@somewhere-tech/cli",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.6",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@somewhere-tech/cli",
|
|
9
|
-
"version": "0.27.
|
|
9
|
+
"version": "0.27.6",
|
|
10
10
|
"bundleDependencies": [
|
|
11
11
|
"@modelcontextprotocol/sdk",
|
|
12
12
|
"chokidar",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@somewhere-tech/cli",
|
|
3
|
-
"version": "0.27.
|
|
4
|
-
"description": "CLI for somewhere.tech
|
|
3
|
+
"version": "0.27.6",
|
|
4
|
+
"description": "CLI for somewhere.tech \u2014 auth, projects, deploy, pull, promote, db, logs, env, MCP bridge.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"somewhere",
|
|
7
7
|
"somewhere.tech",
|
package/dist/lib/client-hint.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
export function detectAgentOrigin(env) {
|
|
5
|
-
if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT)
|
|
6
|
-
return 'claude-code';
|
|
7
|
-
if (env.CURSOR_TRACE_ID || env.CURSOR_AGENT)
|
|
8
|
-
return 'cursor';
|
|
9
|
-
if (Object.keys(env).some((key) => key.startsWith('CODEX_')))
|
|
10
|
-
return 'codex';
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
function currentVersion() {
|
|
14
|
-
try {
|
|
15
|
-
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json'), 'utf8'));
|
|
16
|
-
return typeof pkg.version === 'string' && /^[0-9A-Za-z.+_-]+$/.test(pkg.version)
|
|
17
|
-
? pkg.version
|
|
18
|
-
: 'unknown';
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return 'unknown';
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
/** Privacy-limited client hint: only CLI version plus a detected agent name.
|
|
25
|
-
* Environment variable values are never transmitted. */
|
|
26
|
-
export function buildClientHint(env = process.env, version = currentVersion()) {
|
|
27
|
-
const agent = detectAgentOrigin(env);
|
|
28
|
-
return `cli/${version}${agent ? `+${agent}` : ''}`;
|
|
29
|
-
}
|
|
30
|
-
//# sourceMappingURL=client-hint.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client-hint.js","sourceRoot":"","sources":["../../src/lib/client-hint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAIzC,MAAM,UAAU,iBAAiB,CAAC,GAAsB;IACtD,IAAI,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,sBAAsB;QAAE,OAAO,aAAa,CAAC;IACvE,IAAI,GAAG,CAAC,eAAe,IAAI,GAAG,CAAC,YAAY;QAAE,OAAO,QAAQ,CAAC;IAC7D,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAAE,OAAO,OAAO,CAAC;IAC7E,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,cAAc;IACrB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CACpB,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CACvE,CAAC;QAC3B,OAAO,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,IAAI,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAC9E,CAAC,CAAC,GAAG,CAAC,OAAO;YACb,CAAC,CAAC,SAAS,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;yDACyD;AACzD,MAAM,UAAU,eAAe,CAC7B,MAAyB,OAAO,CAAC,GAAG,EACpC,OAAO,GAAG,cAAc,EAAE;IAE1B,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;IACrC,OAAO,OAAO,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AACrD,CAAC"}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
function isRecord(value) {
|
|
2
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
-
}
|
|
4
|
-
function requiredBaseReleaseId(error) {
|
|
5
|
-
if (!isRecord(error) ||
|
|
6
|
-
error.statusCode !== 400 ||
|
|
7
|
-
error.code !== 'BASE_RELEASE_REQUIRED' ||
|
|
8
|
-
!isRecord(error.data)) {
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
|
-
const activeReleaseId = error.data.active_release_id;
|
|
12
|
-
return typeof activeReleaseId === 'string' && activeReleaseId.trim().length > 0
|
|
13
|
-
? activeReleaseId
|
|
14
|
-
: null;
|
|
15
|
-
}
|
|
16
|
-
export function pinnedReleaseRetryBody(error, body) {
|
|
17
|
-
const activeReleaseId = requiredBaseReleaseId(error);
|
|
18
|
-
return activeReleaseId === null
|
|
19
|
-
? null
|
|
20
|
-
: { ...body, base_release_id: activeReleaseId };
|
|
21
|
-
}
|
|
22
|
-
export async function callWithPinnedReleaseRetry(request, body) {
|
|
23
|
-
try {
|
|
24
|
-
return await request(body);
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
const retryBody = pinnedReleaseRetryBody(error, body);
|
|
28
|
-
if (retryBody === null)
|
|
29
|
-
throw error;
|
|
30
|
-
// Safe to retry once: this is the base the worker just declared current. If
|
|
31
|
-
// another publish lands first, its CAS returns STALE_RELEASE_BASE rather than
|
|
32
|
-
// double-applying the edit, preserving the protection this field provides.
|
|
33
|
-
return request(retryBody);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
//# sourceMappingURL=pinned-release-retry.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-release-retry.js","sourceRoot":"","sources":["../../src/lib/pinned-release-retry.ts"],"names":[],"mappings":"AAAA,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAc;IAC3C,IACE,CAAC,QAAQ,CAAC,KAAK,CAAC;QAChB,KAAK,CAAC,UAAU,KAAK,GAAG;QACxB,KAAK,CAAC,IAAI,KAAK,uBAAuB;QACtC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EACrB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,eAAe,GAAG,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC;IACrD,OAAO,OAAO,eAAe,KAAK,QAAQ,IAAI,eAAe,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAC7E,CAAC,CAAC,eAAe;QACjB,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,KAAc,EACd,IAA6B;IAE7B,MAAM,eAAe,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;IACrD,OAAO,eAAe,KAAK,IAAI;QAC7B,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,eAAe,EAAE,eAAe,EAAE,CAAC;AACpD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,OAAsD,EACtD,IAA6B;IAE7B,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,SAAS,GAAG,sBAAsB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACtD,IAAI,SAAS,KAAK,IAAI;YAAE,MAAM,KAAK,CAAC;QAEpC,4EAA4E;QAC5E,8EAA8E;QAC9E,2EAA2E;QAC3E,OAAO,OAAO,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC;AACH,CAAC"}
|