lecodes-cli 0.19.0 → 0.19.2
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/index.js +730 -235
- package/package.json +14 -13
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +891 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +326 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +448 -0
- package/src/commands/appTemplates.ts +883 -0
- package/src/commands/appTemplatesAndroid.ts +599 -0
- package/src/commands/appTemplatesGradlew.ts +9 -0
- package/src/commands/assets.ts +28 -0
- package/src/commands/clone.ts +60 -0
- package/src/commands/compile.ts +253 -0
- package/src/commands/create.ts +59 -0
- package/src/commands/design.ts +863 -0
- package/src/commands/designTemplates.ts +11 -0
- package/src/commands/desktop.ts +122 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/diff.ts +59 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/install.ts +198 -0
- package/src/commands/lightmap.ts +290 -0
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/navmesh.ts +225 -0
- package/src/commands/pn.ts +305 -0
- package/src/commands/projectTemplates.ts +129 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +414 -0
- package/src/commands/scene.ts +148 -0
- package/src/commands/shaders.ts +157 -0
- package/src/commands/status.ts +29 -0
- package/src/commands/test.ts +370 -0
- package/src/commands/thumbs.ts +178 -0
- package/src/commands/types.ts +87 -0
- package/src/commands/update.ts +190 -0
- package/src/compile/assetIcons.ts +214 -0
- package/src/compile/collect.ts +46 -0
- package/src/compile/collectLocal.ts +37 -0
- package/src/compile/designCompile.ts +104 -0
- package/src/compile/fonts.ts +156 -0
- package/src/compile/headlessBundle.ts +129 -0
- package/src/compile/nativeStack.ts +21 -0
- package/src/compile/projectCompile.ts +109 -0
- package/src/compile/sceneCompile.ts +113 -0
- package/src/compile/screenEntry.ts +127 -0
- package/src/compile/shaders.ts +245 -0
- package/src/config.ts +42 -0
- package/src/designMeta.ts +35 -0
- package/src/desktopRenderer.ts +532 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +292 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +491 -0
- package/src/lecodes-3d-editor.d.ts +41 -0
- package/src/lecodes-assets.d.ts +7 -0
- package/src/lecodes-design.d.ts +191 -0
- package/src/lecodes-renderer.d.ts +131 -0
- package/src/localFiles.ts +144 -0
- package/src/manifest.ts +46 -0
- package/src/matcTool.ts +137 -0
- package/src/peers.ts +40 -0
- package/src/project.ts +61 -0
- package/src/projectEnv.ts +94 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/releases.ts +125 -0
- package/src/serverDiff.ts +78 -0
- package/src/textDiff.ts +103 -0
- package/src/types.ts +0 -0
- package/src/util.ts +146 -0
- package/runtime/sdk/animate/animate.ts +0 -238
- package/runtime/sdk/animate/bezier.ts +0 -138
- package/runtime/sdk/animate/easings.ts +0 -126
- package/runtime/sdk/canvas/Canvas.ts +0 -305
- package/runtime/sdk/compile/aspectMacro.ts +0 -86
- package/runtime/sdk/compile/assetIconMacro.ts +0 -384
- package/runtime/sdk/compile/assetMacro.ts +0 -146
- package/runtime/sdk/compile/assetName.ts +0 -50
- package/runtime/sdk/compile/bundler.ts +0 -287
- package/runtime/sdk/compile/compileProject.ts +0 -129
- package/runtime/sdk/compile/detectEntry.ts +0 -128
- package/runtime/sdk/compile/fontMacro.ts +0 -459
- package/runtime/sdk/compile/fontRegistry.ts +0 -78
- package/runtime/sdk/compile/header.ts +0 -67
- package/runtime/sdk/compile/index.ts +0 -100
- package/runtime/sdk/compile/libraryImports.ts +0 -52
- package/runtime/sdk/compile/liteMaterial.ts +0 -247
- package/runtime/sdk/compile/sceneEditor.ts +0 -88
- package/runtime/sdk/compile/serverSplit.ts +0 -233
- package/runtime/sdk/compile/serverTypes.ts +0 -227
- package/runtime/sdk/compile/sfnt.ts +0 -98
- package/runtime/sdk/compile/shaderSchema.ts +0 -202
- package/runtime/sdk/compile/shaderTargets.ts +0 -81
- package/runtime/sdk/compile/sourcemap.ts +0 -25
- package/runtime/sdk/core/Aspect.ts +0 -568
- package/runtime/sdk/core/InspectorUI.ts +0 -212
- package/runtime/sdk/core/StateMachine.ts +0 -308
- package/runtime/sdk/core/color.ts +0 -66
- package/runtime/sdk/core/compWrite.ts +0 -42
- package/runtime/sdk/core/events.ts +0 -38
- package/runtime/sdk/core/fields.ts +0 -120
- package/runtime/sdk/core/registry.ts +0 -23
- package/runtime/sdk/core/signals.ts +0 -277
- package/runtime/sdk/core/time.ts +0 -84
- package/runtime/sdk/g2/Camera2D.ts +0 -40
- package/runtime/sdk/g2/CharacterController2D.ts +0 -276
- package/runtime/sdk/g2/Node2D.ts +0 -267
- package/runtime/sdk/g2/OneWay2D.ts +0 -66
- package/runtime/sdk/g2/Physics2D.ts +0 -346
- package/runtime/sdk/g2/Scene2D.ts +0 -209
- package/runtime/sdk/g2/Shape2D.ts +0 -259
- package/runtime/sdk/g2/Sprite.ts +0 -89
- package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
- package/runtime/sdk/g2/SpriteSheet.ts +0 -166
- package/runtime/sdk/g2/Texture2D.ts +0 -47
- package/runtime/sdk/g2/Tilemap.ts +0 -41
- package/runtime/sdk/g2/Tileset.ts +0 -71
- package/runtime/sdk/g2/Trigger2D.ts +0 -77
- package/runtime/sdk/g2/autotile.ts +0 -433
- package/runtime/sdk/g2/cells.ts +0 -91
- package/runtime/sdk/g2/defineScene2d.ts +0 -381
- package/runtime/sdk/g2/groups2d.ts +0 -106
- package/runtime/sdk/g2/loop.ts +0 -50
- package/runtime/sdk/g2/scenarios2d.ts +0 -69
- package/runtime/sdk/g2/touch.ts +0 -83
- package/runtime/sdk/gl/Camera.ts +0 -160
- package/runtime/sdk/gl/CameraPlace.ts +0 -52
- package/runtime/sdk/gl/CharacterController.ts +0 -238
- package/runtime/sdk/gl/Gearbox.ts +0 -212
- package/runtime/sdk/gl/Geometry.ts +0 -279
- package/runtime/sdk/gl/IK.ts +0 -193
- package/runtime/sdk/gl/InstancedMesh.ts +0 -132
- package/runtime/sdk/gl/Light.ts +0 -107
- package/runtime/sdk/gl/Lightmap.ts +0 -256
- package/runtime/sdk/gl/Material.ts +0 -260
- package/runtime/sdk/gl/Mesh.ts +0 -83
- package/runtime/sdk/gl/Model.ts +0 -124
- package/runtime/sdk/gl/NavAgent.ts +0 -337
- package/runtime/sdk/gl/NavMesh.ts +0 -397
- package/runtime/sdk/gl/Node.ts +0 -350
- package/runtime/sdk/gl/Noise.ts +0 -30
- package/runtime/sdk/gl/Particles.ts +0 -676
- package/runtime/sdk/gl/Physics.ts +0 -222
- package/runtime/sdk/gl/Plane.ts +0 -53
- package/runtime/sdk/gl/Ragdoll.ts +0 -270
- package/runtime/sdk/gl/Ray.ts +0 -16
- package/runtime/sdk/gl/Scene.ts +0 -486
- package/runtime/sdk/gl/Shape.ts +0 -409
- package/runtime/sdk/gl/Terrain.ts +0 -1092
- package/runtime/sdk/gl/Texture.ts +0 -63
- package/runtime/sdk/gl/Trigger.ts +0 -45
- package/runtime/sdk/gl/Vehicle.ts +0 -473
- package/runtime/sdk/gl/Wheel.ts +0 -240
- package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
- package/runtime/sdk/gl/animation/Animator.ts +0 -120
- package/runtime/sdk/gl/animation/Layer.ts +0 -31
- package/runtime/sdk/gl/animation/Loop.ts +0 -30
- package/runtime/sdk/gl/animation/Playback.ts +0 -44
- package/runtime/sdk/gl/animation/core.ts +0 -336
- package/runtime/sdk/gl/controls.ts +0 -95
- package/runtime/sdk/gl/physicsEvents.ts +0 -20
- package/runtime/sdk/gl/scenarios.ts +0 -291
- package/runtime/sdk/gl/state.ts +0 -6
- package/runtime/sdk/gl/terrainMesh.ts +0 -219
- package/runtime/sdk/gl/touch.ts +0 -68
- package/runtime/sdk/inject.ts +0 -197
- package/runtime/sdk/math/Mathf.ts +0 -118
- package/runtime/sdk/math/mat4.ts +0 -278
- package/runtime/sdk/math/quat.ts +0 -232
- package/runtime/sdk/math/vec.ts +0 -255
- package/runtime/sdk/net/codec.ts +0 -119
- package/runtime/sdk/net/core.ts +0 -384
- package/runtime/sdk/net/index.ts +0 -181
- package/runtime/sdk/net/replication.ts +0 -622
- package/runtime/sdk/plugins/camera.ts +0 -81
- package/runtime/sdk/plugins/geolocation.ts +0 -123
- package/runtime/sdk/plugins/oauth.ts +0 -61
- package/runtime/sdk/plugins/permission.ts +0 -7
- package/runtime/sdk/plugins/push.ts +0 -132
- package/runtime/sdk/plugins/qr.ts +0 -73
- package/runtime/sdk/plugins/service.ts +0 -47
- package/runtime/sdk/runtime/app.ts +0 -101
- package/runtime/sdk/runtime/appEvents.ts +0 -54
- package/runtime/sdk/runtime/channel.ts +0 -61
- package/runtime/sdk/runtime/clipboard.ts +0 -20
- package/runtime/sdk/runtime/datetime.ts +0 -329
- package/runtime/sdk/runtime/device.ts +0 -293
- package/runtime/sdk/runtime/fetch.ts +0 -77
- package/runtime/sdk/runtime/files.ts +0 -23
- package/runtime/sdk/runtime/input.ts +0 -175
- package/runtime/sdk/runtime/media.ts +0 -111
- package/runtime/sdk/runtime/misc.ts +0 -16
- package/runtime/sdk/runtime/net.ts +0 -36
- package/runtime/sdk/runtime/rpc.ts +0 -218
- package/runtime/sdk/runtime/service.ts +0 -83
- package/runtime/sdk/runtime/share.ts +0 -9
- package/runtime/sdk/runtime/storage.ts +0 -13
- package/runtime/sdk/runtime/touch.ts +0 -76
- package/runtime/sdk/scene/defineScene.ts +0 -1353
- package/runtime/sdk/scene/editorPlugins.ts +0 -110
- package/runtime/sdk/scene/gizmos.ts +0 -148
- package/runtime/sdk/scene/grammar.ts +0 -120
- package/runtime/sdk/scene/material.ts +0 -188
- package/runtime/sdk/server/auth/appConfig.ts +0 -12
- package/runtime/sdk/server/auth/global.ts +0 -80
- package/runtime/sdk/server/auth/host.ts +0 -318
- package/runtime/sdk/server/auth/models.ts +0 -83
- package/runtime/sdk/server/auth/types.ts +0 -50
- package/runtime/sdk/server/channel.ts +0 -56
- package/runtime/sdk/server/context.ts +0 -36
- package/runtime/sdk/server/db/defineDb.ts +0 -237
- package/runtime/sdk/server/db/fields.ts +0 -132
- package/runtime/sdk/server/db/httpTransport.ts +0 -93
- package/runtime/sdk/server/db/index.ts +0 -7
- package/runtime/sdk/server/db/marci/query.ts +0 -412
- package/runtime/sdk/server/db/types.ts +0 -202
- package/runtime/sdk/server/errors.ts +0 -12
- package/runtime/sdk/server/host.ts +0 -74
- package/runtime/sdk/server/inject.ts +0 -13
- package/runtime/sdk/server/runtime.ts +0 -133
- package/runtime/sdk/server/validate.ts +0 -87
- package/runtime/sdk/ui/NativeView.ts +0 -142
- package/runtime/sdk/ui/UI.ts +0 -39
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
- package/runtime/sdk/ui/UIButton.ts +0 -101
- package/runtime/sdk/ui/UIContainer.ts +0 -60
- package/runtime/sdk/ui/UIImage.ts +0 -83
- package/runtime/sdk/ui/UIInput.ts +0 -185
- package/runtime/sdk/ui/UIModal.ts +0 -139
- package/runtime/sdk/ui/UINode.ts +0 -830
- package/runtime/sdk/ui/UIPager.ts +0 -362
- package/runtime/sdk/ui/UIPopover.ts +0 -100
- package/runtime/sdk/ui/UIScreen.ts +0 -123
- package/runtime/sdk/ui/UIScrollable.ts +0 -87
- package/runtime/sdk/ui/UISpacer.ts +0 -14
- package/runtime/sdk/ui/UITabs.ts +0 -236
- package/runtime/sdk/ui/UIText.ts +0 -51
- package/runtime/sdk/ui/UIVideo.ts +0 -88
- package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
- package/runtime/sdk/ui/UIWidget.ts +0 -127
- package/runtime/sdk/ui/fonts.ts +0 -13
- package/runtime/sdk/ui/presentable.ts +0 -117
- package/runtime/sdk/ui/router.ts +0 -132
- package/runtime/sdk/ui/theme.ts +0 -84
package/src/api.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { CliError } from "./util"
|
|
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
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createServer } from "node:http"
|
|
2
|
+
import type { AddressInfo } from "node:net"
|
|
3
|
+
import { spawn } from "node:child_process"
|
|
4
|
+
import { randomBytes } from "node:crypto"
|
|
5
|
+
import { hostname } from "node:os"
|
|
6
|
+
import { CliError, c, info, log } from "./util"
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Browser login (loopback OAuth-style flow, like `vercel`/`gh`):
|
|
10
|
+
* 1. start a one-shot HTTP listener on 127.0.0.1:<random port>
|
|
11
|
+
* 2. open the browser to <web>/app/cli-auth?port&state&name
|
|
12
|
+
* 3. the web page (user already signed in) mints a personal access token and redirects it back
|
|
13
|
+
* to the local listener, which captures it and shows a success page.
|
|
14
|
+
* `state` is a random nonce echoed back to prevent another local page from injecting a token.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
|
|
18
|
+
<body style="font-family:system-ui,sans-serif;background:#0f1115;color:#e6e6e6;display:grid;place-items:center;height:100vh;margin:0">
|
|
19
|
+
<div style="text-align:center;max-width:420px;padding:32px">
|
|
20
|
+
<h1 style="font-size:20px;margin:0 0 8px">✓ Authorized</h1>
|
|
21
|
+
<p style="color:#9aa0aa;margin:0;font-size:14px">You can close this tab and return to your terminal.</p></div>`
|
|
22
|
+
|
|
23
|
+
const errorHtml = (msg: string) => `<!doctype html><meta charset="utf-8"><title>lecodes</title>
|
|
24
|
+
<body style="font-family:system-ui,sans-serif;background:#0f1115;color:#e6e6e6;display:grid;place-items:center;height:100vh;margin:0">
|
|
25
|
+
<div style="text-align:center;max-width:420px;padding:32px">
|
|
26
|
+
<h1 style="font-size:20px;margin:0 0 8px">Authorization failed</h1>
|
|
27
|
+
<p style="color:#9aa0aa;margin:0;font-size:14px">${msg}</p></div>`
|
|
28
|
+
|
|
29
|
+
/** Open a URL in the default browser (best-effort; the URL is also printed as a fallback). */
|
|
30
|
+
export const openBrowser = (url: string) => {
|
|
31
|
+
let cmd: string
|
|
32
|
+
let args: string[]
|
|
33
|
+
if (process.platform === "win32") { cmd = "rundll32"; args = [ "url.dll,FileProtocolHandler", url ] }
|
|
34
|
+
else if (process.platform === "darwin") { cmd = "open"; args = [ url ] }
|
|
35
|
+
else { cmd = "xdg-open"; args = [ url ] }
|
|
36
|
+
try {
|
|
37
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true })
|
|
38
|
+
child.on("error", () => {})
|
|
39
|
+
child.unref()
|
|
40
|
+
} catch { /* fall back to the printed URL */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Run the browser login flow against `webBase` (the app origin) and resolve the access token. */
|
|
44
|
+
export const browserLogin = async (
|
|
45
|
+
webBase: string, timeoutMs = 5 * 60 * 1000, open: (url: string) => void = openBrowser,
|
|
46
|
+
): Promise<string> => {
|
|
47
|
+
const state = randomBytes(16).toString("hex")
|
|
48
|
+
|
|
49
|
+
let resolve!: (token: string) => void
|
|
50
|
+
let reject!: (err: Error) => void
|
|
51
|
+
const tokenPromise = new Promise<string>((res, rej) => { resolve = res; reject = rej })
|
|
52
|
+
|
|
53
|
+
const server = createServer((req, res) => {
|
|
54
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1")
|
|
55
|
+
if (url.pathname !== "/") { res.writeHead(404); res.end(); return }
|
|
56
|
+
const token = url.searchParams.get("token")
|
|
57
|
+
if (!token || url.searchParams.get("state") !== state) {
|
|
58
|
+
res.writeHead(400, { "content-type": "text/html" })
|
|
59
|
+
res.end(errorHtml("Invalid or expired authorization."))
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
res.writeHead(200, { "content-type": "text/html" })
|
|
63
|
+
res.end(SUCCESS_HTML)
|
|
64
|
+
resolve(token)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
await new Promise<void>((res, rej) => {
|
|
68
|
+
server.once("error", rej)
|
|
69
|
+
server.listen(0, "127.0.0.1", res)
|
|
70
|
+
})
|
|
71
|
+
const port = (server.address() as AddressInfo).port
|
|
72
|
+
|
|
73
|
+
const timer = setTimeout(() => reject(new CliError("Timed out waiting for browser authorization.")), timeoutMs)
|
|
74
|
+
|
|
75
|
+
const authUrl = `${webBase}/app/cli-auth?port=${port}&state=${state}&name=${encodeURIComponent(hostname())}`
|
|
76
|
+
info("Opening your browser to authorize the CLI…")
|
|
77
|
+
log(c.dim(" " + authUrl))
|
|
78
|
+
log(c.dim(" (waiting — finish in the browser, or press Ctrl+C to cancel)"))
|
|
79
|
+
open(authUrl)
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
return await tokenPromise
|
|
83
|
+
} finally {
|
|
84
|
+
clearTimeout(timer)
|
|
85
|
+
server.close()
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/cmgenTool.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { join, resolve } from "node:path"
|
|
4
|
+
import { downloadReleaseArchive, fetchReleaseProduct, semverNewer, type ManifestVersion } from "./releases"
|
|
5
|
+
import { CliError, note } from "./util"
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Filament's cmgen (environment tool) — the tool behind `lecodes assets sky`, which turns an
|
|
9
|
+
* equirectangular .exr/.hdr into the two cubemaps a scene needs: the sharp one that gets DRAWN as
|
|
10
|
+
* the sky, and the roughness-prefiltered one (+ spherical harmonics) that LIGHTS it.
|
|
11
|
+
*
|
|
12
|
+
* The binary is NOT shipped with the CLI (most projects never bake an environment). It downloads on
|
|
13
|
+
* first use from the same S3 release bucket as the desktop renderer and matc (product "cmgen" in
|
|
14
|
+
* releases/versions.json — published by scripts/release/release-windows.ps1 from the desktop
|
|
15
|
+
* build's in-tree filament host tools, so the KTX it writes always matches the engine the hosts
|
|
16
|
+
* ship). Cache: ~/.lecodes/cmgen/cmgen-<version>/. LECODES_CMGEN overrides resolution entirely
|
|
17
|
+
* (local dev: packages/desktop/build/creator-gl/filament/tools/cmgen/cmgen.exe).
|
|
18
|
+
*
|
|
19
|
+
* The CLI resolves it and hands the path to lecodes-assets through LECODES_CMGEN, so that package
|
|
20
|
+
* stays network-free and still works standalone against a local build.
|
|
21
|
+
*
|
|
22
|
+
* No version pin, same reasoning as matc: the KTX1 layout is tied to the vendored Filament and
|
|
23
|
+
* changes rarely, so resolution takes the newest cached copy, else downloads `latest`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const exeName = () => (process.platform === "win32" ? "cmgen.exe" : "cmgen")
|
|
27
|
+
|
|
28
|
+
const platformKey = (): string => {
|
|
29
|
+
if (process.platform === "win32") return "windows-x64"
|
|
30
|
+
if (process.platform === "linux") return "linux-x64"
|
|
31
|
+
throw new CliError(
|
|
32
|
+
process.platform === "darwin"
|
|
33
|
+
? "cmgen isn't published for macOS yet — build filament's host tools locally and set LECODES_CMGEN."
|
|
34
|
+
: `cmgen isn't published for ${process.platform}.`,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const cacheRoot = () => join(homedir(), ".lecodes", "cmgen")
|
|
39
|
+
|
|
40
|
+
/** Locate the cmgen binary inside a cache/stage dir (the archive may or may not nest a folder). */
|
|
41
|
+
const findCmgenIn = (dir: string): string | null => {
|
|
42
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
43
|
+
const p = join(dir, entry.name)
|
|
44
|
+
if (entry.isFile() && entry.name === exeName()) return p
|
|
45
|
+
if (entry.isDirectory()) { const found = findCmgenIn(p); if (found) return found }
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Versions with a working cmgen binary in the cache, newest first. */
|
|
51
|
+
export const installedCmgenVersions = (): string[] => {
|
|
52
|
+
try {
|
|
53
|
+
return readdirSync(cacheRoot(), { withFileTypes: true })
|
|
54
|
+
.filter((e) => e.isDirectory() && e.name.startsWith("cmgen-"))
|
|
55
|
+
.map((e) => e.name.slice("cmgen-".length))
|
|
56
|
+
.filter((v) => findCmgenIn(join(cacheRoot(), `cmgen-${v}`)) !== null)
|
|
57
|
+
.sort(semverNewer)
|
|
58
|
+
} catch {
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const fetchCmgenManifest = (): Promise<{ latest: string, versions: ManifestVersion[] }> =>
|
|
64
|
+
fetchReleaseProduct("cmgen", "cmgen (environment tool)")
|
|
65
|
+
|
|
66
|
+
const downloadCmgen = async (version: string, versions: ManifestVersion[], dest: string): Promise<void> => {
|
|
67
|
+
const key = platformKey()
|
|
68
|
+
const file = versions.find((v) => v.version === version)?.files[key]
|
|
69
|
+
if (!file) {
|
|
70
|
+
throw new CliError(
|
|
71
|
+
`cmgen ${version} [${key}] isn't published — build filament's host tools locally and point ` +
|
|
72
|
+
`LECODES_CMGEN at the binary.`,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
note(`Downloading the environment tool (cmgen ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`)
|
|
76
|
+
await downloadReleaseArchive(file, {
|
|
77
|
+
cacheRoot: cacheRoot(),
|
|
78
|
+
dest,
|
|
79
|
+
validate: (dir) => (findCmgenIn(dir) ? null : "The downloaded archive doesn't contain the cmgen binary."),
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The cmgen binary WITHOUT touching the network: LECODES_CMGEN → newest cached version → null. */
|
|
84
|
+
export const resolveCmgenExeCached = (): string | null => {
|
|
85
|
+
const override = process.env.LECODES_CMGEN
|
|
86
|
+
if (override) {
|
|
87
|
+
if (!existsSync(override)) throw new CliError(`LECODES_CMGEN points at "${override}" which doesn't exist.`)
|
|
88
|
+
return resolve(override)
|
|
89
|
+
}
|
|
90
|
+
const cached = installedCmgenVersions()[0]
|
|
91
|
+
return cached ? findCmgenIn(join(cacheRoot(), `cmgen-${cached}`)) : null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The cmgen binary: LECODES_CMGEN → newest cached version → download the latest release. */
|
|
95
|
+
export const resolveCmgenExe = async (): Promise<string> => {
|
|
96
|
+
const cached = resolveCmgenExeCached()
|
|
97
|
+
if (cached) return cached
|
|
98
|
+
const { latest, versions } = await fetchCmgenManifest()
|
|
99
|
+
const dir = join(cacheRoot(), `cmgen-${latest}`)
|
|
100
|
+
await downloadCmgen(latest, versions, dir)
|
|
101
|
+
const exe = findCmgenIn(dir)
|
|
102
|
+
if (!exe) throw new CliError(`cmgen cache at ${dir} is broken — delete the folder and retry.`)
|
|
103
|
+
return exe
|
|
104
|
+
}
|