@trypattern/sdk 0.0.0-stage → 0.1.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.
Files changed (273) hide show
  1. package/README.md +127 -2
  2. package/dist/access.d.ts +31 -0
  3. package/dist/access.js +72 -0
  4. package/dist/actions.d.ts +57 -0
  5. package/dist/actions.js +198 -0
  6. package/dist/api.d.ts +30 -0
  7. package/dist/api.js +60 -0
  8. package/dist/app.d.ts +115 -0
  9. package/dist/app.js +266 -0
  10. package/dist/appInfo.d.ts +35 -0
  11. package/dist/appInfo.js +67 -0
  12. package/dist/appTitle.d.ts +6 -0
  13. package/dist/appTitle.js +3 -0
  14. package/dist/appearance.d.ts +7 -0
  15. package/dist/appearance.js +2 -0
  16. package/dist/auth/sessionApi.d.ts +41 -0
  17. package/dist/auth/sessionApi.js +158 -0
  18. package/dist/auth/sessionStore.d.ts +20 -0
  19. package/dist/auth/sessionStore.js +40 -0
  20. package/dist/auth/user.d.ts +11 -0
  21. package/dist/auth/user.js +9 -0
  22. package/dist/builtins/Account.d.ts +25 -0
  23. package/dist/builtins/Account.js +15 -0
  24. package/dist/builtins/AppSettings.d.ts +12 -0
  25. package/dist/builtins/AppSettings.js +31 -0
  26. package/dist/builtins/Palette.d.ts +29 -0
  27. package/dist/builtins/Palette.js +63 -0
  28. package/dist/builtins/SignIn.d.ts +28 -0
  29. package/dist/builtins/SignIn.js +60 -0
  30. package/dist/builtins/Toasts.d.ts +32 -0
  31. package/dist/builtins/Toasts.js +25 -0
  32. package/dist/builtins/actions.d.ts +2 -0
  33. package/dist/builtins/actions.js +45 -0
  34. package/dist/cache.d.ts +24 -0
  35. package/dist/cache.js +112 -0
  36. package/dist/cli/app.d.ts +2 -0
  37. package/dist/cli/app.js +1406 -0
  38. package/dist/cli/publishing.d.ts +21 -0
  39. package/dist/cli/publishing.js +493 -0
  40. package/dist/cli/screenshot.d.ts +1 -0
  41. package/dist/cli/screenshot.js +96 -0
  42. package/dist/cli/tools.d.ts +12 -0
  43. package/dist/cli/tools.js +35 -0
  44. package/dist/clock.d.ts +6 -0
  45. package/dist/clock.js +31 -0
  46. package/dist/components/AppFrame.d.ts +14 -0
  47. package/dist/components/AppFrame.js +64 -0
  48. package/dist/components/AppearancePicker.d.ts +1 -0
  49. package/dist/components/AppearancePicker.js +12 -0
  50. package/dist/components/Avatar.d.ts +9 -0
  51. package/dist/components/Avatar.js +63 -0
  52. package/dist/components/Badge.d.ts +11 -0
  53. package/dist/components/Badge.js +87 -0
  54. package/dist/components/Button.d.ts +24 -0
  55. package/dist/components/Button.js +252 -0
  56. package/dist/components/Card.d.ts +12 -0
  57. package/dist/components/Card.js +38 -0
  58. package/dist/components/CommandMenu.d.ts +23 -0
  59. package/dist/components/CommandMenu.js +214 -0
  60. package/dist/components/CommandPalette.d.ts +1 -0
  61. package/dist/components/CommandPalette.js +7 -0
  62. package/dist/components/Content.d.ts +7 -0
  63. package/dist/components/Content.js +24 -0
  64. package/dist/components/ContextMenu.d.ts +8 -0
  65. package/dist/components/ContextMenu.js +23 -0
  66. package/dist/components/CopyButton.d.ts +11 -0
  67. package/dist/components/CopyButton.js +16 -0
  68. package/dist/components/Dialog.d.ts +11 -0
  69. package/dist/components/Dialog.js +81 -0
  70. package/dist/components/Divider.d.ts +7 -0
  71. package/dist/components/Divider.js +30 -0
  72. package/dist/components/EmptyState.d.ts +8 -0
  73. package/dist/components/EmptyState.js +24 -0
  74. package/dist/components/Form.d.ts +9 -0
  75. package/dist/components/Form.js +23 -0
  76. package/dist/components/Icon.d.ts +10 -0
  77. package/dist/components/Icon.js +22 -0
  78. package/dist/components/IconButton.d.ts +14 -0
  79. package/dist/components/IconButton.js +53 -0
  80. package/dist/components/Kbd.d.ts +6 -0
  81. package/dist/components/Kbd.js +67 -0
  82. package/dist/components/Link.d.ts +14 -0
  83. package/dist/components/Link.js +44 -0
  84. package/dist/components/ListRow.d.ts +12 -0
  85. package/dist/components/ListRow.js +62 -0
  86. package/dist/components/LoadingRows.d.ts +5 -0
  87. package/dist/components/LoadingRows.js +6 -0
  88. package/dist/components/Menu.d.ts +12 -0
  89. package/dist/components/Menu.js +33 -0
  90. package/dist/components/MenuItems.d.ts +21 -0
  91. package/dist/components/MenuItems.js +32 -0
  92. package/dist/components/Placement.d.ts +21 -0
  93. package/dist/components/Placement.js +1 -0
  94. package/dist/components/Popup.d.ts +8 -0
  95. package/dist/components/Popup.js +43 -0
  96. package/dist/components/PopupItem.d.ts +8 -0
  97. package/dist/components/PopupItem.js +51 -0
  98. package/dist/components/PropertyRow.d.ts +7 -0
  99. package/dist/components/PropertyRow.js +23 -0
  100. package/dist/components/RadioGroup.d.ts +18 -0
  101. package/dist/components/RadioGroup.js +85 -0
  102. package/dist/components/Row.d.ts +16 -0
  103. package/dist/components/Row.js +27 -0
  104. package/dist/components/Screen.d.ts +13 -0
  105. package/dist/components/Screen.js +56 -0
  106. package/dist/components/Scrim.d.ts +8 -0
  107. package/dist/components/Scrim.js +40 -0
  108. package/dist/components/ScrollArea.d.ts +15 -0
  109. package/dist/components/ScrollArea.js +13 -0
  110. package/dist/components/SegmentedControl.d.ts +17 -0
  111. package/dist/components/SegmentedControl.js +68 -0
  112. package/dist/components/Select.d.ts +17 -0
  113. package/dist/components/Select.js +107 -0
  114. package/dist/components/SettingsRow.d.ts +8 -0
  115. package/dist/components/SettingsRow.js +32 -0
  116. package/dist/components/Sidebar.d.ts +20 -0
  117. package/dist/components/Sidebar.js +137 -0
  118. package/dist/components/SignInScreen.d.ts +1 -0
  119. package/dist/components/SignInScreen.js +40 -0
  120. package/dist/components/Size.d.ts +1 -0
  121. package/dist/components/Size.js +1 -0
  122. package/dist/components/Skeleton.d.ts +8 -0
  123. package/dist/components/Skeleton.js +48 -0
  124. package/dist/components/Spinner.d.ts +6 -0
  125. package/dist/components/Spinner.js +54 -0
  126. package/dist/components/Stack.d.ts +14 -0
  127. package/dist/components/Stack.js +19 -0
  128. package/dist/components/Switch.d.ts +10 -0
  129. package/dist/components/Switch.js +72 -0
  130. package/dist/components/Text.d.ts +21 -0
  131. package/dist/components/Text.js +77 -0
  132. package/dist/components/TextField.d.ts +25 -0
  133. package/dist/components/TextField.js +68 -0
  134. package/dist/components/Toaster.d.ts +1 -0
  135. package/dist/components/Toaster.js +83 -0
  136. package/dist/components/Tooltip.d.ts +8 -0
  137. package/dist/components/Tooltip.js +43 -0
  138. package/dist/components/controlLook.d.ts +70 -0
  139. package/dist/components/controlLook.js +80 -0
  140. package/dist/components/frame.d.ts +4 -0
  141. package/dist/components/frame.js +2 -0
  142. package/dist/components/icons.d.ts +59 -0
  143. package/dist/components/icons.js +111 -0
  144. package/dist/components/interaction.d.ts +12 -0
  145. package/dist/components/interaction.js +22 -0
  146. package/dist/components/spacing.d.ts +118 -0
  147. package/dist/components/spacing.js +46 -0
  148. package/dist/components/tones.d.ts +28 -0
  149. package/dist/components/tones.js +12 -0
  150. package/dist/entityStore.d.ts +46 -0
  151. package/dist/entityStore.js +160 -0
  152. package/dist/flows/accessFlows.d.ts +9 -0
  153. package/dist/flows/accessFlows.js +167 -0
  154. package/dist/flows/browser.d.ts +34 -0
  155. package/dist/flows/browser.js +167 -0
  156. package/dist/flows/contactSheet.d.ts +1 -0
  157. package/dist/flows/contactSheet.js +63 -0
  158. package/dist/flows/flowBrowser.d.ts +25 -0
  159. package/dist/flows/flowBrowser.js +23 -0
  160. package/dist/flows/flowSetup.d.ts +3 -0
  161. package/dist/flows/flowSetup.js +35 -0
  162. package/dist/flows/flows.d.ts +47 -0
  163. package/dist/flows/flows.js +175 -0
  164. package/dist/flows/screenshotName.d.ts +1 -0
  165. package/dist/flows/screenshotName.js +6 -0
  166. package/dist/gate.d.ts +8 -0
  167. package/dist/gate.js +52 -0
  168. package/dist/hooks/appContext.d.ts +15 -0
  169. package/dist/hooks/appContext.js +5 -0
  170. package/dist/hooks/appHooks.d.ts +21 -0
  171. package/dist/hooks/appHooks.js +24 -0
  172. package/dist/hooks/useAnimatesOnChange.d.ts +1 -0
  173. package/dist/hooks/useAnimatesOnChange.js +14 -0
  174. package/dist/hooks/useStoreState.d.ts +6 -0
  175. package/dist/hooks/useStoreState.js +5 -0
  176. package/dist/index.d.ts +44 -0
  177. package/dist/index.js +21 -0
  178. package/dist/memoryApi.d.ts +19 -0
  179. package/dist/memoryApi.js +42 -0
  180. package/dist/model.d.ts +94 -0
  181. package/dist/model.js +243 -0
  182. package/dist/page.d.ts +11 -0
  183. package/dist/page.js +9 -0
  184. package/dist/platform.d.ts +1 -0
  185. package/dist/platform.js +5 -0
  186. package/dist/publishWire.d.ts +271 -0
  187. package/dist/publishWire.js +168 -0
  188. package/dist/react.d.ts +49 -0
  189. package/dist/react.js +40 -0
  190. package/dist/registered.d.ts +60 -0
  191. package/dist/registered.js +19 -0
  192. package/dist/routes.d.ts +38 -0
  193. package/dist/routes.js +21 -0
  194. package/dist/rules/checkAllFiles.d.ts +1 -0
  195. package/dist/rules/checkAllFiles.js +749 -0
  196. package/dist/rules/checkApiRouting.d.ts +1 -0
  197. package/dist/rules/checkApiRouting.js +112 -0
  198. package/dist/rules/lintPlugin.d.ts +252 -0
  199. package/dist/rules/lintPlugin.js +1159 -0
  200. package/dist/rules/messages.d.ts +48 -0
  201. package/dist/rules/messages.js +48 -0
  202. package/dist/rules/themeTokens.d.ts +6 -0
  203. package/dist/rules/themeTokens.js +64 -0
  204. package/dist/schema.d.ts +4 -0
  205. package/dist/schema.js +2 -0
  206. package/dist/server/auth.d.ts +115 -0
  207. package/dist/server/auth.js +134 -0
  208. package/dist/server/authService.d.ts +9 -0
  209. package/dist/server/authService.js +50 -0
  210. package/dist/server/createServer.d.ts +402 -0
  211. package/dist/server/createServer.js +82 -0
  212. package/dist/server/env.d.ts +15 -0
  213. package/dist/server/env.js +1 -0
  214. package/dist/server/index.d.ts +13 -0
  215. package/dist/server/index.js +9 -0
  216. package/dist/server/services.d.ts +9 -0
  217. package/dist/server/services.js +47 -0
  218. package/dist/server/session.d.ts +29 -0
  219. package/dist/server/session.js +65 -0
  220. package/dist/server/sessions.d.ts +92 -0
  221. package/dist/server/sessions.js +47 -0
  222. package/dist/server/settings.d.ts +49 -0
  223. package/dist/server/settings.js +25 -0
  224. package/dist/server/signInWire.d.ts +53 -0
  225. package/dist/server/signInWire.js +23 -0
  226. package/dist/server/tables.d.ts +242 -0
  227. package/dist/server/tables.js +246 -0
  228. package/dist/server/users.d.ts +189 -0
  229. package/dist/server/users.js +49 -0
  230. package/dist/serviceClients.d.ts +5 -0
  231. package/dist/serviceClients.js +70 -0
  232. package/dist/services.d.ts +53 -0
  233. package/dist/services.js +30 -0
  234. package/dist/shortcuts.d.ts +3 -0
  235. package/dist/shortcuts.js +40 -0
  236. package/dist/start.d.ts +20 -0
  237. package/dist/start.js +145 -0
  238. package/dist/state.d.ts +14 -0
  239. package/dist/state.js +17 -0
  240. package/dist/store.d.ts +45 -0
  241. package/dist/store.js +208 -0
  242. package/dist/subscribers.d.ts +5 -0
  243. package/dist/subscribers.js +13 -0
  244. package/dist/table.d.ts +143 -0
  245. package/dist/table.js +91 -0
  246. package/dist/tables.d.ts +77 -0
  247. package/dist/tables.js +216 -0
  248. package/dist/theme/reset.css +84 -0
  249. package/dist/theme/scales.d.ts +76 -0
  250. package/dist/theme/scales.js +82 -0
  251. package/dist/theme/tokens.stylex.d.ts +218 -0
  252. package/dist/theme/tokens.stylex.js +233 -0
  253. package/dist/vite/commandBridge.d.ts +2 -0
  254. package/dist/vite/commandBridge.js +46 -0
  255. package/dist/vite/designManifest.d.ts +25 -0
  256. package/dist/vite/designManifest.js +191 -0
  257. package/dist/vite/localWorker.d.ts +4 -0
  258. package/dist/vite/localWorker.js +60 -0
  259. package/dist/vite/projectFile.d.ts +9 -0
  260. package/dist/vite/projectFile.js +17 -0
  261. package/dist/vite/rootAlias.d.ts +2 -0
  262. package/dist/vite/rootAlias.js +15 -0
  263. package/dist/vite/vite.d.ts +8 -0
  264. package/dist/vite/vite.js +95 -0
  265. package/dist/wire.d.ts +7 -0
  266. package/dist/wire.js +1 -0
  267. package/package.json +103 -3
  268. package/presets/lintPlugin.js +7 -0
  269. package/presets/oxlint.json +167 -0
  270. package/presets/register.d.ts +21 -0
  271. package/presets/server.d.ts +2 -0
  272. package/presets/tsconfig.app.json +29 -0
  273. package/presets/tsconfig.server.json +27 -0
