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.
- package/README.md +56 -57
- package/dist/index.js +11052 -7592
- package/package.json +14 -8
- package/runtime/materials/decal-relief.mat +12 -1
- package/runtime/materials/decal.mat +17 -3
- package/runtime/materials/lightmap.mat +117 -138
- package/runtime/materials/particles-quad.mat +6 -2
- package/runtime/materials/terrain-lightmap.mat +123 -0
- package/runtime/materials/terrain.mat +36 -55
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +5 -0
- package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
- package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
- package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
- package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
- package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
- package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
- package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
- package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
- package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
- package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
- package/runtime/web/assets/map-CyBxOSV0.js +1 -0
- package/runtime/web/assets/map-XzoOFt0E.js +2 -0
- package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
- package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
- package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
- package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
- package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
- package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
- package/runtime/web/index.html +36 -0
- package/runtime/web/player.js +21 -0
- package/runtime/web-page/index.html +41 -0
- package/runtime/web-page/page.css +1 -0
- package/runtime/web-page/page.js +31 -0
- package/src/api.ts +1 -302
- package/src/cli/args.ts +37 -0
- package/src/cli/command.ts +113 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/help.ts +158 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/output.ts +102 -0
- package/src/cli/run.ts +106 -0
- package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
- package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
- package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
- package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
- package/src/commands/{app.ts → app/index.ts} +400 -290
- package/src/commands/{appShared.ts → app/shared.ts} +43 -44
- package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
- package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
- package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
- package/src/commands/assets.ts +23 -21
- package/src/commands/clone.ts +48 -51
- package/src/commands/compile.ts +76 -89
- package/src/commands/create.ts +46 -47
- package/src/commands/design/comments.ts +361 -0
- package/src/commands/design/context.ts +101 -0
- package/src/commands/design/index.ts +391 -0
- package/src/commands/design/snapshot.ts +134 -0
- package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
- package/src/commands/desktop.ts +94 -82
- package/src/commands/dev.ts +260 -182
- package/src/commands/diff.ts +39 -51
- package/src/commands/index.ts +64 -0
- package/src/commands/{init.ts → init/index.ts} +195 -191
- package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
- package/src/commands/install.ts +80 -67
- package/src/commands/lightmap.ts +543 -252
- package/src/commands/link.ts +87 -94
- package/src/commands/login.ts +25 -23
- package/src/commands/navmesh.ts +169 -139
- package/src/commands/plugin.ts +46 -0
- package/src/commands/pn.ts +201 -244
- package/src/commands/pull.ts +49 -56
- package/src/commands/push.ts +48 -54
- package/src/commands/render.ts +176 -111
- package/src/commands/scene.ts +63 -55
- package/src/commands/serve.ts +58 -0
- package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
- package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
- package/src/commands/shared.ts +79 -0
- package/src/commands/status.ts +22 -26
- package/src/commands/test.ts +94 -63
- package/src/commands/thumbs.ts +185 -175
- package/src/commands/update/index.ts +226 -0
- package/src/commands/{types.ts → update/types.ts} +31 -22
- package/src/compile/collect.ts +5 -5
- package/src/compile/collectLocal.ts +3 -3
- package/src/compile/designCompile.ts +4 -4
- package/src/compile/headlessBundle.ts +16 -12
- package/src/compile/nativeStack.ts +1 -1
- package/src/compile/projectCompile.ts +12 -4
- package/src/compile/sceneCompile.ts +14 -26
- package/src/compile/screenEntry.ts +123 -127
- package/src/compile/shaders.ts +3 -3
- package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
- package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
- package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
- package/src/dev/androidDev.ts +1 -1
- package/src/dev/clientTemplates.ts +18 -14
- package/src/dev/devServer.ts +78 -41
- package/src/dev/iosDev.ts +180 -0
- package/src/dev/webRunner.ts +83 -15
- package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
- package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
- package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
- package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
- package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
- package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
- package/src/{peers.ts → hosts/peers.ts} +2 -2
- package/src/{releases.ts → hosts/releases.ts} +1 -1
- package/src/index.ts +35 -480
- package/src/platform/api.ts +302 -0
- package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
- package/src/{config.ts → platform/config.ts} +1 -1
- package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
- package/src/plugin/contract.ts +438 -0
- package/src/plugin/emitKotlin.ts +357 -0
- package/src/plugin/emitSdk.ts +259 -0
- package/src/plugin/emitSwift.ts +336 -0
- package/src/plugin/emitWeb.ts +383 -0
- package/src/plugin/generate.ts +157 -0
- package/src/plugin/lines.ts +28 -0
- package/src/plugin/lower.ts +231 -0
- package/src/plugin/manifest.ts +192 -0
- package/src/{projectEnv.ts → project/env.ts} +33 -2
- package/src/{ignore.ts → project/ignore.ts} +164 -163
- package/src/{localFiles.ts → project/localFiles.ts} +1 -1
- package/src/{manifest.ts → project/manifest.ts} +1 -1
- package/src/{project.ts → project/materialize.ts} +2 -2
- package/src/project/paths.ts +20 -0
- package/src/{textDiff.ts → project/textDiff.ts} +1 -1
- package/src/{types.ts → project/types.ts} +0 -0
- package/runtime/materials/lightmap-baked-lite.mat +0 -175
- package/runtime/materials/lightmap-baked.mat +0 -176
- package/runtime/scene-harness.json +0 -1
- package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/runtime/web/assets/basis-C64VHDVD.js +0 -1
- package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
- package/runtime/web/assets/draco-BiISTFcR.js +0 -118
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-BMt7AnC5.js +0 -2
- package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
- package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
- package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
- package/runtime/web/embed.html +0 -32
- package/runtime/web/embed.js +0 -171
- package/src/commands/design.ts +0 -846
- package/src/commands/update.ts +0 -211
- package/src/util.ts +0 -146
- /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
- /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
- /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 "
|
|
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 "
|
|
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 "
|
|
4
|
-
import { classifyKind, isTextKind } from "
|
|
5
|
-
import { buildPaths } from "
|
|
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
|
/*
|