lecodes-cli 0.20.2 → 2.0.0

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 (154) hide show
  1. package/README.md +56 -57
  2. package/dist/index.js +11052 -7592
  3. package/package.json +14 -8
  4. package/runtime/materials/decal-relief.mat +12 -1
  5. package/runtime/materials/decal.mat +17 -3
  6. package/runtime/materials/lightmap.mat +117 -138
  7. package/runtime/materials/particles-quad.mat +6 -2
  8. package/runtime/materials/terrain-lightmap.mat +123 -0
  9. package/runtime/materials/terrain.mat +36 -55
  10. package/runtime/sdk-types.json +1 -1
  11. package/runtime/web/VERSION.json +5 -0
  12. package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
  13. package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
  14. package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
  15. package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
  16. package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
  17. package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
  18. package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
  19. package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
  20. package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
  21. package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
  22. package/runtime/web/assets/map-CyBxOSV0.js +1 -0
  23. package/runtime/web/assets/map-XzoOFt0E.js +2 -0
  24. package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
  25. package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
  26. package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
  27. package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
  28. package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
  29. package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
  30. package/runtime/web/index.html +36 -0
  31. package/runtime/web/player.js +21 -0
  32. package/runtime/web-page/index.html +41 -0
  33. package/runtime/web-page/page.css +1 -0
  34. package/runtime/web-page/page.js +31 -0
  35. package/src/api.ts +1 -302
  36. package/src/cli/args.ts +37 -0
  37. package/src/cli/command.ts +113 -0
  38. package/src/cli/errors.ts +13 -0
  39. package/src/cli/help.ts +158 -0
  40. package/src/cli/index.ts +7 -0
  41. package/src/cli/output.ts +102 -0
  42. package/src/cli/run.ts +106 -0
  43. package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
  44. package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
  45. package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
  46. package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
  47. package/src/commands/{app.ts → app/index.ts} +400 -290
  48. package/src/commands/{appShared.ts → app/shared.ts} +43 -44
  49. package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
  50. package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
  51. package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
  52. package/src/commands/assets.ts +23 -21
  53. package/src/commands/clone.ts +48 -51
  54. package/src/commands/compile.ts +76 -89
  55. package/src/commands/create.ts +46 -47
  56. package/src/commands/design/comments.ts +361 -0
  57. package/src/commands/design/context.ts +101 -0
  58. package/src/commands/design/index.ts +391 -0
  59. package/src/commands/design/snapshot.ts +134 -0
  60. package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
  61. package/src/commands/desktop.ts +94 -82
  62. package/src/commands/dev.ts +260 -182
  63. package/src/commands/diff.ts +39 -51
  64. package/src/commands/index.ts +64 -0
  65. package/src/commands/{init.ts → init/index.ts} +195 -191
  66. package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
  67. package/src/commands/install.ts +80 -67
  68. package/src/commands/lightmap.ts +543 -252
  69. package/src/commands/link.ts +87 -94
  70. package/src/commands/login.ts +25 -23
  71. package/src/commands/navmesh.ts +169 -139
  72. package/src/commands/plugin.ts +46 -0
  73. package/src/commands/pn.ts +201 -244
  74. package/src/commands/pull.ts +49 -56
  75. package/src/commands/push.ts +48 -54
  76. package/src/commands/render.ts +176 -111
  77. package/src/commands/scene.ts +63 -55
  78. package/src/commands/serve.ts +58 -0
  79. package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
  80. package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
  81. package/src/commands/shared.ts +79 -0
  82. package/src/commands/status.ts +22 -26
  83. package/src/commands/test.ts +94 -63
  84. package/src/commands/thumbs.ts +185 -175
  85. package/src/commands/update/index.ts +226 -0
  86. package/src/commands/{types.ts → update/types.ts} +31 -22
  87. package/src/compile/collect.ts +5 -5
  88. package/src/compile/collectLocal.ts +3 -3
  89. package/src/compile/designCompile.ts +4 -4
  90. package/src/compile/headlessBundle.ts +16 -12
  91. package/src/compile/nativeStack.ts +1 -1
  92. package/src/compile/projectCompile.ts +12 -4
  93. package/src/compile/sceneCompile.ts +14 -26
  94. package/src/compile/screenEntry.ts +123 -127
  95. package/src/compile/shaders.ts +3 -3
  96. package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
  97. package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
  98. package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
  99. package/src/dev/androidDev.ts +1 -1
  100. package/src/dev/clientTemplates.ts +18 -14
  101. package/src/dev/devServer.ts +78 -41
  102. package/src/dev/iosDev.ts +180 -0
  103. package/src/dev/webRunner.ts +83 -15
  104. package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
  105. package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
  106. package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
  107. package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
  108. package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
  109. package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
  110. package/src/{peers.ts → hosts/peers.ts} +2 -2
  111. package/src/{releases.ts → hosts/releases.ts} +1 -1
  112. package/src/index.ts +35 -480
  113. package/src/platform/api.ts +302 -0
  114. package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
  115. package/src/{config.ts → platform/config.ts} +1 -1
  116. package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
  117. package/src/plugin/contract.ts +438 -0
  118. package/src/plugin/emitKotlin.ts +357 -0
  119. package/src/plugin/emitSdk.ts +259 -0
  120. package/src/plugin/emitSwift.ts +336 -0
  121. package/src/plugin/emitWeb.ts +383 -0
  122. package/src/plugin/generate.ts +157 -0
  123. package/src/plugin/lines.ts +28 -0
  124. package/src/plugin/lower.ts +231 -0
  125. package/src/plugin/manifest.ts +192 -0
  126. package/src/{projectEnv.ts → project/env.ts} +33 -2
  127. package/src/{ignore.ts → project/ignore.ts} +164 -163
  128. package/src/{localFiles.ts → project/localFiles.ts} +1 -1
  129. package/src/{manifest.ts → project/manifest.ts} +1 -1
  130. package/src/{project.ts → project/materialize.ts} +2 -2
  131. package/src/project/paths.ts +20 -0
  132. package/src/{textDiff.ts → project/textDiff.ts} +1 -1
  133. package/src/{types.ts → project/types.ts} +0 -0
  134. package/runtime/materials/lightmap-baked-lite.mat +0 -175
  135. package/runtime/materials/lightmap-baked.mat +0 -176
  136. package/runtime/scene-harness.json +0 -1
  137. package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
  138. package/runtime/web/assets/basis-C64VHDVD.js +0 -1
  139. package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  140. package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
  141. package/runtime/web/assets/draco-BiISTFcR.js +0 -118
  142. package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  143. package/runtime/web/assets/index-BMt7AnC5.js +0 -2
  144. package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
  145. package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
  146. package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
  147. package/runtime/web/embed.html +0 -32
  148. package/runtime/web/embed.js +0 -171
  149. package/src/commands/design.ts +0 -846
  150. package/src/commands/update.ts +0 -211
  151. package/src/util.ts +0 -146
  152. /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
  153. /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
  154. /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