@@ -0,0 +1,21 @@
1
+ type Outcome = {
2
+ code: number;
3
+ data: Record<string, unknown>;
4
+ text: string;
5
+ };
6
+ export type PublishingIo = {
7
+ say: (line: string) => void;
8
+ openUrl: (url: string) => void;
9
+ run: (bin: string, args: readonly string[], cwd: string) => Promise<{
10
+ code: number;
11
+ output: string;
12
+ }>;
13
+ build: () => Promise<Outcome>;
14
+ sdkVersion: string;
15
+ };
16
+ export declare function login(io: PublishingIo): Promise<Outcome>;
17
+ export declare function pull(io: PublishingIo, name: string | undefined, cwd: string): Promise<Outcome>;
18
+ export declare function publish(io: PublishingIo, root: string, isStarter: boolean): Promise<Outcome>;
19
+ export declare function versions(io: PublishingIo, root: string): Promise<Outcome>;
20
+ export declare function restore(io: PublishingIo, root: string, number: string | undefined): Promise<Outcome>;
21
+ export {};
@@ -0,0 +1,493 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
+ import { homedir, hostname, userInfo } from 'node:os';
4
+ import { basename, dirname, extname, join, matchesGlob, relative } from 'node:path';
5
+ import { z } from 'zod';
6
+ import { DASHBOARD_URL, UPLOAD, appSummary, isPrivatePath, loginPoll, loginStarted, nameProblem, publishSummary, sourcePathProblem } from "../publishWire.js";
7
+ import { appInfo } from "../appTitle.js";
8
+ import { PROJECT_FILE, readProjectFile } from "../vite/projectFile.js";
9
+ const CONFIG_DIR = join(homedir(), '.config', 'pattern');
10
+ const CREDENTIALS_FILE = join(CONFIG_DIR, 'credentials.json');
11
+ const PENDING_FILE = join(CONFIG_DIR, 'pending-login.json');
12
+ const LOGIN_MINUTES = 10;
13
+ const POLL_MS = 2000;
14
+ const LOGIN_WAIT_MS = 120_000;
15
+ const PUBLISH_WAIT_MS = 300_000;
16
+ const SESSION_COOKIE = 'pattern_session';
17
+ const IGNORED_IN_EMPTY_FOLDER = new Set(['.DS_Store', '.claude']);
18
+ const MIME_TYPES = {
19
+ '.html': 'text/html',
20
+ '.js': 'text/javascript',
21
+ '.mjs': 'text/javascript',
22
+ '.css': 'text/css',
23
+ '.json': 'application/json',
24
+ '.map': 'application/json',
25
+ '.webmanifest': 'application/manifest+json',
26
+ '.svg': 'image/svg+xml',
27
+ '.png': 'image/png',
28
+ '.jpg': 'image/jpeg',
29
+ '.jpeg': 'image/jpeg',
30
+ '.gif': 'image/gif',
31
+ '.webp': 'image/webp',
32
+ '.avif': 'image/avif',
33
+ '.ico': 'image/x-icon',
34
+ '.woff2': 'font/woff2',
35
+ '.woff': 'font/woff',
36
+ '.txt': 'text/plain',
37
+ '.xml': 'application/xml',
38
+ '.pdf': 'application/pdf',
39
+ '.mp4': 'video/mp4',
40
+ '.sql': 'application/sql'
41
+ };
42
+ const STEP_WORDS = {
43
+ reserve: 'Reserving the address',
44
+ database: 'Updating the database',
45
+ 'sign-in': 'Turning on sign-in',
46
+ publish: 'Publishing the site'
47
+ };
48
+ function dashboard() {
49
+ return (process.env['PATTERN_DASHBOARD_URL'] ?? DASHBOARD_URL).replace(/\/$/, '');
50
+ }
51
+ const credentials = z.record(z.string(), z.object({ cookie: z.string() }));
52
+ const pendingLogin = z.object({
53
+ dashboard: z.string(),
54
+ secret: z.string(),
55
+ code: z.string(),
56
+ url: z.string(),
57
+ startedAt: z.number()
58
+ });
59
+ function readJson(path, schema) {
60
+ try {
61
+ const parsed = schema.safeParse(JSON.parse(readFileSync(path, 'utf8')));
62
+ return parsed.success ? parsed.data : undefined;
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ }
68
+ function writePrivate(path, value) {
69
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
70
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
71
+ }
72
+ function savedCookie() {
73
+ return readJson(CREDENTIALS_FILE, credentials)?.[dashboard()]?.cookie;
74
+ }
75
+ function saveCookie(cookie) {
76
+ const all = { ...readJson(CREDENTIALS_FILE, credentials) };
77
+ if (cookie === undefined)
78
+ delete all[dashboard()];
79
+ else
80
+ all[dashboard()] = { cookie };
81
+ writePrivate(CREDENTIALS_FILE, all);
82
+ }
83
+ class SignedOut extends Error {
84
+ }
85
+ async function call(path, init = {}) {
86
+ const cookie = savedCookie();
87
+ if (cookie === undefined)
88
+ throw new SignedOut();
89
+ const response = await fetch(`${dashboard()}${path}`, {
90
+ ...init,
91
+ headers: {
92
+ ...init.headers,
93
+ cookie,
94
+ origin: dashboard()
95
+ }
96
+ });
97
+ const renewed = response.headers
98
+ .getSetCookie()
99
+ .find((line) => line.startsWith(`${SESSION_COOKIE}=`));
100
+ if (renewed !== undefined) {
101
+ const pair = renewed.split(';')[0] ?? '';
102
+ if (pair !== `${SESSION_COOKIE}=`)
103
+ saveCookie(pair);
104
+ }
105
+ if (response.status === 401) {
106
+ saveCookie(undefined);
107
+ throw new SignedOut();
108
+ }
109
+ return response;
110
+ }
111
+ async function callJson(path, schema, init = {}) {
112
+ const response = await call(path, init);
113
+ const body = await response.json().catch(() => null);
114
+ if (!response.ok) {
115
+ const error = Reflect.get(Object(body), 'error');
116
+ throw new Error(typeof error === 'string' ? error : `HTTP ${response.status}`);
117
+ }
118
+ return schema.parse(body);
119
+ }
120
+ function labelOfThisMachine() {
121
+ const machine = hostname().replace(/\.local$/, '');
122
+ return `${userInfo().username} on ${machine}`.slice(0, 80);
123
+ }
124
+ function sleep(ms) {
125
+ return new Promise((done) => setTimeout(done, ms));
126
+ }
127
+ async function waitForApproval(secret) {
128
+ const deadline = Date.now() + LOGIN_WAIT_MS;
129
+ while (Date.now() < deadline) {
130
+ const response = await fetch(`${dashboard()}/api/auth/cli/logins/${secret}`);
131
+ const poll = loginPoll.safeParse(await response.json().catch(() => null));
132
+ if (poll.success && poll.data.status === 'approved')
133
+ return poll.data.cookie;
134
+ await sleep(POLL_MS);
135
+ }
136
+ return undefined;
137
+ }
138
+ async function signIn(io, again) {
139
+ const pending = readJson(PENDING_FILE, pendingLogin);
140
+ const isFresh = pending !== undefined &&
141
+ pending.dashboard === dashboard() &&
142
+ Date.now() - pending.startedAt < LOGIN_MINUTES * 60_000;
143
+ if (pending !== undefined && isFresh) {
144
+ io.say(`Waiting for Allow on ${pending.url} (code ${pending.code})`);
145
+ const cookie = await waitForApproval(pending.secret);
146
+ if (cookie !== undefined) {
147
+ saveCookie(cookie);
148
+ rmSync(PENDING_FILE, { force: true });
149
+ return { code: 0, data: { signedIn: true }, text: '✓ Signed in' };
150
+ }
151
+ return {
152
+ code: 3,
153
+ data: { waitingFor: 'approval', code: pending.code, url: pending.url },
154
+ text: `✗ Not allowed yet. The browser page ${pending.url} shows the code ${pending.code}.\n next once they click Allow there, run ${again} again`
155
+ };
156
+ }
157
+ const response = await fetch(`${dashboard()}/api/auth/cli/logins`, {
158
+ method: 'POST',
159
+ headers: { 'content-type': 'application/json' },
160
+ body: JSON.stringify({ label: labelOfThisMachine() })
161
+ });
162
+ const started = loginStarted.parse(await response.json());
163
+ writePrivate(PENDING_FILE, {
164
+ dashboard: dashboard(),
165
+ ...started,
166
+ startedAt: Date.now()
167
+ });
168
+ io.openUrl(started.url);
169
+ return {
170
+ code: 3,
171
+ data: { waitingFor: 'approval', code: started.code, url: started.url },
172
+ text: `Opened ${started.url} in the browser.\n code ${started.code}\n next tell them: "Check the code on the page is ${started.code}, then click Allow." Then run ${again} again.`
173
+ };
174
+ }
175
+ async function signedIn(io, again, work) {
176
+ if (savedCookie() === undefined) {
177
+ const outcome = await signIn(io, again);
178
+ if (outcome.code !== 0)
179
+ return outcome;
180
+ }
181
+ try {
182
+ return await work();
183
+ }
184
+ catch (error) {
185
+ if (!(error instanceof SignedOut))
186
+ throw error;
187
+ const outcome = await signIn(io, again);
188
+ if (outcome.code !== 0)
189
+ return outcome;
190
+ return work();
191
+ }
192
+ }
193
+ function isOutcome(value) {
194
+ return typeof value === 'object' && value !== null && 'text' in value;
195
+ }
196
+ function failure(error, data = {}) {
197
+ const message = error instanceof TypeError && error.message === 'fetch failed'
198
+ ? `Couldn’t reach ${dashboard()}. Check the internet connection and try again.`
199
+ : error instanceof Error
200
+ ? error.message
201
+ : String(error);
202
+ return { code: 1, data: { ...data, error: message }, text: `✗ ${message}` };
203
+ }
204
+ export async function login(io) {
205
+ try {
206
+ if (savedCookie() !== undefined) {
207
+ const me = await call('/api/me').catch(() => undefined);
208
+ if (me?.ok === true) {
209
+ return {
210
+ code: 0,
211
+ data: { signedIn: true },
212
+ text: '✓ Already signed in'
213
+ };
214
+ }
215
+ }
216
+ return await signIn(io, 'app login');
217
+ }
218
+ catch (error) {
219
+ return failure(error);
220
+ }
221
+ }
222
+ function isEmptyFolder(path) {
223
+ return readdirSync(path).every((entry) => IGNORED_IN_EMPTY_FOLDER.has(entry));
224
+ }
225
+ function commandLineToolsReady() {
226
+ return spawnSync('xcode-select', ['-p'], { stdio: 'ignore' }).status === 0;
227
+ }
228
+ export async function pull(io, name, cwd) {
229
+ if (name === undefined || nameProblem(name) !== undefined) {
230
+ return {
231
+ code: 2,
232
+ data: { error: 'missing name' },
233
+ text: '✗ Which site? app pull sundayparty'
234
+ };
235
+ }
236
+ const target = isEmptyFolder(cwd) ? cwd : join(cwd, name);
237
+ if (target !== cwd && existsSync(target) && !isEmptyFolder(target)) {
238
+ return {
239
+ code: 1,
240
+ data: { error: 'folder not empty', folder: target },
241
+ text: `✗ ${target} already has files in it. Pull into an empty folder.`
242
+ };
243
+ }
244
+ try {
245
+ const result = await signedIn(io, `app pull ${name}`, async () => {
246
+ const app = await callJson(`/api/apps/${name}`, appSummary);
247
+ const response = await call(`/api/apps/${name}/source`);
248
+ if (!response.ok)
249
+ throw new Error(`Couldn’t download ${name}`);
250
+ const number = response.headers.get('x-pattern-publish') ?? '?';
251
+ const live = await fetch(`${app.url}/api/app`)
252
+ .then((reply) => reply.json())
253
+ .then((body) => appInfo.parse(body).title)
254
+ .catch(() => '');
255
+ const title = live === '' ? app.title : live;
256
+ return { app, title, number, form: await response.formData() };
257
+ });
258
+ if (isOutcome(result))
259
+ return result;
260
+ let files = 0;
261
+ for (const [key, value] of result.form.entries()) {
262
+ if (!key.startsWith(UPLOAD.source) || typeof value === 'string')
263
+ continue;
264
+ const path = key.slice(UPLOAD.source.length);
265
+ if (sourcePathProblem(path) !== undefined)
266
+ continue;
267
+ const destination = join(target, path);
268
+ mkdirSync(dirname(destination), { recursive: true });
269
+ writeFileSync(destination, Buffer.from(await value.arrayBuffer()));
270
+ files += 1;
271
+ }
272
+ writeFileSync(join(target, PROJECT_FILE), `${JSON.stringify({ name, title: result.title }, null, 2)}\n`);
273
+ io.say(`✓ Downloaded ${files} files of version ${result.number}`);
274
+ const install = await io.run('pnpm', ['install'], target);
275
+ if (install.code !== 0) {
276
+ return failure(new Error(`pnpm install failed\n${install.output}`));
277
+ }
278
+ io.say('✓ Installed');
279
+ const setup = await io.run(join(target, 'app'), ['setup'], target);
280
+ if (setup.code !== 0)
281
+ io.say(setup.output.trimEnd());
282
+ if (commandLineToolsReady() && !existsSync(join(target, '.git'))) {
283
+ await io.run('git', ['init', '-q'], target);
284
+ await io.run('git', ['add', '-A'], target);
285
+ await io.run('git', ['commit', '-q', '-m', `Version ${result.number} from onpattern.app`], target);
286
+ }
287
+ const moved = target === cwd ? '' : `\n folder ${target} (open this folder in Claude)`;
288
+ return {
289
+ code: 0,
290
+ data: { name, folder: target, version: result.number, files },
291
+ text: `✓ ${result.title} is on this computer${moved}\n next read AGENTS.md, then app start`
292
+ };
293
+ }
294
+ catch (error) {
295
+ return failure(error);
296
+ }
297
+ }
298
+ function filesUnder(root) {
299
+ if (!existsSync(root))
300
+ return [];
301
+ return readdirSync(root, { recursive: true, withFileTypes: true })
302
+ .filter((entry) => entry.isFile())
303
+ .map((entry) => relative(root, join(entry.parentPath, entry.name)));
304
+ }
305
+ function ignoredPatterns(root) {
306
+ const path = join(root, '.gitignore');
307
+ if (!existsSync(path))
308
+ return [];
309
+ return readFileSync(path, 'utf8')
310
+ .split('\n')
311
+ .map((line) => line.trim())
312
+ .filter((line) => line !== '' && !line.startsWith('#'))
313
+ .map((line) => line.replace(/^\//, '').replace(/\/$/, ''));
314
+ }
315
+ function sourceFiles(root) {
316
+ const git = existsSync(join(root, '.git')) || commandLineToolsReady()
317
+ ? spawnSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], { cwd: root, encoding: 'utf8' })
318
+ : undefined;
319
+ const listed = git !== undefined && git.status === 0
320
+ ? git.stdout.split('\0').filter((path) => path !== '')
321
+ : filesUnder(root).filter((path) => {
322
+ const patterns = ignoredPatterns(root);
323
+ return !patterns.some((pattern) => matchesGlob(path, pattern) ||
324
+ path.split('/').some((part) => matchesGlob(part, pattern)));
325
+ });
326
+ return listed.filter((path) => sourcePathProblem(path) === undefined &&
327
+ existsSync(join(root, path)) &&
328
+ statSync(join(root, path)).isFile());
329
+ }
330
+ function fileBlob(path, contents) {
331
+ return new Blob([contents ?? readFileSync(path)], {
332
+ type: MIME_TYPES[extname(path).toLowerCase()] ?? 'application/octet-stream'
333
+ });
334
+ }
335
+ function workerDirectory(dist) {
336
+ return readdirSync(dist, { withFileTypes: true })
337
+ .filter((entry) => entry.isDirectory() && entry.name !== 'client')
338
+ .map((entry) => join(dist, entry.name))
339
+ .filter((path) => existsSync(join(path, 'wrangler.json')))
340
+ .toSorted((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
341
+ }
342
+ function starterPackageJson(path, sdkVersion) {
343
+ return readFileSync(path, 'utf8').replace(/"@trypattern\/sdk": "workspace:[^"]*"/, `"@trypattern/sdk": "${sdkVersion}"`);
344
+ }
345
+ function uploadForm(root, io, isStarter) {
346
+ const dist = join(root, 'dist');
347
+ const worker = workerDirectory(dist);
348
+ if (worker === undefined)
349
+ throw new Error('The build left no worker in dist/');
350
+ const wrangler = JSON.parse(readFileSync(join(worker, 'wrangler.json'), 'utf8'));
351
+ const form = new FormData();
352
+ form.append(UPLOAD.compatibilityDate, String(Reflect.get(Object(wrangler), 'compatibility_date') ?? ''));
353
+ form.append(UPLOAD.sdkVersion, io.sdkVersion);
354
+ for (const file of readdirSync(worker)) {
355
+ if (!file.endsWith('.js') || isPrivatePath(file))
356
+ continue;
357
+ form.append(`${UPLOAD.worker}${file}`, fileBlob(join(worker, file)), file);
358
+ }
359
+ for (const path of filesUnder(join(dist, 'client'))) {
360
+ if (basename(path) === '.assetsignore' || isPrivatePath(path))
361
+ continue;
362
+ form.append(`${UPLOAD.client}${path}`, fileBlob(join(dist, 'client', path)), basename(path));
363
+ }
364
+ const migrations = join(root, 'server', 'migrations');
365
+ for (const file of existsSync(migrations) ? readdirSync(migrations) : []) {
366
+ if (!file.endsWith('.sql'))
367
+ continue;
368
+ form.append(`${UPLOAD.migrations}${file}`, fileBlob(join(migrations, file)), file);
369
+ }
370
+ const sources = sourceFiles(root);
371
+ for (const path of sources) {
372
+ const full = join(root, path);
373
+ const contents = isStarter && path === 'package.json'
374
+ ? starterPackageJson(full, io.sdkVersion)
375
+ : undefined;
376
+ form.append(`${UPLOAD.source}${path}`, fileBlob(full, contents), basename(path));
377
+ }
378
+ return { form, sources: sources.length };
379
+ }
380
+ async function followPublish(io, name, started) {
381
+ let publish = started;
382
+ let shown;
383
+ const deadline = Date.now() + PUBLISH_WAIT_MS;
384
+ while (publish.status === 'running' && Date.now() < deadline) {
385
+ if (publish.step !== shown) {
386
+ io.say(` ${STEP_WORDS[publish.step]}…`);
387
+ shown = publish.step;
388
+ }
389
+ await sleep(POLL_MS);
390
+ publish = await callJson(`/api/apps/${name}/publishes/${publish.number}`, publishSummary);
391
+ }
392
+ const data = { name, version: publish.number, status: publish.status };
393
+ if (publish.status === 'live') {
394
+ return {
395
+ code: 0,
396
+ data,
397
+ text: `✓ Version ${publish.number} is live at https://${name}.onpattern.app`
398
+ };
399
+ }
400
+ if (publish.status === 'failed') {
401
+ return {
402
+ code: 1,
403
+ data: { ...data, error: publish.error },
404
+ text: `✗ Version ${publish.number} didn’t go live: ${publish.error ?? 'no details'}\n The site still runs the previous version.`
405
+ };
406
+ }
407
+ return {
408
+ code: 1,
409
+ data,
410
+ text: `✗ Version ${publish.number} is still publishing after ${PUBLISH_WAIT_MS / 60_000} minutes\n next app versions shows how it ends`
411
+ };
412
+ }
413
+ export async function publish(io, root, isStarter) {
414
+ try {
415
+ const { name } = readProjectFile(root);
416
+ const built = await io.build();
417
+ if (built.code !== 0)
418
+ return built;
419
+ io.say('✓ Built');
420
+ const { form, sources } = uploadForm(root, io, isStarter);
421
+ io.say(`✓ Packed the build and ${sources} source files`);
422
+ const again = isStarter ? 'app publish --starter' : 'app publish';
423
+ const result = await signedIn(io, again, async () => {
424
+ if (isStarter) {
425
+ await callJson('/api/starters', z.object({ key: z.string() }), {
426
+ method: 'POST',
427
+ body: form
428
+ });
429
+ return undefined;
430
+ }
431
+ return callJson(`/api/apps/${name}/publishes`, publishSummary, {
432
+ method: 'POST',
433
+ body: form
434
+ });
435
+ });
436
+ if (isOutcome(result))
437
+ return result;
438
+ if (result === undefined) {
439
+ return {
440
+ code: 0,
441
+ data: { starter: io.sdkVersion },
442
+ text: `✓ Published the starter for SDK ${io.sdkVersion}`
443
+ };
444
+ }
445
+ return await followPublish(io, name, result);
446
+ }
447
+ catch (error) {
448
+ return failure(error);
449
+ }
450
+ }
451
+ export async function versions(io, root) {
452
+ try {
453
+ const { name } = readProjectFile(root);
454
+ const result = await signedIn(io, 'app versions', () => callJson(`/api/apps/${name}/publishes`, z.array(publishSummary)));
455
+ if (isOutcome(result))
456
+ return result;
457
+ const lines = result.map((item) => {
458
+ const what = item.restoredFrom !== null
459
+ ? `restored version ${item.restoredFrom}`
460
+ : item.isStarter
461
+ ? 'starter site'
462
+ : `published by ${item.byEmail}`;
463
+ return ` ${String(item.number).padStart(3)} ${item.status.padEnd(7)} ${item.startedAt.slice(0, 16).replace('T', ' ')} ${what}`;
464
+ });
465
+ return {
466
+ code: 0,
467
+ data: { name, versions: result },
468
+ text: [`Versions of ${name} (newest first)`, ...lines].join('\n')
469
+ };
470
+ }
471
+ catch (error) {
472
+ return failure(error);
473
+ }
474
+ }
475
+ export async function restore(io, root, number) {
476
+ if (number === undefined || !/^\d+$/.test(number)) {
477
+ return {
478
+ code: 2,
479
+ data: { error: 'missing version' },
480
+ text: '✗ Which version? app restore 3 (app versions lists them)'
481
+ };
482
+ }
483
+ try {
484
+ const { name } = readProjectFile(root);
485
+ const result = await signedIn(io, `app restore ${number}`, () => callJson(`/api/apps/${name}/publishes/${number}/restore`, publishSummary, { method: 'POST' }));
486
+ if (isOutcome(result))
487
+ return result;
488
+ return await followPublish(io, name, result);
489
+ }
490
+ catch (error) {
491
+ return failure(error);
492
+ }
493
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,96 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { launchChrome, openApp, viewportNames, viewports } from "../flows/browser.js";
4
+ const ROOT = process.cwd();
5
+ const SCREENSHOTS_DIRECTORY = join(ROOT, '.dev', 'screenshots');
6
+ function readRequest() {
7
+ const parsed = JSON.parse(process.argv[2] ?? '');
8
+ if (typeof parsed !== 'object' ||
9
+ parsed === null ||
10
+ !('url' in parsed) ||
11
+ !('path' in parsed) ||
12
+ !('name' in parsed) ||
13
+ !('commands' in parsed) ||
14
+ !('signedInAs' in parsed) ||
15
+ (typeof parsed.signedInAs !== 'string' && parsed.signedInAs !== null) ||
16
+ typeof parsed.url !== 'string' ||
17
+ typeof parsed.path !== 'string' ||
18
+ typeof parsed.name !== 'string' ||
19
+ !Array.isArray(parsed.commands)) {
20
+ throw new Error('screenshot.ts expects {url, path, name, commands, signedInAs} JSON');
21
+ }
22
+ return {
23
+ url: parsed.url,
24
+ path: parsed.path,
25
+ name: parsed.name,
26
+ commands: parsed.commands,
27
+ signedInAs: parsed.signedInAs
28
+ };
29
+ }
30
+ async function captureAll(browser, request) {
31
+ const [first = 'desktop'] = viewportNames;
32
+ const app = await openApp(browser, request.url, request.path, first, request.signedInAs);
33
+ try {
34
+ let snapshot = await app.snapshot();
35
+ let commandError;
36
+ for (const command of request.commands) {
37
+ try {
38
+ snapshot = await app.send(command);
39
+ }
40
+ catch (error) {
41
+ commandError = `command ${JSON.stringify(command)} failed: ${error instanceof Error ? error.message : String(error)}`;
42
+ break;
43
+ }
44
+ }
45
+ const shots = [];
46
+ for (const viewport of viewportNames) {
47
+ const seenBefore = app.problems.length;
48
+ const file = join(SCREENSHOTS_DIRECTORY, `${request.name}-${viewport}.png`);
49
+ writeFileSync(file, Buffer.from(await app.captureAt(viewport), 'base64'));
50
+ const problems = [
51
+ ...(commandError === undefined ? [] : [commandError]),
52
+ ...(viewport === first ? app.problems : app.problems.slice(seenBefore))
53
+ ];
54
+ shots.push({ viewport, ...viewports[viewport], file, snapshot, problems });
55
+ }
56
+ return shots;
57
+ }
58
+ finally {
59
+ await app.close();
60
+ }
61
+ }
62
+ async function main() {
63
+ try {
64
+ const request = readRequest();
65
+ mkdirSync(SCREENSHOTS_DIRECTORY, { recursive: true });
66
+ const browser = await launchChrome();
67
+ const shots = await captureAll(browser, request).finally(() => browser.close());
68
+ const viewportsByProblem = new Map();
69
+ for (const shot of shots) {
70
+ for (const problem of shot.problems) {
71
+ viewportsByProblem.set(problem, [
72
+ ...(viewportsByProblem.get(problem) ?? []),
73
+ shot.viewport
74
+ ]);
75
+ }
76
+ }
77
+ const result = {
78
+ screenshots: shots.map(({ viewport, width, height, file }) => ({
79
+ viewport,
80
+ width,
81
+ height,
82
+ file
83
+ })),
84
+ snapshot: shots[0]?.snapshot ?? null,
85
+ problems: [...viewportsByProblem].map(([message, seenIn]) => ({
86
+ message,
87
+ viewports: seenIn
88
+ }))
89
+ };
90
+ process.stdout.write(`${JSON.stringify(result)}\n`);
91
+ }
92
+ catch (error) {
93
+ process.stdout.write(`${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n`);
94
+ }
95
+ }
96
+ await main();
@@ -0,0 +1,12 @@
1
+ declare const TOOLS: {
2
+ readonly tsc: readonly ["typescript", "tsc"];
3
+ readonly oxlint: readonly ["oxlint", "oxlint"];
4
+ readonly prettier: readonly ["prettier", "prettier"];
5
+ readonly vite: readonly ["vite", "vite"];
6
+ readonly vitest: readonly ["vitest", "vitest"];
7
+ readonly wrangler: readonly ["wrangler", "wrangler"];
8
+ readonly 'drizzle-kit': readonly ["drizzle-kit", "drizzle-kit"];
9
+ };
10
+ export type Tool = keyof typeof TOOLS;
11
+ export declare function toolBin(tool: Tool): string;
12
+ export {};
@@ -0,0 +1,35 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, join } from 'node:path';
4
+ const require = createRequire(import.meta.url);
5
+ const TOOLS = {
6
+ tsc: ['typescript', 'tsc'],
7
+ oxlint: ['oxlint', 'oxlint'],
8
+ prettier: ['prettier', 'prettier'],
9
+ vite: ['vite', 'vite'],
10
+ vitest: ['vitest', 'vitest'],
11
+ wrangler: ['wrangler', 'wrangler'],
12
+ 'drizzle-kit': ['drizzle-kit', 'drizzle-kit']
13
+ };
14
+ export function toolBin(tool) {
15
+ const [name, bin] = TOOLS[tool];
16
+ const manifest = (require.resolve.paths(name) ?? [])
17
+ .map((directory) => join(directory, name, 'package.json'))
18
+ .find((path) => existsSync(path));
19
+ if (manifest === undefined) {
20
+ throw new Error(`${name} is not installed`);
21
+ }
22
+ const parsed = JSON.parse(readFileSync(manifest, 'utf8'));
23
+ const bins = typeof parsed === 'object' && parsed !== null && 'bin' in parsed
24
+ ? parsed.bin
25
+ : undefined;
26
+ const path = typeof bins === 'string'
27
+ ? bins
28
+ : typeof bins === 'object' && bins !== null
29
+ ? Reflect.get(bins, bin)
30
+ : undefined;
31
+ if (typeof path !== 'string') {
32
+ throw new Error(`${name} has no ${bin} command`);
33
+ }
34
+ return join(dirname(manifest), path);
35
+ }
@@ -0,0 +1,6 @@
1
+ export type Clock = {
2
+ every: (ms: number, run: () => void) => () => void;
3
+ tick: () => void;
4
+ };
5
+ export declare function realClock(): Clock;
6
+ export declare function manualClock(): Clock;