lecodes-cli 0.19.0 → 0.20.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 +1 -0
- package/dist/index.js +1069 -396
- package/package.json +14 -13
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
- package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/runtime/web/assets/basis-C64VHDVD.js +1 -0
- package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web/assets/createViewerLite-BvHobVhp.js +852 -0
- package/runtime/web/assets/draco-BiISTFcR.js +118 -0
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-CfNCCBjN.js +2 -0
- package/runtime/web/assets/mapViewImpl-B2JcES8l.js +810 -0
- package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +8 -0
- package/runtime/web/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
- package/runtime/web/assets/worker-Caf-yYEI.js +2 -0
- package/runtime/web/embed.html +32 -0
- package/runtime/web/embed.js +171 -0
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +896 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +339 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +542 -0
- package/src/commands/appTemplates.ts +907 -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 +127 -0
- package/src/commands/dev.ts +236 -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 +130 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +416 -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 +371 -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 +545 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +333 -0
- package/src/dev/webRunner.ts +22 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +494 -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,542 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process"
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { homedir } from "node:os"
|
|
4
|
+
import { dirname, join, resolve } from "node:path"
|
|
5
|
+
import { findProjectRoot } from "../manifest"
|
|
6
|
+
import { DEFAULT_IGNORE_FILE } from "../ignore"
|
|
7
|
+
import { CliError, c, flagStr, log, note, warn, type Args } from "../util"
|
|
8
|
+
|
|
9
|
+
/* Shared plumbing for the `lecodes app` platform arms (app.ts = iOS + dispatch,
|
|
10
|
+
* appAndroid.ts = Android). Everything here moved verbatim out of app.ts when the Android
|
|
11
|
+
* arm landed — behavior for iOS is unchanged; the SDK helpers just grew a repo parameter
|
|
12
|
+
* (lecodes-ios-sdk vs lecodes-android-sdk) and resolvePlugin a platform one. */
|
|
13
|
+
|
|
14
|
+
/** First-party plugin registry: id → GitHub repo holding plugins/<id>/. Third-party plugins
|
|
15
|
+
* use a `github:owner/repo[@tag]` spec in app.json instead (manifest at the repo root). */
|
|
16
|
+
export const FIRST_PARTY_PLUGINS: Record<string, string> = {
|
|
17
|
+
"camera": "letary/lecodes-plugins",
|
|
18
|
+
"qr-scanner": "letary/lecodes-plugins",
|
|
19
|
+
"map": "letary/lecodes-plugins",
|
|
20
|
+
"geolocation": "letary/lecodes-plugins",
|
|
21
|
+
"push": "letary/lecodes-plugins",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AppConfig {
|
|
25
|
+
name?: string
|
|
26
|
+
version?: string
|
|
27
|
+
/** Project-root-relative path of a square ≥1024px PNG/JPEG master. When set, every sync
|
|
28
|
+
* renders the platform icons from it (iOS AppIcon + Android adaptive/legacy mipmaps) and
|
|
29
|
+
* those files become CLI-owned like assets/. Absent = icons stay hand-managed; removing
|
|
30
|
+
* the key stops regeneration but deletes nothing. */
|
|
31
|
+
icon?: string
|
|
32
|
+
/** Icon backdrop ("#RGB"/"#RRGGBB", default white): the Android adaptive-icon background
|
|
33
|
+
* layer, and what a transparent master is flattened onto for the opacity-required iOS icon. */
|
|
34
|
+
iconBackground?: string
|
|
35
|
+
plugins?: Record<string, string>
|
|
36
|
+
/** `remote`: lecodesignore-style globs of resources to STREAM from the server instead of
|
|
37
|
+
* embedding in the app (server-backed projects only — a streamed asset needs a server URL). */
|
|
38
|
+
resources?: { remote?: string[] }
|
|
39
|
+
/** Over-the-air JS updates: where the shell checks for newer compiled code at launch
|
|
40
|
+
* (natively, before the JS engine — offline is a no-op, new code applies next launch).
|
|
41
|
+
* `true` = this project's live bundle on the platform (server-backed only); or an https
|
|
42
|
+
* URL to a compiled bundle on any hosting (string or `{ "url": … }`). Absent = no OTA.
|
|
43
|
+
* iOS-only for now — the Android runtime has no native updater yet. */
|
|
44
|
+
update?: boolean | string | { url?: string }
|
|
45
|
+
/** `variant`: SDK product — "auto" (default: sync picks the smallest one covering the engines
|
|
46
|
+
* the compiled bundle actually uses) or core|2d|3d|full to force one.
|
|
47
|
+
* `sdk`: native SDK version — an exact lecodes-ios-sdk release (init pins the newest one,
|
|
48
|
+
* `lecodes app update` moves it), or "latest" to re-resolve on every sync. */
|
|
49
|
+
ios?: { bundleId?: string, teamId?: string, variant?: string, buildNumber?: number, sdk?: string }
|
|
50
|
+
/** `applicationId`: the Android package name (init default: com.example.<slug>).
|
|
51
|
+
* `versionCode`: Play's monotonically increasing build number (versionName comes from the
|
|
52
|
+
* shared `version`). `sdk`: an exact lecodes-android-sdk release or "latest", like ios.sdk.
|
|
53
|
+
* `variant`: SDK artifact — "auto" (default: sync picks the smallest one covering the engines
|
|
54
|
+
* the compiled bundle actually uses) or core|2d|3d|full to force one, like ios.variant
|
|
55
|
+
* (needs lecodes-android-sdk >= 1.3.0 — older releases ship only the full AAR).
|
|
56
|
+
* `signing`: keystore METADATA only — the store/key passwords live in android/
|
|
57
|
+
* keystore.properties (gitignored) or LECODES_ANDROID_* env vars, never in app.json. */
|
|
58
|
+
android?: {
|
|
59
|
+
applicationId?: string, versionCode?: number, sdk?: string, variant?: string,
|
|
60
|
+
signing?: { storeFile?: string, keyAlias?: string },
|
|
61
|
+
}
|
|
62
|
+
/** Desktop host (packages/desktop). `window` sizes are LOGICAL units — the host multiplies by
|
|
63
|
+
* the display scale. `entry` picks a per-target entry file (default: the detected project
|
|
64
|
+
* entry) — see docs/desktop-target-plan.md. */
|
|
65
|
+
desktop?: {
|
|
66
|
+
entry?: string,
|
|
67
|
+
window?: DesktopWindowConfig,
|
|
68
|
+
/** Which desktop host binary to run: "gl" (default) or "vulkan" (Windows only, host >= 1.4.0).
|
|
69
|
+
* A BUILD/DEV-TIME host selection, not a runtime switch — the two are separate binaries with
|
|
70
|
+
* their own shaders and different features (the Vulkan one has no 2D engine and no native-view
|
|
71
|
+
* plugins). Overridable per run with `--renderer` / LECODES_DESKTOP_RENDERER. */
|
|
72
|
+
renderer?: "gl" | "vulkan",
|
|
73
|
+
/** 3D buffer scale vs the window (0.25–1, host ≥1.4.0). The UI always renders at native
|
|
74
|
+
* pixels; only the 3D scene is rendered smaller and upscaled by the compositor. Multiplies
|
|
75
|
+
* the scene's own `renderScale` (SceneOptions). Headless render/test ignore it. */
|
|
76
|
+
renderScale?: number,
|
|
77
|
+
/** 3D buffer cap in px, e.g. [1920, 1080]: on a bigger window/monitor the 3D renders at most
|
|
78
|
+
* this size (aspect kept) — the per-machine answer to "fullscreen on a 4K display is slow". */
|
|
79
|
+
maxRenderSize?: [number, number],
|
|
80
|
+
/** The macOS .app build (`lecodes app build desktop` on a Mac) — identity + signing +
|
|
81
|
+
* notarization. See appDesktopMac.ts for the whole story. */
|
|
82
|
+
macos?: MacDesktopConfig,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** app.json `desktop.macos`. Only NAMES live here (identity, keychain profile) — the certificate
|
|
87
|
+
* and the notarization password stay in the keychain. */
|
|
88
|
+
export interface MacDesktopConfig {
|
|
89
|
+
/** CFBundleIdentifier (default codes.le.app.<slug>). Reverse-DNS, stable across releases. */
|
|
90
|
+
bundleId?: string
|
|
91
|
+
/** CFBundleVersion (default 1). Bump per upload; the top-level `version` is the marketing one. */
|
|
92
|
+
buildNumber?: number
|
|
93
|
+
/** LSApplicationCategoryType, e.g. "public.app-category.games". */
|
|
94
|
+
category?: string
|
|
95
|
+
/** `identity`: a "Developer ID Application: Name (TEAM)" codesign identity, or "adhoc" (local
|
|
96
|
+
* only — refused on every other Mac). Absent/"auto": the keychain's Developer ID cert, else
|
|
97
|
+
* ad-hoc. Overridden by --sign / LECODES_MACOS_SIGN_ID. */
|
|
98
|
+
signing?: { identity?: string }
|
|
99
|
+
/** `profile`: a `xcrun notarytool store-credentials <name>` keychain profile. With it the build
|
|
100
|
+
* is notarized + stapled (needs a Developer ID identity). Overridden by --notarize /
|
|
101
|
+
* LECODES_NOTARY_PROFILE; --no-notarize skips for a local build. */
|
|
102
|
+
notarize?: { profile?: string }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface DesktopWindowConfig {
|
|
106
|
+
/** Initial client size, logical units (host default: 1280×720). */
|
|
107
|
+
width?: number
|
|
108
|
+
height?: number
|
|
109
|
+
minWidth?: number
|
|
110
|
+
minHeight?: number
|
|
111
|
+
/** false → fixed-size window (default: resizable). */
|
|
112
|
+
resizable?: boolean
|
|
113
|
+
/** Window title (default: the top-level `name`). */
|
|
114
|
+
title?: string
|
|
115
|
+
/** Borderless fullscreen on the primary monitor (kiosks). `width`/`height` stay the
|
|
116
|
+
* design/dev frame; headless render/test always run windowed (the render contract). */
|
|
117
|
+
fullscreen?: boolean
|
|
118
|
+
/** Display scale (host ≥1.3.0). "system" (default): the OS display scale. A number (0.5–4):
|
|
119
|
+
* a fixed density — the window opens at `width×scale` physical px. "fit": derived at launch
|
|
120
|
+
* from the design frame vs the monitor (fullscreen) / work area, so a 1080p-designed app
|
|
121
|
+
* runs density-2 on a 4K display and scales DOWN on smaller screens. Headless render/test
|
|
122
|
+
* always force scale 1 (the render contract). */
|
|
123
|
+
scale?: number | "fit" | "system"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** app.json wherever a command needs the project-level desktop/window config. Tolerant of a
|
|
127
|
+
* missing or broken file — standalone projects (no app.json) must keep working everywhere
|
|
128
|
+
* this is consulted. */
|
|
129
|
+
export const readAppConfigAt = (root: string): AppConfig | null =>
|
|
130
|
+
readJson<AppConfig>(join(root, "app.json"))
|
|
131
|
+
|
|
132
|
+
/** The desktop host's own built-in fallback, mirrored where the CLI must fill a missing side. */
|
|
133
|
+
export const DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 }
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The renderer for this run: `--renderer` > LECODES_DESKTOP_RENDERER > app.json `desktop.renderer`
|
|
137
|
+
* > "gl". Resolve it ONCE per command and pass the result to both `resolveDesktopExe` and the
|
|
138
|
+
* shader staging — two independent reads of the config is how a build ends up with a Vulkan exe
|
|
139
|
+
* next to an OpenGL .filamat, which fails at the first material with a Filament abort.
|
|
140
|
+
*/
|
|
141
|
+
export const resolveDesktopRenderer = (root: string, args?: Args): "gl" | "vulkan" => {
|
|
142
|
+
const raw = (args ? flagStr(args, "renderer") : undefined)
|
|
143
|
+
?? process.env.LECODES_DESKTOP_RENDERER
|
|
144
|
+
?? readAppConfigAt(root)?.desktop?.renderer
|
|
145
|
+
if (raw === undefined) return "gl"
|
|
146
|
+
const value = String(raw).toLowerCase()
|
|
147
|
+
if (value === "gl" || value === "opengl") return "gl"
|
|
148
|
+
if (value === "vulkan" || value === "vk") {
|
|
149
|
+
// The macOS host is Metal, full stop. A project tuned for the Windows Vulkan host must still
|
|
150
|
+
// build/run on a Mac, so the setting is a no-op here rather than an error.
|
|
151
|
+
if (process.platform === "darwin") { note("desktop.renderer \"vulkan\" is Windows-only — the macOS host renders with Metal."); return "gl" }
|
|
152
|
+
return "vulkan"
|
|
153
|
+
}
|
|
154
|
+
throw new CliError(`Unknown desktop renderer "${raw}". Use "gl" or "vulkan".`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Project-level default viewport for HEADLESS render/test: the desktop window when the project
|
|
158
|
+
* declares desktop as its ONLY native target (a desktop-only project gets desktop-sized frames
|
|
159
|
+
* and flow tests for free). Mixed or mobile projects keep the phone default — headless frames
|
|
160
|
+
* exercise the phone UI there. */
|
|
161
|
+
export const desktopDefaultViewport = (root: string): { width: number, height: number } | null => {
|
|
162
|
+
const cfg = readAppConfigAt(root)
|
|
163
|
+
if (!cfg?.desktop || cfg.ios || cfg.android) return null
|
|
164
|
+
return {
|
|
165
|
+
width: cfg.desktop.window?.width ?? DESKTOP_DEFAULT_WINDOW.width,
|
|
166
|
+
height: cfg.desktop.window?.height ?? DESKTOP_DEFAULT_WINDOW.height,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A SwiftPM dependency a plugin's iOS target links (manifest ios.packages). */
|
|
171
|
+
export interface IosPackage {
|
|
172
|
+
url: string
|
|
173
|
+
/** Pinned release — the normal case (a plugin's sources compile against one API). */
|
|
174
|
+
exact?: string
|
|
175
|
+
/** Minimum release, SwiftPM picks the newest compatible major. Exclusive with `exact`. */
|
|
176
|
+
from?: string
|
|
177
|
+
/** Library products of that package the plugin target imports. */
|
|
178
|
+
products: string[]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** SwiftPM's package identity for a url: the last path segment without `.git`. */
|
|
182
|
+
export const swiftPackageName = (url: string): string =>
|
|
183
|
+
(url.replace(/\/+$/, "").split("/").pop() ?? url).replace(/\.git$/, "")
|
|
184
|
+
|
|
185
|
+
export interface PluginManifest {
|
|
186
|
+
id: string
|
|
187
|
+
version?: string
|
|
188
|
+
provides?: { views?: string[], services?: string[] }
|
|
189
|
+
ios?: {
|
|
190
|
+
sources?: string, module?: string, register?: string, minSdk?: string,
|
|
191
|
+
infoPlist?: Record<string, string>,
|
|
192
|
+
/** Entitlements the plugin's capability needs (plist-typed JSON values, arrays allowed —
|
|
193
|
+
* e.g. push's `{"aps-environment": "development"}`). Merged into App/App.entitlements
|
|
194
|
+
* by sync; the relay signs sends for Letary-published bundle ids with the shared team
|
|
195
|
+
* key, and the register route fail-fasts for anything outside that namespace. */
|
|
196
|
+
entitlements?: Record<string, unknown>,
|
|
197
|
+
/** Swift packages the vendored target links (e.g. the map plugin's maplibre-native). Each
|
|
198
|
+
* becomes a `.package(url:…)` in the generated LeCodesRuntime/Package.swift (exact or
|
|
199
|
+
* from — one of the two) and its `products` become `.product(name:package:)` deps of the
|
|
200
|
+
* plugin target. The package name is the URL's last path segment minus `.git`, as SwiftPM
|
|
201
|
+
* derives it. Two plugins naming the same url must agree on the version. */
|
|
202
|
+
packages?: IosPackage[],
|
|
203
|
+
} | null
|
|
204
|
+
android?: {
|
|
205
|
+
/** Dir vendored into the shell (default "android") — a Gradle-module-shaped tree minus
|
|
206
|
+
* build.gradle (sync generates that): src/main/AndroidManifest.xml carries the plugin's
|
|
207
|
+
* permissions/services (AGP's manifest merger is the infoPlist analog), libs/*.aar become
|
|
208
|
+
* local file deps, consumer-rules.pro becomes the module's consumer ProGuard rules. */
|
|
209
|
+
sources?: string,
|
|
210
|
+
/** Gradle module name (kebab-case) → :plugins:<module> in the shell. */
|
|
211
|
+
module?: string,
|
|
212
|
+
/** Fully-qualified Kotlin object with `fun register(engine: LecodesEngine, context: Context)`;
|
|
213
|
+
* its package doubles as the generated module's AGP namespace. */
|
|
214
|
+
register?: string,
|
|
215
|
+
/** Minimum lecodes-android-sdk release (NOT Android's minSdkVersion — the OS floor comes
|
|
216
|
+
* from the SDK itself). Sync hard-errors below it, like ios.minSdk. */
|
|
217
|
+
minSdk?: string,
|
|
218
|
+
/** Maven coordinates for the generated build.gradle. A "platform:" prefix maps to
|
|
219
|
+
* implementation(platform(…)) — BoMs; versionless coords are only valid under one. */
|
|
220
|
+
dependencies?: string[],
|
|
221
|
+
/** App-level Gradle plugin ids the shell must apply (e.g. com.google.gms.google-services).
|
|
222
|
+
* Bare ids resolve through the CLI's known-version table; "id:version" pins explicitly. */
|
|
223
|
+
gradlePlugins?: string[],
|
|
224
|
+
} | null
|
|
225
|
+
/** Desktop host (packages/desktop): a PREBUILT shared library per OS/arch behind the C ABI in
|
|
226
|
+
* `packages/desktop/plugin-sdk/lecodes_plugin.h` — no host rebuild, no source vendoring.
|
|
227
|
+
* `library` keys are `${process.platform}-${process.arch}` ("win32-x64", "linux-x64",
|
|
228
|
+
* "linux-arm64"), values plugin-dir-relative paths. `lecodes desktop run` / `render --desktop`
|
|
229
|
+
* forward the library's dir to the host (CREATOR_PLUGIN_DIRS); `app build desktop` stages the
|
|
230
|
+
* file into `<build>/plugins/`. A plugin without a build for the current target is skipped. */
|
|
231
|
+
desktop?: {
|
|
232
|
+
library?: Record<string, string>,
|
|
233
|
+
} | null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface LockEntry { repo: string, tag: string, commit: string, version: string }
|
|
237
|
+
|
|
238
|
+
export const readJson = <T>(path: string): T | null => {
|
|
239
|
+
if (!existsSync(path)) return null
|
|
240
|
+
// BOM-tolerant: Notepad and PowerShell write UTF-8 with a BOM, and app.json is hand-edited.
|
|
241
|
+
try { return JSON.parse(readFileSync(path, "utf8").replace(/^\uFEFF/, "")) as T } catch {
|
|
242
|
+
throw new CliError(`Invalid JSON in ${path}`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export const writeJson = (path: string, value: unknown) =>
|
|
247
|
+
writeFileSync(path, JSON.stringify(value, null, 2) + "\n")
|
|
248
|
+
|
|
249
|
+
export const projectRoot = (): string => {
|
|
250
|
+
try { return findProjectRoot(process.cwd()) } catch { return process.cwd() }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const git = (cwd: string, ...args: string[]): string =>
|
|
254
|
+
execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim()
|
|
255
|
+
|
|
256
|
+
export const slugify = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app"
|
|
257
|
+
|
|
258
|
+
/** Run a tool, capturing output; on failure surface the tail (xcodebuild/gradle logs are huge). */
|
|
259
|
+
export const runTool = (cwd: string, cmd: string, args: string[]): string => {
|
|
260
|
+
try {
|
|
261
|
+
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 })
|
|
262
|
+
} catch (e) {
|
|
263
|
+
const err = e as { stdout?: string, stderr?: string }
|
|
264
|
+
const tail = ((err.stdout ?? "") + "\n" + (err.stderr ?? "")).split("\n").filter(l => l.trim()).slice(-25).join("\n")
|
|
265
|
+
throw new CliError(`${cmd} ${args[0] ?? ""} failed:\n${tail}`)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Add a shell dir (ios/, android/) to .lecodesignore — shells are never pushed to LeCodes. */
|
|
270
|
+
export const ensureShellIgnored = (root: string, dir: string) => {
|
|
271
|
+
const ignorePath = join(root, ".lecodesignore")
|
|
272
|
+
const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : DEFAULT_IGNORE_FILE
|
|
273
|
+
if (new RegExp(`^${dir}\\/$`, "m").test(ignore)) return
|
|
274
|
+
const comment = ignore.includes("# Native app shells")
|
|
275
|
+
? ""
|
|
276
|
+
: "# Native app shells — generated by `lecodes app init`, never pushed to LeCodes.\n"
|
|
277
|
+
writeFileSync(ignorePath, ignore.replace(/\n*$/, "\n") + "\n" + comment + `${dir}/\n`)
|
|
278
|
+
log(`${c.green("updated")} .lecodesignore (+ ${dir}/)`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* ----------------------------------- SDK variants ----------------------------------- */
|
|
282
|
+
|
|
283
|
+
// Both native SDKs ship the same variant matrix (full / 3d / 2d / core), so the detection and
|
|
284
|
+
// resolution logic is shared between the iOS (app.ts) and Android (appAndroid.ts) syncs.
|
|
285
|
+
export const SDK_VARIANTS = ["core", "2d", "3d", "full"] as const
|
|
286
|
+
type SdkVariantShared = (typeof SDK_VARIANTS)[number]
|
|
287
|
+
|
|
288
|
+
/** Engine signals from a compiled bundle's header — the same lines buildHeader emits.
|
|
289
|
+
* No `// gl: disable` line → the 3D engine is used (eagerly, or lazily via `gl: defer`);
|
|
290
|
+
* `// 2d: enable` → the 2D engine is used. */
|
|
291
|
+
export const detectEngines = (bundleJs: string): { gl: boolean, g2: boolean } => {
|
|
292
|
+
const lines: string[] = []
|
|
293
|
+
for (const line of bundleJs.split("\n")) {
|
|
294
|
+
if (!line.startsWith("//")) break
|
|
295
|
+
lines.push(line)
|
|
296
|
+
}
|
|
297
|
+
const header = lines.join("\n")
|
|
298
|
+
return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Refuse a Vulkan desktop build for a project that uses the 2D engine. The Vulkan host is built with
|
|
303
|
+
* CREATOR_PKG_2D=OFF (creator-2d is sokol-on-GL, with no Vulkan twin), so those scenes don't render
|
|
304
|
+
* — a blank window at runtime with nothing in the log to explain it. Fail at build time instead,
|
|
305
|
+
* where the fix ("use the default renderer") is one line away.
|
|
306
|
+
*
|
|
307
|
+
* Detection is the same `// 2d: enable` bundle header `lecodes app sync` picks its SDK variant from,
|
|
308
|
+
* so it can't drift from what the engine actually needs.
|
|
309
|
+
*/
|
|
310
|
+
export const assertRendererSupportsBundle = (renderer: "gl" | "vulkan", bundleJs: string): void => {
|
|
311
|
+
if (renderer !== "vulkan") return
|
|
312
|
+
if (!detectEngines(bundleJs).g2) return
|
|
313
|
+
throw new CliError(
|
|
314
|
+
"This project uses the 2D engine, which the Vulkan desktop host doesn't include " +
|
|
315
|
+
"(creator-2d renders through OpenGL and has no Vulkan path yet).\n" +
|
|
316
|
+
" Build with the default renderer: drop `desktop.renderer` from app.json, or pass `--renderer gl`.",
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The SDK product for a shell: the smallest variant covering the engines the compiled bundle
|
|
321
|
+
* actually uses, unless <configKey> (ios.variant / android.variant) forces one — then warn when
|
|
322
|
+
* the forced product can't run the project (missing engine = those scenes silently no-op on
|
|
323
|
+
* device). */
|
|
324
|
+
export const resolveVariant = (requested: string, bundleJs: string, configKey = "ios.variant"): { variant: SdkVariantShared, detected: SdkVariantShared } => {
|
|
325
|
+
const uses = detectEngines(bundleJs)
|
|
326
|
+
const detected: SdkVariantShared = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core"
|
|
327
|
+
if (requested === "auto") return { variant: detected, detected }
|
|
328
|
+
const variant = requested as SdkVariantShared
|
|
329
|
+
const missing = [
|
|
330
|
+
uses.gl && variant !== "3d" && variant !== "full" ? "3D" : null,
|
|
331
|
+
uses.g2 && variant !== "2d" && variant !== "full" ? "2D" : null,
|
|
332
|
+
].filter(Boolean)
|
|
333
|
+
if (missing.length > 0) {
|
|
334
|
+
warn(`project uses the ${missing.join(" + ")} engine but ${configKey} forces "${variant}" — those scenes won't run on device; remove ${configKey} to auto-select "${detected}".`)
|
|
335
|
+
}
|
|
336
|
+
return { variant, detected }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* ----------------------------------- SDK version ------------------------------------ */
|
|
340
|
+
|
|
341
|
+
export const VERSION_RE = /^\d+\.\d+\.\d+$/
|
|
342
|
+
|
|
343
|
+
/** Numeric semver-ish compare (only x.y.z tags are published). */
|
|
344
|
+
export const cmpVersion = (a: string, b: string): number => {
|
|
345
|
+
const pa = a.split(".").map(Number)
|
|
346
|
+
const pb = b.split(".").map(Number)
|
|
347
|
+
for (let i = 0; i < 3; i++) {
|
|
348
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0)
|
|
349
|
+
}
|
|
350
|
+
return 0
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** The newest published release of an SDK dist repo (prereleases excluded). Network — throws. */
|
|
354
|
+
export const fetchLatestSdkVersion = async (repo: string): Promise<string> => {
|
|
355
|
+
const url = `https://api.github.com/repos/${repo}/releases/latest`
|
|
356
|
+
const res = await fetch(url, { headers: { "accept": "application/vnd.github+json", "user-agent": "lecodes-cli" } })
|
|
357
|
+
if (!res.ok) throw new CliError(`Could not read the latest ${repo} release (HTTP ${res.status}) — ${url}`)
|
|
358
|
+
const tag = ((await res.json()) as { tag_name?: string }).tag_name?.replace(/^v/, "") ?? ""
|
|
359
|
+
if (!VERSION_RE.test(tag)) throw new CliError(`${repo} latest release has no x.y.z tag (got "${tag}")`)
|
|
360
|
+
return tag
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Resolve an app.json sdk value ("latest" or an exact release) to the concrete release the
|
|
364
|
+
* shell links; `fallback` is what "latest" degrades to when the network is unavailable. */
|
|
365
|
+
export const resolveSdkVersion = async (
|
|
366
|
+
pin: string, fallback: string | null,
|
|
367
|
+
opts: { repo: string, minVersion: string, configKey: string },
|
|
368
|
+
): Promise<string> => {
|
|
369
|
+
if (pin !== "latest") {
|
|
370
|
+
if (!VERSION_RE.test(pin)) {
|
|
371
|
+
throw new CliError(`app.json ${opts.configKey} must be "latest" or an exact x.y.z release (got "${pin}").`)
|
|
372
|
+
}
|
|
373
|
+
if (cmpVersion(pin, opts.minVersion) < 0) {
|
|
374
|
+
throw new CliError(`app.json ${opts.configKey} ${pin} is older than the ${opts.minVersion} these generated shells need.`)
|
|
375
|
+
}
|
|
376
|
+
return pin
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
return await fetchLatestSdkVersion(opts.repo)
|
|
380
|
+
} catch (e) {
|
|
381
|
+
const kept = fallback ?? opts.minVersion
|
|
382
|
+
warn(`Could not resolve the latest SDK (${(e as Error).message}) — staying on ${kept}.`)
|
|
383
|
+
return kept
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* ---------------------------------- plugin vendoring ---------------------------------- */
|
|
388
|
+
|
|
389
|
+
export interface ResolvedPlugin {
|
|
390
|
+
id: string
|
|
391
|
+
dir: string // directory containing lecodes-plugin.json
|
|
392
|
+
manifest: PluginManifest
|
|
393
|
+
lock: LockEntry
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Clone-or-update the plugin repo cache and check out the wanted commit (detached).
|
|
397
|
+
* `LECODES_PLUGINS_DIR` overrides with a local working tree — the plugin-development loop. */
|
|
398
|
+
export const ensureRepo = (repo: string, wantTag: string | null, lock: LockEntry | undefined): { dir: string, tag: string, commit: string } => {
|
|
399
|
+
const override = process.env.LECODES_PLUGINS_DIR
|
|
400
|
+
if (override) {
|
|
401
|
+
note(`Using local plugin checkout ${override} (LECODES_PLUGINS_DIR)`)
|
|
402
|
+
return { dir: resolve(override), tag: "local", commit: "local" }
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const cache = join(homedir(), ".lecodes", "plugins", repo.replace("/", "__"))
|
|
406
|
+
const url = `https://github.com/${repo}.git`
|
|
407
|
+
if (!existsSync(join(cache, ".git"))) {
|
|
408
|
+
mkdirSync(dirname(cache), { recursive: true })
|
|
409
|
+
note(`Cloning ${url}…`)
|
|
410
|
+
git(dirname(cache), "clone", "--quiet", url, cache)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// A lock pin wins; else an explicit @tag; else the latest v* tag (fetched fresh).
|
|
414
|
+
let tag = lock?.tag ?? wantTag
|
|
415
|
+
let commit = lock?.commit
|
|
416
|
+
if (commit) {
|
|
417
|
+
try { git(cache, "cat-file", "-e", `${commit}^{commit}`) } catch {
|
|
418
|
+
git(cache, "fetch", "--quiet", "--tags", "origin")
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
git(cache, "fetch", "--quiet", "--tags", "origin")
|
|
422
|
+
if (!tag) {
|
|
423
|
+
tag = git(cache, "tag", "--list", "v*", "--sort=-version:refname").split("\n")[0] ?? ""
|
|
424
|
+
if (!tag) throw new CliError(`${repo} has no v* version tags to install from`)
|
|
425
|
+
}
|
|
426
|
+
commit = git(cache, "rev-list", "-n", "1", tag)
|
|
427
|
+
}
|
|
428
|
+
git(cache, "checkout", "--quiet", "--detach", commit)
|
|
429
|
+
return { dir: cache, tag: tag ?? "unknown", commit }
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Resolve one app.json plugin entry to vendorable sources + manifest + lock pin. The platform
|
|
433
|
+
* being synced must be supported (its manifest branch non-null) — a hard error otherwise. */
|
|
434
|
+
export type PluginPlatform = "ios" | "android" | "desktop"
|
|
435
|
+
const PLUGIN_PLATFORM_LABEL: Record<PluginPlatform, string> = { ios: "iOS", android: "Android", desktop: "desktop" }
|
|
436
|
+
|
|
437
|
+
export function resolvePlugin(id: string, spec: string, lock: LockEntry | undefined, platform: PluginPlatform): ResolvedPlugin
|
|
438
|
+
export function resolvePlugin(id: string, spec: string, lock: LockEntry | undefined, platform: PluginPlatform, opts: { optional: true }): ResolvedPlugin | null
|
|
439
|
+
export function resolvePlugin(id: string, spec: string, lock: LockEntry | undefined, platform: PluginPlatform, opts?: { optional?: boolean }): ResolvedPlugin | null {
|
|
440
|
+
let repo: string
|
|
441
|
+
let subdir: string
|
|
442
|
+
let wantTag: string | null = null
|
|
443
|
+
|
|
444
|
+
if (spec.startsWith("github:")) {
|
|
445
|
+
const [repoPart, tagPart] = spec.slice("github:".length).split("@")
|
|
446
|
+
repo = repoPart
|
|
447
|
+
wantTag = tagPart ?? null
|
|
448
|
+
subdir = "" // third-party: manifest at the repo root
|
|
449
|
+
} else {
|
|
450
|
+
repo = FIRST_PARTY_PLUGINS[id]
|
|
451
|
+
if (!repo) {
|
|
452
|
+
throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use a github:owner/repo spec.`)
|
|
453
|
+
}
|
|
454
|
+
subdir = `plugins/${id}`
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const checkout = ensureRepo(repo, wantTag, lock?.repo === repo ? lock : undefined)
|
|
458
|
+
const dir = subdir ? join(checkout.dir, subdir) : checkout.dir
|
|
459
|
+
const manifest = readJson<PluginManifest>(join(dir, "lecodes-plugin.json"))
|
|
460
|
+
if (!manifest) throw new CliError(`Plugin "${id}": no lecodes-plugin.json in ${dir}`)
|
|
461
|
+
if (manifest.id !== id) throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`)
|
|
462
|
+
if (!manifest[platform]) {
|
|
463
|
+
if (opts?.optional) return null
|
|
464
|
+
throw new CliError(`Plugin "${id}" does not support ${PLUGIN_PLATFORM_LABEL[platform]} (manifest "${platform}" is null)`)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Namespacing: third-party channel names must be vendor-prefixed ("acme.thing").
|
|
468
|
+
if (spec.startsWith("github:")) {
|
|
469
|
+
const names = [...(manifest.provides?.views ?? []), ...(manifest.provides?.services ?? [])]
|
|
470
|
+
for (const name of names) {
|
|
471
|
+
if (!name.includes(".")) warn(`Plugin "${id}" provides bare channel name "${name}" — third-party names should be vendor-prefixed (e.g. "acme.${name}").`)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
id, dir, manifest,
|
|
477
|
+
lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" },
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Newest v* tag of a plugin repo, via the shared plugin cache. Null when it can't be read
|
|
482
|
+
* (no network, no tags, local override) — `app update` reports it and moves on. */
|
|
483
|
+
export const latestPluginTag = (repo: string): string | null => {
|
|
484
|
+
try { return ensureRepo(repo, null, undefined).tag } catch { return null }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* ---------------------------------- desktop native plugins ---------------------------------- */
|
|
488
|
+
|
|
489
|
+
/** The desktop host's plugin library target key (manifest `desktop.library` keys). */
|
|
490
|
+
export const desktopPluginTarget = (): string => `${process.platform}-${process.arch}`
|
|
491
|
+
|
|
492
|
+
const isDesktopPluginLibrary = (name: string): boolean => {
|
|
493
|
+
const n = name.toLowerCase()
|
|
494
|
+
return process.platform === "win32" ? n.endsWith(".dll") : n.endsWith(".so")
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Native plugin libraries for THIS project on THIS machine (packages/desktop/plugin-sdk/
|
|
498
|
+
* lecodes_plugin.h — runtime-loaded .dll/.so, no host rebuild):
|
|
499
|
+
* - every library in the project's own `plugins/` dir — the zero-manifest dev loop (build a
|
|
500
|
+
* DLL, drop it there, `lecodes desktop run`);
|
|
501
|
+
* - the `desktop.library[target]` of every app.json plugin whose manifest declares one
|
|
502
|
+
* (mobile-only plugins are skipped, not errors).
|
|
503
|
+
* `dirs` is what a run forwards to the host as CREATOR_PLUGIN_DIRS (the host scans them);
|
|
504
|
+
* `files` is what `app build desktop` copies into `<build>/plugins/`. `strict` turns a broken
|
|
505
|
+
* declaration into an error (builds); runs warn and go on without that plugin. */
|
|
506
|
+
export const desktopPluginLibraries = (root: string, opts: { strict?: boolean } = {}): { dirs: string[], files: string[] } => {
|
|
507
|
+
const dirs: string[] = []
|
|
508
|
+
const files: string[] = []
|
|
509
|
+
const local = join(root, "plugins")
|
|
510
|
+
if (existsSync(local) && statSync(local).isDirectory()) {
|
|
511
|
+
dirs.push(local)
|
|
512
|
+
for (const f of readdirSync(local)) if (isDesktopPluginLibrary(f)) files.push(join(local, f))
|
|
513
|
+
}
|
|
514
|
+
const config = readJson<AppConfig>(join(root, "app.json"))
|
|
515
|
+
const specs = Object.entries(config?.plugins ?? {})
|
|
516
|
+
if (specs.length === 0) return { dirs, files }
|
|
517
|
+
const lock = readJson<{ plugins?: Record<string, LockEntry> }>(join(root, "plugins.lock.json"))?.plugins ?? {}
|
|
518
|
+
const target = desktopPluginTarget()
|
|
519
|
+
for (const [id, spec] of specs) {
|
|
520
|
+
let plugin: ResolvedPlugin | null
|
|
521
|
+
try {
|
|
522
|
+
plugin = resolvePlugin(id, spec, lock[id], "desktop", { optional: true })
|
|
523
|
+
} catch (e) {
|
|
524
|
+
if (opts.strict) throw e
|
|
525
|
+
warn(`Plugin "${id}": ${(e as Error).message} — skipped for this desktop run`)
|
|
526
|
+
continue
|
|
527
|
+
}
|
|
528
|
+
const rel = plugin?.manifest.desktop?.library?.[target]
|
|
529
|
+
if (!plugin || !rel) continue // mobile-only, or no build for this OS/arch
|
|
530
|
+
const abs = resolve(plugin.dir, rel)
|
|
531
|
+
if (!existsSync(abs)) {
|
|
532
|
+
const msg = `Plugin "${id}": desktop library ${abs} is missing (manifest desktop.library["${target}"])`
|
|
533
|
+
if (opts.strict) throw new CliError(msg)
|
|
534
|
+
warn(`${msg} — skipped for this desktop run`)
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
files.push(abs)
|
|
538
|
+
const dir = dirname(abs)
|
|
539
|
+
if (!dirs.includes(dir)) dirs.push(dir)
|
|
540
|
+
}
|
|
541
|
+
return { dirs, files }
|
|
542
|
+
}
|