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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Types for the optional `lecodes-design` package's server entry (lazy-loaded by the `design`
|
|
2
|
+
// command). The real package is installed separately; this lets the dynamic import typecheck
|
|
3
|
+
// without it present.
|
|
4
|
+
declare module "lecodes-design/server" {
|
|
5
|
+
export type CompiledScreen = { js: string, warnings: string[] }
|
|
6
|
+
|
|
7
|
+
export type RenderedScreen = { json?: string, png?: Buffer, warnings: string[] }
|
|
8
|
+
|
|
9
|
+
export type DesignServerOptions = {
|
|
10
|
+
root: string
|
|
11
|
+
designDir: string
|
|
12
|
+
name?: string
|
|
13
|
+
compileScreen: (id: string, state?: string) => Promise<CompiledScreen>
|
|
14
|
+
renderScreen?: (id: string, format: "json" | "png" | "both", state?: string) => Promise<RenderedScreen>
|
|
15
|
+
port?: number
|
|
16
|
+
appDir?: string
|
|
17
|
+
log?: (message: string) => void
|
|
18
|
+
/** True when a platform project backs this folder — decides who owns the conversation. */
|
|
19
|
+
linked?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type DesignServer = {
|
|
23
|
+
port: number
|
|
24
|
+
url: string
|
|
25
|
+
close: () => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function startDesignServer(options: DesignServerOptions): Promise<DesignServer>
|
|
29
|
+
|
|
30
|
+
export type ArrangeSummary = {
|
|
31
|
+
changed: boolean
|
|
32
|
+
moved: number
|
|
33
|
+
previous: Record<string, [number, number]>
|
|
34
|
+
mode: "full" | "new" | "selection"
|
|
35
|
+
score?: { before: number, after: number }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function arrangeDesign(designDir: string, opts?: { onlyNew?: boolean, only?: string[] }): ArrangeSummary
|
|
39
|
+
|
|
40
|
+
export type DesignScreen = {
|
|
41
|
+
id: string
|
|
42
|
+
file: string
|
|
43
|
+
desc?: string
|
|
44
|
+
pos: [number, number]
|
|
45
|
+
size: [number, number]
|
|
46
|
+
group?: string
|
|
47
|
+
role?: string
|
|
48
|
+
/** State ids discovered from the screen source (≥1). */
|
|
49
|
+
states: string[]
|
|
50
|
+
/** The canonical state (∈ states). */
|
|
51
|
+
canonical: string
|
|
52
|
+
/** Soft: default export has an annotated non-union param (states not discoverable). */
|
|
53
|
+
statesWarning?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type DesignEdge = { from: string, to: string, label?: string, activator?: string }
|
|
57
|
+
export type ServedEdge = {
|
|
58
|
+
from: string
|
|
59
|
+
to: string
|
|
60
|
+
label?: string
|
|
61
|
+
activator?: string
|
|
62
|
+
index: number
|
|
63
|
+
fromState?: string
|
|
64
|
+
toState?: string
|
|
65
|
+
}
|
|
66
|
+
export type DesignGroup = { name: string, color: string, desc?: string, members: string[], impliedBy?: string }
|
|
67
|
+
|
|
68
|
+
/** A source-discovered tab bar (`defineTabs` in shared/ + mount callsites) — see tabbar-surface.md. */
|
|
69
|
+
export type DesignHub = {
|
|
70
|
+
name: string
|
|
71
|
+
file: string
|
|
72
|
+
/** Tab ids in declaration order (root screen ids). */
|
|
73
|
+
tabs: string[]
|
|
74
|
+
/** screen id → tab id for every screen that mounts the bar. */
|
|
75
|
+
lanes: Record<string, string>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type DesignState = {
|
|
79
|
+
screens: DesignScreen[]
|
|
80
|
+
edges: ServedEdge[]
|
|
81
|
+
groups: DesignGroup[]
|
|
82
|
+
hubs: DesignHub[]
|
|
83
|
+
warnings: string[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readDesignState(designDir: string): DesignState
|
|
87
|
+
|
|
88
|
+
export type ConceptReport = {
|
|
89
|
+
hasSpec: boolean
|
|
90
|
+
screenRefs: { id: string, exists: boolean }[]
|
|
91
|
+
screensWithoutRole: string[]
|
|
92
|
+
warnings: string[]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function conceptReport(designDir: string, screens: DesignScreen[]): ConceptReport
|
|
96
|
+
|
|
97
|
+
/** Provenance: `share` = unverified stranger on the link, `ai` = Claude on a member's behalf. */
|
|
98
|
+
export type CommentOrigin = "share" | "editor" | "cli" | "ai"
|
|
99
|
+
/** A review comment pinned to the board (docs/design-comments-plan.md). */
|
|
100
|
+
export type CommentMessage = {
|
|
101
|
+
id: string
|
|
102
|
+
author: string
|
|
103
|
+
origin: CommentOrigin
|
|
104
|
+
at: string
|
|
105
|
+
body: string
|
|
106
|
+
editedAt?: string
|
|
107
|
+
mine?: boolean
|
|
108
|
+
}
|
|
109
|
+
/** What the pin was dropped on, captured from the laid-out tree at pin time. `name` is the author
|
|
110
|
+
* `name` of the nearest named ancestor, `text` what it read; `path` is a weak positional hint. */
|
|
111
|
+
export type CommentTarget = { name?: string, text?: string, path?: string }
|
|
112
|
+
export type CommentAnchor = {
|
|
113
|
+
screen?: string
|
|
114
|
+
state?: string
|
|
115
|
+
target?: CommentTarget
|
|
116
|
+
point?: [number, number]
|
|
117
|
+
}
|
|
118
|
+
export type CommentThread = {
|
|
119
|
+
id: string
|
|
120
|
+
/** Absent until the platform allocates one — a thread started locally is a draft until pushed. */
|
|
121
|
+
number?: number
|
|
122
|
+
anchor: CommentAnchor
|
|
123
|
+
resolved: boolean
|
|
124
|
+
resolvedAt?: string | null
|
|
125
|
+
createdAt: string
|
|
126
|
+
updatedAt: string
|
|
127
|
+
commitId?: number | null
|
|
128
|
+
/** Set here and not yet sent; `lecodes push` delivers it. */
|
|
129
|
+
pendingResolve?: true
|
|
130
|
+
messages: CommentMessage[]
|
|
131
|
+
mine?: boolean
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Read `design/comments/` — one file per thread (absent directory → no threads). */
|
|
135
|
+
export function listDesignComments(designDir: string): {
|
|
136
|
+
threads: CommentThread[]
|
|
137
|
+
canComment: boolean
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Start a thread. Linked → a numberless draft `lecodes push` names; standalone → numbered now. */
|
|
141
|
+
export function startThread(
|
|
142
|
+
designDir: string,
|
|
143
|
+
draft: { anchor: CommentAnchor, body: string, author?: string, origin?: CommentOrigin },
|
|
144
|
+
mode: { linked: boolean },
|
|
145
|
+
): CommentThread
|
|
146
|
+
/** Append a reply (marked pending until `lecodes push`). `as` overrides attribution — pass
|
|
147
|
+
* Claude/ai when the assistant, not the developer, is writing. */
|
|
148
|
+
export function replyToThread(
|
|
149
|
+
designDir: string,
|
|
150
|
+
threadId: string,
|
|
151
|
+
body: string,
|
|
152
|
+
as?: { author?: string, origin?: CommentOrigin },
|
|
153
|
+
): CommentThread
|
|
154
|
+
/** Flip resolved (marked pending until `lecodes push`). */
|
|
155
|
+
export function resolveThread(designDir: string, threadId: string, resolved: boolean): CommentThread
|
|
156
|
+
|
|
157
|
+
export type MirrorSummary = { added: number, updated: number, closed: number }
|
|
158
|
+
/** Fold the platform's OPEN threads into `design/comments/`. A local file the snapshot doesn't
|
|
159
|
+
* carry was resolved or deleted upstream and is removed — unless it still has unsent writes. */
|
|
160
|
+
export function mirrorThreads(designDir: string, remote: CommentThread[]): MirrorSummary
|
|
161
|
+
|
|
162
|
+
export type QueuedThread = {
|
|
163
|
+
thread: CommentThread
|
|
164
|
+
file: string
|
|
165
|
+
pending: { newThread: boolean, messages: CommentMessage[], resolve: boolean }
|
|
166
|
+
}
|
|
167
|
+
/** Everything waiting to go up. Nothing leaves the machine except through `lecodes push`, which
|
|
168
|
+
* drains exactly this. */
|
|
169
|
+
export function queuedThreads(designDir: string): QueuedThread[]
|
|
170
|
+
/** What a push actually delivered for one queued thread (`messageIds` holds LOCAL message ids). */
|
|
171
|
+
export type DeliveredWrites = { messageIds: Set<string>, resolve: boolean }
|
|
172
|
+
/** Adopt what the platform says the thread is now. Only what `delivered` names settles — the
|
|
173
|
+
* rest keeps its pending flag, so a half-failed push retries instead of duplicating. */
|
|
174
|
+
export function settleQueued(
|
|
175
|
+
designDir: string,
|
|
176
|
+
queued: QueuedThread,
|
|
177
|
+
upstream: CommentThread,
|
|
178
|
+
delivered: DeliveredWrites,
|
|
179
|
+
): void
|
|
180
|
+
|
|
181
|
+
/** How a thread is referred to: "#7", or "new" while it's an unpushed draft. */
|
|
182
|
+
export function threadRef(thread: CommentThread): string
|
|
183
|
+
/** The pre-directory `comments.json`, if it's still lying around. */
|
|
184
|
+
export function legacyCommentsFile(designDir: string): string | null
|
|
185
|
+
|
|
186
|
+
export type ScreenStates = { states: string[], canonical: string, warning?: string, error?: string }
|
|
187
|
+
/** Discover a screen's states from its source (the default export's first-param literal union). */
|
|
188
|
+
export function discoverScreenStates(source: string): ScreenStates
|
|
189
|
+
/** Split a `screen@state` reference on the last `@` (no `@` → no state). */
|
|
190
|
+
export function parseStateRef(ref: string): { screen: string, state?: string }
|
|
191
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Types for the optional `lecodes-renderer` package's node headless entry (lazy-loaded by the
|
|
2
|
+
// `render` command). The real package is installed separately; this lets the dynamic import
|
|
3
|
+
// typecheck without it present.
|
|
4
|
+
declare module "lecodes-renderer/headless" {
|
|
5
|
+
export type RenderOptions = {
|
|
6
|
+
width?: number
|
|
7
|
+
height?: number
|
|
8
|
+
images?: Record<string, { width: number, height: number }>
|
|
9
|
+
/** Max ms to wait for promises/timers/fetch/image decode before snapshotting (ends early on idle). */
|
|
10
|
+
settleMs?: number
|
|
11
|
+
/** Let data fetches hit the real network (off by default). */
|
|
12
|
+
waitNetwork?: boolean
|
|
13
|
+
/** Resolve `lecodesfile://<abs-path>` resource URLs from the local filesystem (the `render`
|
|
14
|
+
* command emits these for a local project's assets). */
|
|
15
|
+
localAssets?: boolean
|
|
16
|
+
/** Render at this animation time (ms); 0 = the initial frame-0 state. */
|
|
17
|
+
timeMs?: number
|
|
18
|
+
/** Advance animations to their resting state before snapshotting (default true). */
|
|
19
|
+
afterAnimations?: boolean
|
|
20
|
+
/** Override safe-area insets [top, right, bottom, left] (px) for the `safe-*` keywords. */
|
|
21
|
+
safeArea?: [number, number, number, number]
|
|
22
|
+
/** Receive the project's console.* output (unset → swallowed). */
|
|
23
|
+
onConsole?: (level: string, text: string) => void
|
|
24
|
+
/** Canned `_creatorUtils.fetch` responses (URL pattern → { status?, json?, text? }); they beat
|
|
25
|
+
* every other fetch path, including waitNetwork. */
|
|
26
|
+
fixtures?: Fixtures
|
|
27
|
+
}
|
|
28
|
+
export type FetchFixture = { status?: number, json?: unknown, text?: string }
|
|
29
|
+
export type Fixtures = Record<string, FetchFixture>
|
|
30
|
+
export function validateFixtures(fixtures: Fixtures): void
|
|
31
|
+
export type RenderResult = { text: string, tree: unknown, warnings: string[] }
|
|
32
|
+
export function renderToJson(js: string, opts?: RenderOptions): Promise<RenderResult>
|
|
33
|
+
|
|
34
|
+
/** A frame region in logical px: a named node (its layout rect) or an explicit rect. */
|
|
35
|
+
export type ClipTarget = string | { x: number, y: number, width: number, height: number }
|
|
36
|
+
export type PngOptions = RenderOptions & { scale?: number, clip?: ClipTarget }
|
|
37
|
+
export type PngResult = { png: Buffer, width: number, height: number, warnings: string[] }
|
|
38
|
+
export function renderToPng(js: string, opts?: PngOptions): Promise<PngResult>
|
|
39
|
+
|
|
40
|
+
// ---- top-down map (lecodes render --map) ----
|
|
41
|
+
export type MapRenderOptions = { maxPx?: number, title?: string, labelAll?: boolean }
|
|
42
|
+
export function renderToMap(
|
|
43
|
+
js: string, opts?: RenderOptions & MapRenderOptions,
|
|
44
|
+
): Promise<{ png: Buffer, data: unknown, warnings: string[] }>
|
|
45
|
+
|
|
46
|
+
// ---- interactive session (tap/type/scroll/wait between snapshots) ----
|
|
47
|
+
export type SessionTarget = string | { x: number, y: number }
|
|
48
|
+
export type SessionOptions = RenderOptions & {
|
|
49
|
+
/** Which safe-area edges are exact-height system bars (Android) vs clearance insets (iOS). */
|
|
50
|
+
safeAreaBars?: ("top" | "right" | "bottom" | "left")[]
|
|
51
|
+
/** true → require @napi-rs/canvas (throw if missing); false → JSON metrics only (no png());
|
|
52
|
+
* "auto" (default) → rasterize when the package is installed. */
|
|
53
|
+
raster?: boolean | "auto"
|
|
54
|
+
/** Device-pixel scale for png() (default 2). */
|
|
55
|
+
scale?: number
|
|
56
|
+
}
|
|
57
|
+
export type Session = {
|
|
58
|
+
warnings: string[]
|
|
59
|
+
canRasterize: boolean
|
|
60
|
+
find: (target: string) => unknown | null
|
|
61
|
+
tap: (target: SessionTarget) => Promise<void>
|
|
62
|
+
type: (target: SessionTarget, text: string) => Promise<void>
|
|
63
|
+
fill: (value?: string) => Promise<void>
|
|
64
|
+
scroll: (delta: number, target?: SessionTarget) => Promise<void>
|
|
65
|
+
wait: (ms: number) => Promise<void>
|
|
66
|
+
frames: (n: number) => Promise<void>
|
|
67
|
+
drag: (from: SessionTarget, to: SessionTarget, opts?: { ms?: number }) => Promise<void>
|
|
68
|
+
hold: (target: SessionTarget, ms: number) => Promise<void>
|
|
69
|
+
keyDown: (code: string) => void
|
|
70
|
+
keyUp: (code: string) => void
|
|
71
|
+
key: (code: string, ms: number) => Promise<void>
|
|
72
|
+
settle: (ms?: number) => Promise<void>
|
|
73
|
+
json: () => unknown
|
|
74
|
+
text: () => string
|
|
75
|
+
png: (opts?: { clip?: ClipTarget }) => Promise<Buffer>
|
|
76
|
+
clipRect: (clip: ClipTarget) => { x: number, y: number, width: number, height: number }
|
|
77
|
+
logs: () => string[]
|
|
78
|
+
screenName: () => string | null
|
|
79
|
+
value: (target: string) => string
|
|
80
|
+
clock: () => number
|
|
81
|
+
dispose: () => void
|
|
82
|
+
}
|
|
83
|
+
export function createSession(js: string, opts?: SessionOptions): Promise<Session>
|
|
84
|
+
|
|
85
|
+
// ---- scenario engine (lecodes render --script / lecodes test) ----
|
|
86
|
+
export type ExpectSpec = {
|
|
87
|
+
text?: string
|
|
88
|
+
notText?: string
|
|
89
|
+
node?: string
|
|
90
|
+
notNode?: string
|
|
91
|
+
screen?: string
|
|
92
|
+
value?: { target: string, equals: string }
|
|
93
|
+
log?: string
|
|
94
|
+
}
|
|
95
|
+
export type ScenarioStep = {
|
|
96
|
+
name?: string
|
|
97
|
+
tap?: SessionTarget
|
|
98
|
+
type?: string
|
|
99
|
+
text?: string
|
|
100
|
+
fill?: { value?: string }
|
|
101
|
+
scroll?: string | number
|
|
102
|
+
by?: number
|
|
103
|
+
wait?: number
|
|
104
|
+
frames?: number
|
|
105
|
+
drag?: SessionTarget
|
|
106
|
+
to?: SessionTarget
|
|
107
|
+
ms?: number
|
|
108
|
+
hold?: SessionTarget
|
|
109
|
+
key?: string
|
|
110
|
+
keyDown?: string
|
|
111
|
+
keyUp?: string
|
|
112
|
+
/** 3D inspection camera (scene3d report): named auto-fitted view, fixed eye/look-at, or "off". */
|
|
113
|
+
view?: "iso" | "top" | "front" | "side" | "back" | "off"
|
|
114
|
+
fov?: number
|
|
115
|
+
camera?: { at: [number, number, number], lookAt: [number, number, number], fov?: number } | "off"
|
|
116
|
+
expect?: ExpectSpec
|
|
117
|
+
screenshot?: string
|
|
118
|
+
clip?: ClipTarget
|
|
119
|
+
json?: string
|
|
120
|
+
/** Write the annotated top-down map of the 3D scene: `<path>.png` + `<path>.json` data. */
|
|
121
|
+
map?: string
|
|
122
|
+
}
|
|
123
|
+
export type ScenarioIO = { write: (relPath: string, data: string | Uint8Array) => void }
|
|
124
|
+
export type StepOutcome = { index: number, desc: string, ok: boolean, error?: string }
|
|
125
|
+
export type ScenarioOutcome = { ok: boolean, steps: StepOutcome[], failureArtifacts: string[] }
|
|
126
|
+
export function parseScenarioSteps(raw: unknown): ScenarioStep[]
|
|
127
|
+
export function describeStep(step: ScenarioStep): string
|
|
128
|
+
export function runScenario(
|
|
129
|
+
session: Session, steps: ScenarioStep[], io: ScenarioIO, opts?: { artifactPrefix?: string },
|
|
130
|
+
): Promise<ScenarioOutcome>
|
|
131
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
2
|
+
import { join, relative } from "node:path"
|
|
3
|
+
import { sanitizeAssetName } from "lecodes-sdk/compile/assetName"
|
|
4
|
+
import { sha256, toKey } from "./util"
|
|
5
|
+
import { loadIgnore, type IgnoreMatcher } from "./ignore"
|
|
6
|
+
import type { FileKind, Manifest } from "./manifest"
|
|
7
|
+
|
|
8
|
+
/* Scanning the working tree and diffing it against the manifest. Paths are normalized to posix
|
|
9
|
+
* keys ("/a/b.ts") so they line up with the asset tree regardless of OS. */
|
|
10
|
+
|
|
11
|
+
// Directories never treated as project content. tsconfig.json / .gitignore etc. are skipped as
|
|
12
|
+
// files (see SKIP_FILES) since they're CLI/tooling artifacts, not assets.
|
|
13
|
+
const SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"])
|
|
14
|
+
const SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"])
|
|
15
|
+
|
|
16
|
+
// Extensions stored as text by the platform. ".mat" -> shader, ".scene" -> scene; everything
|
|
17
|
+
// else (images, audio, glb, ...) is a binary resource.
|
|
18
|
+
const TEXT_EXTS = new Set([
|
|
19
|
+
"ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "svg", "glsl", "vert", "frag",
|
|
20
|
+
"txt", "md", "css", "html", "xml", "yml", "yaml", "csv",
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
const ext = (name: string) => {
|
|
24
|
+
const i = name.lastIndexOf(".")
|
|
25
|
+
return i < 0 ? "" : name.slice(i + 1).toLowerCase()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Best-effort kind for a file the user added locally (known files keep their manifest type). */
|
|
29
|
+
export const classifyKind = (name: string): FileKind => {
|
|
30
|
+
const e = ext(name)
|
|
31
|
+
if (e === "mat") return "shader"
|
|
32
|
+
if (e === "scene") return "scene"
|
|
33
|
+
return TEXT_EXTS.has(e) ? "text" : "resource"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const isTextKind = (kind: FileKind) => kind !== "resource"
|
|
37
|
+
|
|
38
|
+
export type LocalFile = { path: string, absPath: string, sha: string }
|
|
39
|
+
|
|
40
|
+
export type LocalScan = { files: LocalFile[], dirs: string[], ignored: string[] }
|
|
41
|
+
|
|
42
|
+
export type ScanOptions = {
|
|
43
|
+
/** A `.lecodesignore` matcher; untracked matching files/dirs are skipped (see ignore.ts). */
|
|
44
|
+
ignore?: IgnoreMatcher
|
|
45
|
+
/** Paths already in the project — never ignored, so an ignore rule can't drop an existing asset. */
|
|
46
|
+
tracked?: Set<string>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Recursively scan the working tree, skipping ignored dirs/files; hashes each file. */
|
|
50
|
+
export const scanLocal = (root: string, opts: ScanOptions = {}): LocalScan => {
|
|
51
|
+
const { ignore, tracked } = opts
|
|
52
|
+
const files: LocalFile[] = []
|
|
53
|
+
const dirs: string[] = []
|
|
54
|
+
const ignored: string[] = []
|
|
55
|
+
const isTracked = (key: string) => tracked?.has(key) ?? false
|
|
56
|
+
// Don't prune a dir that contains a tracked file (e.g. an ignored `assets/` that holds a real asset).
|
|
57
|
+
const hasTrackedUnder = (key: string) => {
|
|
58
|
+
if (!tracked) return false
|
|
59
|
+
const prefix = key + "/"
|
|
60
|
+
for (const t of tracked) if (t.startsWith(prefix)) return true
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
// `parentIgnored`: we descended into an ignored directory only because it holds a tracked file.
|
|
64
|
+
// Everything else inside it stays ignored (gitignore: an ignored dir's contents are ignored even
|
|
65
|
+
// when a single file is force-tracked).
|
|
66
|
+
const walk = (dir: string, parentIgnored: boolean) => {
|
|
67
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
68
|
+
const abs = join(dir, entry.name)
|
|
69
|
+
const key = toKey(relative(root, abs))
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
if (SKIP_DIRS.has(entry.name)) continue
|
|
72
|
+
const dirIgnored = parentIgnored || (!!ignore && ignore(key, true))
|
|
73
|
+
if (dirIgnored && !isTracked(key) && !hasTrackedUnder(key)) { ignored.push(key + "/"); continue }
|
|
74
|
+
dirs.push(key)
|
|
75
|
+
walk(abs, dirIgnored)
|
|
76
|
+
} else if (entry.isFile()) {
|
|
77
|
+
if (SKIP_FILES.has(entry.name)) continue
|
|
78
|
+
const fileIgnored = parentIgnored || (!!ignore && ignore(key, false))
|
|
79
|
+
if (fileIgnored && !isTracked(key)) { ignored.push(key); continue }
|
|
80
|
+
files.push({ path: key, absPath: abs, sha: sha256(readFileSync(abs)) })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
walk(root, false)
|
|
85
|
+
return { files, dirs, ignored }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The file a project key ("/img/logo.png") points at on disk. Keys carry the canonical spelling of
|
|
90
|
+
* each name (toKey), so a file whose real name isn't canonical — an NBSP, a decomposed "й" on a
|
|
91
|
+
* byte-exact filesystem — is found by matching sanitized names in its folder.
|
|
92
|
+
*/
|
|
93
|
+
export const resolveLocalPath = (root: string, key: string): string | null => {
|
|
94
|
+
const direct = join(root, key.slice(1))
|
|
95
|
+
if (existsSync(direct)) return direct
|
|
96
|
+
let dir = root
|
|
97
|
+
try {
|
|
98
|
+
for (const name of key.slice(1).split("/")) {
|
|
99
|
+
const match = readdirSync(dir).find(entry => entry === name || sanitizeAssetName(entry) === name)
|
|
100
|
+
if (!match) return null
|
|
101
|
+
dir = join(dir, match)
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
return null // a segment that isn't a readable directory
|
|
105
|
+
}
|
|
106
|
+
return dir
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Scan honoring the project's `.lecodesignore` (untracked files only). The common entry point. */
|
|
110
|
+
export const scanProject = (root: string, manifest: Manifest): LocalScan =>
|
|
111
|
+
scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) })
|
|
112
|
+
|
|
113
|
+
export type Diff = {
|
|
114
|
+
added: LocalFile[]
|
|
115
|
+
modified: LocalFile[]
|
|
116
|
+
deleted: string[]
|
|
117
|
+
/** newPath -> oldPath, detected by matching content of an added file to a deleted one. */
|
|
118
|
+
moves: Map<string, string>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Compare a local scan against the manifest's recorded state. */
|
|
122
|
+
export const diffLocal = (manifest: Manifest, scan: LocalScan): Diff => {
|
|
123
|
+
const added: LocalFile[] = []
|
|
124
|
+
const modified: LocalFile[] = []
|
|
125
|
+
const localByPath = new Map(scan.files.map(f => [f.path, f]))
|
|
126
|
+
|
|
127
|
+
for (const file of scan.files) {
|
|
128
|
+
const known = manifest.files[file.path]
|
|
129
|
+
if (!known) added.push(file)
|
|
130
|
+
else if (known.sha !== file.sha) modified.push(file)
|
|
131
|
+
}
|
|
132
|
+
const deleted = Object.keys(manifest.files).filter(path => !localByPath.has(path))
|
|
133
|
+
|
|
134
|
+
// Pair each added file with a deleted file of identical content -> a move/rename.
|
|
135
|
+
const moves = new Map<string, string>()
|
|
136
|
+
const claimed = new Set<string>()
|
|
137
|
+
for (const file of added) {
|
|
138
|
+
const from = deleted.find(path => !claimed.has(path) && manifest.files[path].sha === file.sha)
|
|
139
|
+
if (from) { moves.set(file.path, from); claimed.add(from) }
|
|
140
|
+
}
|
|
141
|
+
return { added, modified, deleted, moves }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const hasChanges = (diff: Diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { dirname, join } from "node:path"
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
|
|
3
|
+
import { CliError } from "./util"
|
|
4
|
+
|
|
5
|
+
/* The per-checkout state in <root>/.lecodes/manifest.json: which remote project this is, the
|
|
6
|
+
* head we last synced from (for drift detection), and a path -> { id, type, sha } map used to
|
|
7
|
+
* diff local edits against on push. */
|
|
8
|
+
|
|
9
|
+
export type FileKind = "text" | "shader" | "scene" | "resource"
|
|
10
|
+
|
|
11
|
+
export type ManifestFile = { id: number, type: FileKind, sha: string }
|
|
12
|
+
|
|
13
|
+
export type Manifest = {
|
|
14
|
+
uuid: string
|
|
15
|
+
apiUrl: string
|
|
16
|
+
name: string
|
|
17
|
+
baseHeadId: number | null
|
|
18
|
+
files: Record<string, ManifestFile>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const LECODES_DIR = ".lecodes"
|
|
22
|
+
const manifestPath = (root: string) => join(root, LECODES_DIR, "manifest.json")
|
|
23
|
+
|
|
24
|
+
/** Walk up from `start` to find the project root (the dir holding .lecodes/manifest.json). */
|
|
25
|
+
export const findProjectRoot = (start: string): string => {
|
|
26
|
+
let dir = start
|
|
27
|
+
while (true) {
|
|
28
|
+
if (existsSync(manifestPath(dir))) return dir
|
|
29
|
+
const parent = dirname(dir)
|
|
30
|
+
if (parent === dir) throw new CliError("Not inside a lecodes project (no .lecodes/manifest.json found).")
|
|
31
|
+
dir = parent
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const readManifest = (root: string): Manifest => {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(manifestPath(root), "utf8")) as Manifest
|
|
38
|
+
} catch {
|
|
39
|
+
throw new CliError("Could not read .lecodes/manifest.json — is this a lecodes project?")
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const writeManifest = (root: string, manifest: Manifest) => {
|
|
44
|
+
mkdirSync(join(root, LECODES_DIR), { recursive: true })
|
|
45
|
+
writeFileSync(manifestPath(root), JSON.stringify(manifest, null, 2))
|
|
46
|
+
}
|
package/src/matcTool.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
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 matc (material compiler) — the tool behind LOCAL shader compile: `lecodes dev`
|
|
9
|
+
* compiles a project's .mat files into the per-platform .filamat artifact family with it
|
|
10
|
+
* (src/compile/shaders.ts), instead of requiring a push so the server compiles them.
|
|
11
|
+
*
|
|
12
|
+
* The binary is NOT shipped with the CLI (most projects have no custom shaders). It downloads on
|
|
13
|
+
* first use from the same S3 release bucket as the desktop renderer (product "matc" in
|
|
14
|
+
* releases/versions.json — published by scripts/release/release-windows.ps1 / release-ubuntu.sh
|
|
15
|
+
* from the desktop build's in-tree filament host tools, so it always matches the engines the
|
|
16
|
+
* hosts ship). Cache: ~/.lecodes/matc/matc-<version>/. LECODES_MATC overrides resolution entirely
|
|
17
|
+
* (local dev: point it at packages/desktop/build/creator-gl/filament/tools/matc/matc.exe).
|
|
18
|
+
*
|
|
19
|
+
* No version pin: a filamat's compatibility is tied to the vendored Filament, which changes
|
|
20
|
+
* rarely — resolution takes the newest cached copy, else downloads `latest`. `lecodes shaders
|
|
21
|
+
* update` moves an existing cache forward after an engine upgrade.
|
|
22
|
+
*
|
|
23
|
+
* Platforms: windows-x64, linux-x64, macos-arm64. On Windows/Linux matc rides the DESKTOP build
|
|
24
|
+
* (filament's host tools build in-tree there). macOS has no such tree — release-macos.sh builds the
|
|
25
|
+
* app from the vendored xcframeworks — so its matc comes from creator-gl's own host-tool build
|
|
26
|
+
* (`out/cmake-release/filament/tools/matc`), which is the same filament tree those xcframeworks are
|
|
27
|
+
* compiled from. Same guarantee, different source; see scripts/release/release-macos.sh.
|
|
28
|
+
*
|
|
29
|
+
* macos-x64 is deliberately absent: the host tools are built for the machine doing the release, and
|
|
30
|
+
* only Apple Silicon is built today (release-macos.sh skips Intel for the same reason).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const matcName = () => (process.platform === "win32" ? "matc.exe" : "matc")
|
|
34
|
+
|
|
35
|
+
const platformKey = (): string => {
|
|
36
|
+
if (process.platform === "win32") return "windows-x64"
|
|
37
|
+
if (process.platform === "linux") return "linux-x64"
|
|
38
|
+
if (process.platform === "darwin") {
|
|
39
|
+
// Intel Macs have no published matc (see the platform note above). Rosetta would run an
|
|
40
|
+
// arm64 binary anyway, but publishing a key we never build is how a download turns into a
|
|
41
|
+
// corrupt-archive error instead of a readable one.
|
|
42
|
+
if (process.arch !== "arm64") {
|
|
43
|
+
throw new CliError(
|
|
44
|
+
"matc isn't published for Intel macOS — set LECODES_MATC to a locally built matc " +
|
|
45
|
+
"(creator-gl/out/cmake-release/filament/tools/matc), or push the project once so shaders " +
|
|
46
|
+
"compile on the server.",
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
return "macos-arm64"
|
|
50
|
+
}
|
|
51
|
+
throw new CliError(`matc isn't published for ${process.platform}.`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const cacheRoot = () => join(homedir(), ".lecodes", "matc")
|
|
55
|
+
|
|
56
|
+
/** Locate the matc binary inside a cache/stage dir (the archive may or may not nest a folder). */
|
|
57
|
+
const findMatc = (dir: string): string | null => {
|
|
58
|
+
const name = matcName()
|
|
59
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
60
|
+
const p = join(dir, entry.name)
|
|
61
|
+
if (entry.isFile() && entry.name === name) return p
|
|
62
|
+
if (entry.isDirectory()) { const found = findMatc(p); if (found) return found }
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Versions with a working matc binary in the cache, newest first. */
|
|
68
|
+
export const installedMatcVersions = (): string[] => {
|
|
69
|
+
try {
|
|
70
|
+
return readdirSync(cacheRoot(), { withFileTypes: true })
|
|
71
|
+
.filter((e) => e.isDirectory() && e.name.startsWith("matc-"))
|
|
72
|
+
.map((e) => e.name.slice("matc-".length))
|
|
73
|
+
.filter((v) => findMatc(join(cacheRoot(), `matc-${v}`)) !== null)
|
|
74
|
+
.sort(semverNewer)
|
|
75
|
+
} catch {
|
|
76
|
+
return []
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const fetchMatcManifest = (): Promise<{ latest: string, versions: ManifestVersion[] }> =>
|
|
81
|
+
fetchReleaseProduct("matc", "matc (shader compiler)")
|
|
82
|
+
|
|
83
|
+
const downloadMatc = async (version: string, versions: ManifestVersion[], dest: string): Promise<void> => {
|
|
84
|
+
const key = platformKey()
|
|
85
|
+
const file = versions.find((v) => v.version === version)?.files[key]
|
|
86
|
+
if (!file) {
|
|
87
|
+
throw new CliError(
|
|
88
|
+
`matc ${version} [${key}] isn't published — push the project once so shaders compile on the ` +
|
|
89
|
+
`server, or set LECODES_MATC to a locally built matc.`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
note(`Downloading the shader compiler (matc ${version}, ${(file.size / 1024 / 1024).toFixed(1)} MB)…`)
|
|
93
|
+
await downloadReleaseArchive(file, {
|
|
94
|
+
cacheRoot: cacheRoot(),
|
|
95
|
+
dest,
|
|
96
|
+
validate: (dir) => (findMatc(dir) ? null : "The downloaded archive doesn't contain the matc binary."),
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The matc binary WITHOUT touching the network: LECODES_MATC override → newest cached version →
|
|
101
|
+
* null. `lecodes shaders` status uses this (a status command must not download). */
|
|
102
|
+
export const resolveMatcExeCached = (): string | null => {
|
|
103
|
+
const override = process.env.LECODES_MATC
|
|
104
|
+
if (override) {
|
|
105
|
+
if (!existsSync(override)) throw new CliError(`LECODES_MATC points at "${override}" which doesn't exist.`)
|
|
106
|
+
return resolve(override)
|
|
107
|
+
}
|
|
108
|
+
const cached = installedMatcVersions()[0]
|
|
109
|
+
return cached ? findMatc(join(cacheRoot(), `matc-${cached}`)) : null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The matc binary: LECODES_MATC override → newest cached version → download the latest release.
|
|
113
|
+
* Throws CliError when unavailable (callers degrade to the push-once server-compile warning). */
|
|
114
|
+
export const resolveMatcExe = async (): Promise<string> => {
|
|
115
|
+
const cached = resolveMatcExeCached()
|
|
116
|
+
if (cached) return cached
|
|
117
|
+
const { latest, versions } = await fetchMatcManifest()
|
|
118
|
+
const dir = join(cacheRoot(), `matc-${latest}`)
|
|
119
|
+
await downloadMatc(latest, versions, dir)
|
|
120
|
+
const exe = findMatc(dir)
|
|
121
|
+
if (!exe) throw new CliError(`matc cache at ${dir} is broken — delete the folder and retry.`)
|
|
122
|
+
return exe
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** `lecodes shaders update`: download the newest published matc (cached versions stay; the newest
|
|
126
|
+
* wins resolution). Run it after an engine upgrade so local shader artifacts match the hosts. */
|
|
127
|
+
export const updateMatc = async (): Promise<void> => {
|
|
128
|
+
const { latest, versions } = await fetchMatcManifest()
|
|
129
|
+
const before = installedMatcVersions()[0]
|
|
130
|
+
const dir = join(cacheRoot(), `matc-${latest}`)
|
|
131
|
+
if (existsSync(dir) && findMatc(dir) !== null) {
|
|
132
|
+
note(`Already up to date: matc ${latest}.`)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
await downloadMatc(latest, versions, dir)
|
|
136
|
+
note(before ? `matc updated to ${latest} (was ${before}).` : `matc ${latest} installed.`)
|
|
137
|
+
}
|