@@ -0,0 +1,302 @@
1
+ import { CliError } from "../cli"
2
+
3
+ /* Thin client over the backend HTTP API. All project/account calls go through `${apiUrl}/api/...`;
4
+ * blob downloads hit `${apiUrl}<src>` (the public /uploads/* route). Mirrors the contract the
5
+ * frontend uses (see packages/frontend/src/api). */
6
+
7
+ export type ProjectAsset = {
8
+ id: number
9
+ name: string
10
+ type: "folder" | "file" | "text" | "shader" | "scene"
11
+ parentId: number | null
12
+ meta?: Record<string, unknown> | null
13
+ text?: string | null
14
+ file?: { src: string, previewSrc?: string | null, size?: number } | null
15
+ shaders?: { platform: string, src: string }[]
16
+ }
17
+
18
+ export type Project = {
19
+ uuid: string
20
+ name: string
21
+ role: "owner" | "developer" | "viewer"
22
+ assets: ProjectAsset[]
23
+ }
24
+
25
+ export type CommitsResponse = { dirty: boolean, headId: number | null }
26
+
27
+ export type SyncFileInput = {
28
+ path: string
29
+ assetId?: number
30
+ type: "text" | "shader" | "scene" | "resource"
31
+ text?: string | null
32
+ blobFileId?: number | null
33
+ }
34
+ export type SyncFolderInput = { path: string }
35
+ export type SyncResponse = {
36
+ created: boolean
37
+ headId: number
38
+ seq?: number
39
+ nodes: { id: number, path: string, type: string, versionId: number | null }[]
40
+ /** Set when the push requested `publish` — whether the deploy (compile → live) succeeded. */
41
+ published?: boolean
42
+ /** A human-readable reason when `publish` was requested but the deploy failed (push still landed). */
43
+ publishError?: string
44
+ }
45
+
46
+ const errorMessage = (body: unknown, status: number): string => {
47
+ if (typeof body === "string" && body) return body
48
+ if (body && typeof body === "object") {
49
+ const b = body as Record<string, unknown>
50
+ if (typeof b.error === "string") return b.error
51
+ if (b.error && typeof b.error === "object") {
52
+ const first = Object.values(b.error)[0] as unknown
53
+ if (typeof first === "string") return first
54
+ if (first && typeof first === "object" && typeof (first as { message?: unknown }).message === "string") {
55
+ return (first as { message: string }).message
56
+ }
57
+ }
58
+ if (typeof b.message === "string") return b.message
59
+ }
60
+ return `Request failed with status ${status}`
61
+ }
62
+
63
+ type RequestOptions = { method?: string, body?: unknown, token?: string }
64
+
65
+ export const apiRequest = async <T = unknown>(apiUrl: string, path: string, opts: RequestOptions = {}): Promise<T> => {
66
+ const headers: Record<string, string> = {}
67
+ const isForm = opts.body instanceof FormData
68
+ if (opts.token) headers["Authorization"] = "Bearer " + opts.token
69
+ if (opts.body !== undefined && !isForm) headers["Content-Type"] = "application/json"
70
+
71
+ let resp: Response
72
+ try {
73
+ resp = await fetch(`${apiUrl}/api${path}`, {
74
+ method: opts.method ?? (opts.body !== undefined ? "POST" : "GET"),
75
+ headers,
76
+ body: opts.body === undefined ? undefined : isForm ? (opts.body as FormData) : JSON.stringify(opts.body),
77
+ })
78
+ } catch (e) {
79
+ throw new CliError(`Cannot reach ${apiUrl} — is the server URL correct and online?`)
80
+ }
81
+
82
+ const contentType = resp.headers.get("content-type") ?? ""
83
+ const parsed: unknown = contentType.startsWith("application/json") ? await resp.json()
84
+ : contentType.startsWith("text/") ? await resp.text()
85
+ : null
86
+
87
+ if (resp.status >= 400) throw new CliError(errorMessage(parsed, resp.status), resp.status)
88
+ return parsed as T
89
+ }
90
+
91
+ export const login = (apiUrl: string, login: string, password: string) =>
92
+ apiRequest<{ accessToken: string, refreshToken: string }>(apiUrl, "/account/login", { body: { login, password } })
93
+
94
+ export const createAccessToken = (apiUrl: string, jwt: string, name: string) =>
95
+ apiRequest<{ id: number, name: string, prefix: string, token: string }>(apiUrl, "/account/tokens", { token: jwt, body: { name } })
96
+
97
+ export const getAccount = (apiUrl: string, token: string) =>
98
+ apiRequest<{ id: number, name: string, email: string }>(apiUrl, "/account", { token })
99
+
100
+ export const getProject = (apiUrl: string, token: string, uuid: string) =>
101
+ apiRequest<Project>(apiUrl, `/projects/${uuid}`, { token })
102
+
103
+ /** Create a new (empty) project on the server. Returns its uuid. */
104
+ export const createProject = (apiUrl: string, token: string, name: string, teamUuid?: string) =>
105
+ apiRequest<{ uuid: string }>(apiUrl, "/projects", { token, body: { name, teamUuid } })
106
+
107
+ export const getCommits = (apiUrl: string, token: string, uuid: string) =>
108
+ apiRequest<CommitsResponse>(apiUrl, `/projects/${uuid}/commits`, { token })
109
+
110
+ export const getTypes = (apiUrl: string) =>
111
+ apiRequest<Record<string, string>>(apiUrl, "/types")
112
+
113
+ export const uploadBlob = (apiUrl: string, token: string, uuid: string, name: string, bytes: Uint8Array) => {
114
+ const form = new FormData()
115
+ // Cast is type-only (TS 5.7 typed-array generics vs the Blob signature); Blob accepts a Uint8Array.
116
+ form.append("file", new Blob([bytes as unknown as ArrayBuffer]), name)
117
+ return apiRequest<{ id: number, src: string, size: number }>(apiUrl, `/projects/${uuid}/blobs`, { token, body: form })
118
+ }
119
+
120
+ export const sync = (apiUrl: string, token: string, uuid: string, payload: { message: string, files: SyncFileInput[], folders: SyncFolderInput[], publish?: boolean }) =>
121
+ apiRequest<SyncResponse>(apiUrl, `/projects/${uuid}/sync`, { token, body: payload })
122
+
123
+ /** Deploy the project's current server state (compile → live bundle). Used by `push --publish` when
124
+ * there are no file changes to sync. */
125
+ export const publishProject = (apiUrl: string, token: string, uuid: string) =>
126
+ apiRequest<{ published: boolean, size: number }>(apiUrl, `/projects/${uuid}/publish`, { token, body: {} })
127
+
128
+ export type DesignShare = {
129
+ token: string | null
130
+ url: string | null
131
+ /** Whether share-link visitors may WRITE comments (reading is never gated on it). */
132
+ commentsOpen?: boolean
133
+ }
134
+
135
+ /** Current public share-link state for a project's design board. */
136
+ export const getDesignShare = (apiUrl: string, token: string, uuid: string) =>
137
+ apiRequest<DesignShare>(apiUrl, `/projects/${uuid}/design/share`, { token })
138
+
139
+ /** Enable the design share link (idempotent). */
140
+ export const enableDesignShare = (apiUrl: string, token: string, uuid: string) =>
141
+ apiRequest<DesignShare>(apiUrl, `/projects/${uuid}/design/share`, { token, body: {} })
142
+
143
+ /** Revoke the design share link. */
144
+ export const disableDesignShare = (apiUrl: string, token: string, uuid: string) =>
145
+ apiRequest<DesignShare>(apiUrl, `/projects/${uuid}/design/share`, { token, method: "DELETE" })
146
+
147
+ /** Let / stop share-link visitors WRITING comments. Reading the existing conversation is unaffected,
148
+ * so switching this off never hides feedback that already arrived. */
149
+ export const setDesignCommentsOpen = (apiUrl: string, token: string, uuid: string, open: boolean) =>
150
+ apiRequest<{ commentsOpen: boolean }>(apiUrl, `/projects/${uuid}/design/comments/open`, { token, body: { open } })
151
+
152
+ /* ---- Design-board comments (docs/design-comments-plan.md) ---------------------------------
153
+ * The platform is the source of truth; `design/comments/` (one file per thread) is the mirror the
154
+ * running `lecodes design` sync — or `comments pull` — fills. The shapes below are the same
155
+ * document the design package's core defines —
156
+ * spelled out here, like every other DTO in this file, because `lecodes-design` is an optional
157
+ * peer the CLI must typecheck and run without. */
158
+
159
+ export type CommentAnchor = {
160
+ screen?: string
161
+ state?: string
162
+ element?: string
163
+ point?: [number, number]
164
+ world?: [number, number]
165
+ }
166
+ export type CommentMessage = {
167
+ id: string
168
+ author: string
169
+ /** Provenance: `share` = unverified stranger on the link, `ai` = Claude on a member's behalf. */
170
+ origin: "share" | "editor" | "cli" | "ai"
171
+ at: string
172
+ body: string
173
+ editedAt?: string
174
+ mine?: boolean
175
+ }
176
+ export type CommentThread = {
177
+ id: string
178
+ number: number
179
+ anchor: CommentAnchor
180
+ resolved: boolean
181
+ resolvedAt?: string | null
182
+ createdAt: string
183
+ updatedAt: string
184
+ commitId?: number | null
185
+ messages: CommentMessage[]
186
+ mine?: boolean
187
+ }
188
+
189
+ export type DesignCommentsSnapshot = {
190
+ threads: CommentThread[]
191
+ cursor: string | null
192
+ canComment: boolean
193
+ shared?: boolean
194
+ commentsOpen?: boolean
195
+ /** The project's thread-number allocator, so a pull can't hand out a used number. */
196
+ nextNumber?: number
197
+ }
198
+
199
+ export const listDesignComments = (apiUrl: string, token: string, uuid: string) =>
200
+ apiRequest<DesignCommentsSnapshot>(apiUrl, `/projects/${uuid}/design/comments`, { token })
201
+
202
+ /** `origin` is provenance, carried up so a reply Claude wrote doesn't reach the reviewer looking
203
+ * like one the developer typed. The account is the identity either way; the server rejects any
204
+ * value but `cli` / `ai` / `editor`. */
205
+ export const createDesignComment = (
206
+ apiUrl: string, token: string, uuid: string,
207
+ input: { anchor: CommentAnchor, body: string, origin?: CommentMessage["origin"] },
208
+ ) => apiRequest<CommentThread>(apiUrl, `/projects/${uuid}/design/comments`, { token, body: input })
209
+
210
+ export const replyDesignComment = (
211
+ apiUrl: string, token: string, uuid: string, threadId: string, body: string,
212
+ origin?: CommentMessage["origin"],
213
+ ) => apiRequest<CommentThread>(
214
+ apiUrl, `/projects/${uuid}/design/comments/${threadId}/messages`, { token, body: { body, origin } })
215
+
216
+ export const resolveDesignComment = (apiUrl: string, token: string, uuid: string, threadId: string, resolved: boolean) =>
217
+ apiRequest<CommentThread>(apiUrl, `/projects/${uuid}/design/comments/${threadId}`, { token, method: "PATCH", body: { resolved } })
218
+
219
+ export const deleteDesignComment = (apiUrl: string, token: string, uuid: string, threadId: string) =>
220
+ apiRequest<{ ok: boolean }>(apiUrl, `/projects/${uuid}/design/comments/${threadId}`, { token, method: "DELETE" })
221
+
222
+ /** Re-anchor a pin. The anchor goes up whole — `element` is replaced, never merged, or a stale name
223
+ * silently re-anchors the pin to whatever else still answers to it. */
224
+ export const moveDesignComment = (
225
+ apiUrl: string, token: string, uuid: string, threadId: string, anchor: CommentAnchor,
226
+ ) => apiRequest<CommentThread>(apiUrl, `/projects/${uuid}/design/comments/${threadId}`, {
227
+ token,
228
+ method: "PATCH",
229
+ body: { screen: anchor.screen, state: anchor.state, element: anchor.element, point: anchor.point },
230
+ })
231
+
232
+ export const editDesignCommentMessage = (
233
+ apiUrl: string, token: string, uuid: string, threadId: string, messageId: string, body: string,
234
+ ) => apiRequest<CommentThread>(
235
+ apiUrl, `/projects/${uuid}/design/comments/${threadId}/messages/${messageId}`, { token, method: "PATCH", body: { body } })
236
+
237
+ export const deleteDesignCommentMessage = (
238
+ apiUrl: string, token: string, uuid: string, threadId: string, messageId: string,
239
+ ) => apiRequest<{ threadRemoved: boolean }>(
240
+ apiUrl, `/projects/${uuid}/design/comments/${threadId}/messages/${messageId}`, { token, method: "DELETE" })
241
+
242
+ /* ---- Push notifications (`lecodes pn`, docs/push-plan.md) ---------------------------------
243
+ * Every call here authenticates with the stored PAT: /push/send accepts a developer-role user
244
+ * token exactly like a project push key, so test sends need no key at all. Keys are minted for
245
+ * ONE purpose — the developer's own production server — and only an owner can manage them. */
246
+
247
+ export type PushSendPayload = {
248
+ to?: string[]
249
+ users?: string[]
250
+ title: string
251
+ body?: string
252
+ url?: string
253
+ data?: Record<string, unknown>
254
+ badge?: number
255
+ }
256
+ export type PushSendResult = { sent: number, failed: { address: string, reason: string }[] }
257
+
258
+ export type PushDevice = {
259
+ address: string
260
+ platform: "apns" | "fcm" | "webpush"
261
+ environment: "production" | "sandbox"
262
+ hostApp: string
263
+ user: string | null
264
+ createdAt: string
265
+ lastSeenAt: string
266
+ disabledAt: string | null
267
+ }
268
+
269
+ export type PushKey = {
270
+ id: number
271
+ name: string
272
+ prefix: string
273
+ createdAt: string
274
+ lastUsedAt: string | null
275
+ }
276
+ /** Only the mint response carries `key` — the plaintext exists in this one response and nowhere else. */
277
+ export type CreatedPushKey = PushKey & { key: string }
278
+
279
+ export const sendPush = (apiUrl: string, token: string, uuid: string, payload: PushSendPayload) =>
280
+ apiRequest<PushSendResult>(apiUrl, `/projects/${uuid}/push/send`, { token, body: payload })
281
+
282
+ export const listPushDevices = (apiUrl: string, token: string, uuid: string, user?: string) =>
283
+ apiRequest<{ devices: PushDevice[], truncated: boolean }>(
284
+ apiUrl, `/projects/${uuid}/push/registrations${user ? `?user=${encodeURIComponent(user)}` : ""}`, { token })
285
+
286
+ export const forgetPushDevice = (apiUrl: string, token: string, uuid: string, address: string) =>
287
+ apiRequest<void>(apiUrl, `/projects/${uuid}/push/registrations/${encodeURIComponent(address)}`, { token, method: "DELETE" })
288
+
289
+ export const listPushKeys = (apiUrl: string, token: string, uuid: string) =>
290
+ apiRequest<PushKey[]>(apiUrl, `/projects/${uuid}/push/keys`, { token })
291
+
292
+ export const createPushKey = (apiUrl: string, token: string, uuid: string, name?: string) =>
293
+ apiRequest<CreatedPushKey>(apiUrl, `/projects/${uuid}/push/keys`, { token, body: { name } })
294
+
295
+ export const revokePushKey = (apiUrl: string, token: string, uuid: string, id: number) =>
296
+ apiRequest<void>(apiUrl, `/projects/${uuid}/push/keys/${id}`, { token, method: "DELETE" })
297
+
298
+ export const downloadBlob = async (apiUrl: string, src: string): Promise<Uint8Array> => {
299
+ const resp = await fetch(`${apiUrl}${src}`)
300
+ if (!resp.ok) throw new CliError(`Failed to download ${src} (status ${resp.status})`)
301
+ return new Uint8Array(await resp.arrayBuffer())
302
+ }
@@ -3,7 +3,7 @@ import type { AddressInfo } from "node:net"
3
3
  import { spawn } from "node:child_process"
4
4
  import { randomBytes } from "node:crypto"
5
5
  import { hostname } from "node:os"
6
- import { CliError, c, info, log } from "./util"
6
+ import { CliError, c, info, log } from "../cli"
7
7
 
8
8
  /*
9
9
  * Browser login (loopback OAuth-style flow, like `vercel`/`gh`):
@@ -1,7 +1,7 @@
1
1
  import { homedir } from "node:os"
2
2
  import { join } from "node:path"
3
3
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
4
- import { CliError } from "./util"
4
+ import { CliError } from "../cli"
5
5
 
6
6
  /* Global CLI config in ~/.lecodes/config.json: the server URL + the personal access token.
7
7
  * Env vars LECODES_API / LECODES_TOKEN override the stored values (handy for CI). */
@@ -1,8 +1,8 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import type { Project, ProjectAsset } from "./api"
3
- import type { LocalScan } from "./localFiles"
4
- import { classifyKind, isTextKind } from "./localFiles"
5
- import { buildPaths } from "./project"
3
+ import type { LocalScan } from "../project/localFiles"
4
+ import { classifyKind, isTextKind } from "../project/localFiles"
5
+ import { buildPaths } from "../project/materialize"
6
6
  import { basename } from "node:path"
7
7
 
8
8
  /*