lecodes-cli 0.20.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +56 -57
  2. package/dist/index.js +5918 -5020
  3. package/package.json +7 -5
  4. package/runtime/materials/decal-relief.mat +12 -1
  5. package/runtime/materials/decal.mat +17 -3
  6. package/runtime/materials/lightmap-baked-lite.mat +6 -1
  7. package/runtime/materials/lightmap-baked.mat +6 -1
  8. package/runtime/materials/lightmap.mat +3 -0
  9. package/runtime/scene-harness.json +1 -1
  10. package/runtime/sdk-types.json +1 -1
  11. package/runtime/web/assets/createViewerLite-DHqTFhWB.js +866 -0
  12. package/runtime/web/assets/{index-BMt7AnC5.js → index-Q06c6oHx.js} +1 -1
  13. package/runtime/web/embed.js +1 -1
  14. package/src/api.ts +1 -302
  15. package/src/cli/args.ts +37 -0
  16. package/src/cli/command.ts +113 -0
  17. package/src/cli/errors.ts +13 -0
  18. package/src/cli/help.ts +133 -0
  19. package/src/cli/index.ts +7 -0
  20. package/src/cli/output.ts +102 -0
  21. package/src/cli/run.ts +106 -0
  22. package/src/commands/{appAndroid.ts → app/android.ts} +451 -42
  23. package/src/commands/{appDesktop.ts → app/desktop.ts} +63 -32
  24. package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +24 -18
  25. package/src/commands/{appIcon.ts → app/icon.ts} +2 -2
  26. package/src/commands/{app.ts → app/index.ts} +397 -289
  27. package/src/commands/{appShared.ts → app/shared.ts} +30 -39
  28. package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +62 -28
  29. package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
  30. package/src/commands/{appTemplates.ts → app/templates/ios.ts} +1 -1
  31. package/src/commands/assets.ts +23 -21
  32. package/src/commands/clone.ts +48 -51
  33. package/src/commands/compile.ts +62 -88
  34. package/src/commands/create.ts +46 -47
  35. package/src/commands/design/comments.ts +361 -0
  36. package/src/commands/design/context.ts +101 -0
  37. package/src/commands/design/index.ts +391 -0
  38. package/src/commands/design/snapshot.ts +134 -0
  39. package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
  40. package/src/commands/desktop.ts +90 -82
  41. package/src/commands/dev.ts +201 -176
  42. package/src/commands/diff.ts +39 -51
  43. package/src/commands/index.ts +60 -0
  44. package/src/commands/{init.ts → init/index.ts} +199 -191
  45. package/src/commands/install.ts +80 -67
  46. package/src/commands/lightmap.ts +203 -171
  47. package/src/commands/link.ts +87 -94
  48. package/src/commands/login.ts +25 -23
  49. package/src/commands/navmesh.ts +168 -138
  50. package/src/commands/pn.ts +201 -244
  51. package/src/commands/pull.ts +49 -56
  52. package/src/commands/push.ts +48 -54
  53. package/src/commands/render.ts +158 -99
  54. package/src/commands/scene.ts +58 -55
  55. package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
  56. package/src/commands/{shadersNew.ts → shaders/new.ts} +2 -2
  57. package/src/commands/shared.ts +79 -0
  58. package/src/commands/status.ts +22 -26
  59. package/src/commands/test.ts +398 -371
  60. package/src/commands/thumbs.ts +185 -175
  61. package/src/commands/update/index.ts +226 -0
  62. package/src/commands/{types.ts → update/types.ts} +31 -22
  63. package/src/compile/collect.ts +5 -5
  64. package/src/compile/collectLocal.ts +3 -3
  65. package/src/compile/designCompile.ts +3 -3
  66. package/src/compile/headlessBundle.ts +133 -129
  67. package/src/compile/projectCompile.ts +2 -2
  68. package/src/compile/sceneCompile.ts +10 -8
  69. package/src/compile/screenEntry.ts +123 -127
  70. package/src/compile/shaders.ts +3 -3
  71. package/src/dev/androidDev.ts +1 -1
  72. package/src/dev/devServer.ts +3 -3
  73. package/src/dev/webRunner.ts +1 -1
  74. package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +1 -1
  75. package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +6 -5
  76. package/src/{desktopScript.ts → hosts/desktopScript.ts} +1 -1
  77. package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
  78. package/src/{matcTool.ts → hosts/matcTool.ts} +1 -1
  79. package/src/{peerInstall.ts → hosts/peerInstall.ts} +2 -2
  80. package/src/{releases.ts → hosts/releases.ts} +1 -1
  81. package/src/index.ts +32 -480
  82. package/src/platform/api.ts +302 -0
  83. package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
  84. package/src/{config.ts → platform/config.ts} +1 -1
  85. package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
  86. package/src/{projectEnv.ts → project/env.ts} +33 -2
  87. package/src/{localFiles.ts → project/localFiles.ts} +1 -1
  88. package/src/{manifest.ts → project/manifest.ts} +1 -1
  89. package/src/{project.ts → project/materialize.ts} +2 -2
  90. package/src/project/paths.ts +20 -0
  91. package/src/{textDiff.ts → project/textDiff.ts} +1 -1
  92. package/src/{types.ts → project/types.ts} +0 -0
  93. package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
  94. package/src/commands/design.ts +0 -846
  95. package/src/commands/update.ts +0 -211
  96. package/src/util.ts +0 -146
  97. /package/src/commands/{projectTemplates.ts → init/templates.ts} +0 -0
  98. /package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +0 -0
  99. /package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +0 -0
  100. /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
  101. /package/src/{lecodes-renderer.d.ts → declarations/lecodes-renderer.d.ts} +0 -0
  102. /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
  103. /package/src/{peers.ts → hosts/peers.ts} +0 -0
  104. /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
  105. /package/src/{ignore.ts → project/ignore.ts} +0 -0
@@ -1,15 +1,15 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"
2
- import { basename, dirname, join, resolve } from "node:path"
3
- import { findProjectRoot, readManifest, type Manifest } from "../manifest"
2
+ import { basename, dirname, join, relative, resolve } from "node:path"
3
+ import { CliError, bool, c, defineCommand, log, note, num, warnErr } from "../cli"
4
4
  import { compileSceneEditor } from "../compile/sceneCompile"
5
5
  import { localShaderArtifact } from "../compile/shaders"
6
- import { openBrowser } from "../browserAuth"
7
- import { CliError, c, flagBool, flagStr, log, note, warnErr, type Args } from "../util"
8
- import { loadPeer } from "../peerInstall"
6
+ import { loadPeer } from "../hosts/peerInstall"
7
+ import { openBrowser } from "../platform/browserAuth"
8
+ import { findProjectRoot, readManifest, type Manifest } from "../project/manifest"
9
9
 
10
10
  /*
11
- * `lecodes scene [path]` — the 3D scene editor (lecodes-tools/packages/lecodes-3d-editor) hosted on a local project,
12
- * exactly the way `lecodes design` hosts the design board. Starts scene-editor's node:http server,
11
+ * `lecodes scene [path]` — the 3D scene editor (lecodes-tools/packages/lecodes-3d-editor) hosted on a
12
+ * local project, the way `lecodes design` hosts the design board. Starts the editor's node:http server,
13
13
  * injects the CLI's compile closure (chisel + the vendored SDK + a vendored harness), and opens the
14
14
  * browser. `.scene.ts` files are edited visually (tree + inspector) and written back to disk;
15
15
  * external (IDE) edits hot-reload over SSE.
@@ -25,6 +25,9 @@ type SceneContext = {
25
25
  /** Null in standalone mode: the scene lives outside any cloned lecodes project. */
26
26
  manifest: Manifest | null
27
27
  name: string
28
+ /** The `.scene.ts` the positional arg named, as a project path ("./src/map.scene.ts") — the
29
+ * editor opens on it (`?scene=`) instead of the project's first scene file. */
30
+ scene: string | null
28
31
  }
29
32
 
30
33
  /** The nearest ancestor (inclusive) holding an `app.json` — a local project's root — or null. */
@@ -42,14 +45,10 @@ const findAppRoot = (start: string): string | null => {
42
45
  * app.json, else the directory itself. `lecodes thumbs` resolves its root this way — its
43
46
  * positional args are targets, not the root, so it can't go through getContext. */
44
47
  export const projectRootOf = (dir: string): string => {
45
- try {
46
- return findProjectRoot(dir)
47
- } catch {
48
- return findAppRoot(dir) ?? dir
49
- }
48
+ try { return findProjectRoot(dir) } catch { return findAppRoot(dir) ?? dir }
50
49
  }
51
50
 
52
- const getContext = (args: Args): SceneContext => {
51
+ const getContext = (pathArg: string | undefined): SceneContext => {
53
52
  let root: string
54
53
  let manifest: Manifest | null
55
54
  try {
@@ -62,7 +61,6 @@ const getContext = (args: Args): SceneContext => {
62
61
  // `app.json`; only without one does a scene path fall back to its containing dir.
63
62
  root = process.cwd()
64
63
  manifest = null
65
- const pathArg = args._[0]
66
64
  if (pathArg) {
67
65
  const abs = resolve(process.cwd(), pathArg)
68
66
  if (existsSync(abs) && statSync(abs).isDirectory()) root = abs
@@ -72,23 +70,25 @@ const getContext = (args: Args): SceneContext => {
72
70
  root = findAppRoot(root) ?? root
73
71
  }
74
72
  }
75
- return { root, manifest, name: manifest?.name ?? basename(root) }
73
+ // a `.scene.ts` arg (either mode) picks the file the editor opens on
74
+ let scene: string | null = null
75
+ if (pathArg?.endsWith(".scene.ts")) {
76
+ const abs = resolve(process.cwd(), pathArg)
77
+ const rel = relative(root, abs).replace(/\\/g, "/")
78
+ if (existsSync(abs) && !rel.startsWith("..")) scene = "./" + rel
79
+ }
80
+ return { root, manifest, name: manifest?.name ?? basename(root), scene }
76
81
  }
77
82
 
78
83
  /** Register the scene server as a project-scoped MCP server so Claude Code auto-discovers its
79
- * scene_state / check_scene tools. Merges into an existing .mcp.json (never clobbers a broken one).
80
- * Ported from `lecodes design init`'s writer; keyed "lecodes-scene". */
81
- const writeMcpConfig = (root: string, port: number) => {
84
+ * scene_state / check_scene tools. Merges into an existing .mcp.json (never clobbers a broken one). */
85
+ const writeMcpConfig = (root: string, port: number): void => {
82
86
  const path = join(root, ".mcp.json")
83
87
  const entry = { type: "http", url: `http://localhost:${port}/mcp` }
84
88
  let config: { mcpServers?: Record<string, unknown> } = {}
85
89
  if (existsSync(path)) {
86
- try {
87
- config = JSON.parse(readFileSync(path, "utf8"))
88
- } catch {
89
- warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.")
90
- return
91
- }
90
+ try { config = JSON.parse(readFileSync(path, "utf8")) }
91
+ catch { warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand."); return }
92
92
  }
93
93
  config.mcpServers ??= {}
94
94
  const existing = config.mcpServers["lecodes-scene"] as { url?: string } | undefined
@@ -98,7 +98,7 @@ const writeMcpConfig = (root: string, port: number) => {
98
98
  note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`)
99
99
  }
100
100
 
101
- /** Recursively find the first-level count of `.scene.ts` files under root (skip node_modules/dotdirs). */
101
+ /** How many `.scene.ts` files sit under root (node_modules / dot-dirs skipped). */
102
102
  const countScenes = (dir: string): number => {
103
103
  let n = 0
104
104
  for (const name of readdirSync(dir)) {
@@ -110,35 +110,38 @@ const countScenes = (dir: string): number => {
110
110
  return n
111
111
  }
112
112
 
113
- export const scene = async (args: Args) => {
114
- const ctx = getContext(args)
115
-
116
- const sceneServer = await loadPeer<typeof import("lecodes-3d-editor/server")>("lecodes-3d-editor", "server", { for: "lecodes scene" })
113
+ export default defineCommand({
114
+ name: "scene",
115
+ summary: "The scene editor: edit a .scene.ts visually (tree, inspector, gizmo)",
116
+ usage: "[path]",
117
+ description: "Needs the optional lecodes-3d-editor package (installed on first use). Works inside a cloned project or standalone in any folder; `path` is a folder or a .scene.ts to host. Registers an MCP server in .mcp.json so Claude Code can call scene_state / check_scene.",
118
+ flags: {
119
+ port: num("editor-server port", { default: DEFAULT_PORT }),
120
+ "no-open": bool("don't open the browser"),
121
+ "no-mcp": bool("don't register the MCP server in .mcp.json"),
122
+ },
123
+ examples: ["lecodes scene", "lecodes scene src/forest.scene.ts --no-open"],
124
+ run: async ({ args, flags }) => {
125
+ const ctx = getContext(args[0])
126
+ const sceneServer = await loadPeer<typeof import("lecodes-3d-editor/server")>("lecodes-3d-editor", "server", { for: "lecodes scene" })
127
+ if (countScenes(ctx.root) === 0) warnErr(`No .scene.ts file under ${ctx.root} yet — add one and the editor will pick it up.`)
117
128
 
118
- const port = Number(flagStr(args, "port")) || DEFAULT_PORT
129
+ const server = await sceneServer.startSceneServer({
130
+ root: ctx.root,
131
+ port: flags.port,
132
+ log: note,
133
+ compile: (scenePath: string) => compileSceneEditor({ root: ctx.root, manifest: ctx.manifest, name: ctx.name, scenePath }),
134
+ // live material patches load a shader's compiled artifact (matc'd for the Rendered mode,
135
+ // the lite JSON otherwise) from the local shader cache
136
+ shaderArtifact: (shaderText, platform) => localShaderArtifact(ctx.root, shaderText, platform),
137
+ })
138
+ if (!flags["no-mcp"]) writeMcpConfig(ctx.root, flags.port)
119
139
 
120
- if (countScenes(ctx.root) === 0) {
121
- warnErr(`No .scene.ts file under ${ctx.root} yet — add one and the editor will pick it up.`)
122
- }
123
-
124
- const server = await sceneServer.startSceneServer({
125
- root: ctx.root,
126
- port,
127
- log: note,
128
- compile: (scenePath: string) => compileSceneEditor({
129
- root: ctx.root, manifest: ctx.manifest, name: ctx.name, scenePath,
130
- }),
131
- // live material patches load a shader's compiled artifact (matc'd for the Rendered mode,
132
- // the lite JSON otherwise) from the local shader cache
133
- shaderArtifact: (shaderText, platform) => localShaderArtifact(ctx.root, shaderText, platform),
134
- })
135
-
136
- // Let Claude Code reach the running server over MCP (scene_state / check_scene), unless opted out.
137
- if (!flagBool(args, "no-mcp")) writeMcpConfig(ctx.root, port)
138
-
139
- log(`${c.bold("LeCodes Scene Editor")} — ${ctx.name}`)
140
- log(`Editor: ${c.bold(server.url)}`)
141
- log(`MCP: ${c.bold(server.url + "/mcp")}`)
142
- log(`Watching: ${ctx.root} (scenes hot-reload on save; Ctrl+C to stop)`)
143
- if (!flagBool(args, "no-open")) openBrowser(server.url)
144
- }
140
+ log(`${c.bold("LeCodes Scene Editor")} — ${ctx.name}`)
141
+ const editorUrl = ctx.scene ? `${server.url}/?scene=${encodeURIComponent(ctx.scene.replace(/^\.\//, ""))}` : server.url
142
+ log(`Editor: ${c.bold(editorUrl)}`)
143
+ log(`MCP: ${c.bold(server.url + "/mcp")}`)
144
+ log(`Watching: ${ctx.root} (scenes hot-reload on save; Ctrl+C to stop)`)
145
+ if (!flags["no-open"]) openBrowser(editorUrl)
146
+ },
147
+ })
@@ -1,15 +1,15 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { relative } from "node:path"
3
- import { findProjectRoot, readManifest, type Manifest } from "../manifest"
4
- import { collectLocalEntries } from "../compile/collectLocal"
3
+ import { collectLocalEntries } from "../../compile/collectLocal"
5
4
  import {
6
5
  allShaderPlatforms, applyLocalShaders, matcIdentity, shaderCompiledPlatforms, shaderIsCompiled,
7
6
  shadersCacheDir,
8
- } from "../compile/shaders"
9
- import { fetchMatcManifest, installedMatcVersions, resolveMatcExe, resolveMatcExeCached, updateMatc } from "../matcTool"
10
- import { semverNewer } from "../releases"
11
- import { CliError, c, flagStr, log, note, warnErr, type Args } from "../util"
12
- import { engineMaterialNames, writeNewShader } from "./shadersNew"
7
+ } from "../../compile/shaders"
8
+ import { fetchMatcManifest, installedMatcVersions, resolveMatcExe, resolveMatcExeCached, updateMatc } from "../../hosts/matcTool"
9
+ import { semverNewer } from "../../hosts/releases"
10
+ import { CliError, UsageError, bool, c, defineCommand, log, note, str, warnErr } from "../../cli"
11
+ import { localProject } from "../shared"
12
+ import { engineMaterialNames, writeNewShader } from "./new"
13
13
  import { parseShaderSchema, type AnyShaderPlatform } from "lecodes-sdk/compile"
14
14
 
15
15
  /*
@@ -21,7 +21,7 @@ import { parseShaderSchema, type AnyShaderPlatform } from "lecodes-sdk/compile"
21
21
  * update download the newest published matc (run after an engine upgrade)
22
22
  * new <name> a fresh .mat + its .material.ts asset in the current dir (--dir elsewhere): the
23
23
  * starter template, or `--ref lit|unlit|particles|…` = a copy of that engine source
24
- * to start from (shadersNew.ts); --force overwrites
24
+ * to start from (new.ts); --force overwrites
25
25
  * matc (Filament's material compiler) caches in ~/.lecodes/matc/; LECODES_MATC overrides it.
26
26
  *
27
27
  * A shader compiles to one artifact per target: `opengl` + `metal` (mobile profile, Android/Apple),
@@ -34,20 +34,12 @@ import { parseShaderSchema, type AnyShaderPlatform } from "lecodes-sdk/compile"
34
34
 
35
35
  /** The project's shader entries from the working tree (works with or without a server manifest). */
36
36
  const collectShaders = () => {
37
- let root: string
38
- let manifest: Manifest | null
39
- try {
40
- root = findProjectRoot(process.cwd())
41
- manifest = readManifest(root)
42
- } catch {
43
- root = process.cwd()
44
- manifest = null
45
- }
37
+ const { root, manifest } = localProject()
46
38
  const { entries } = collectLocalEntries(root, manifest, "", { localShaders: true })
47
39
  return { root, entries, shaders: entries.filter((e) => e.type === "shader" && e.absPath) }
48
40
  }
49
41
 
50
- const status = async () => {
42
+ const status = async (): Promise<void> => {
51
43
  const { root, shaders } = collectShaders()
52
44
 
53
45
  const installed = installedMatcVersions()
@@ -104,8 +96,7 @@ const status = async () => {
104
96
 
105
97
  /** Parse `--platform a,b` into a validated target list. A typo must fail loudly: silently compiling
106
98
  * nothing would look like success and leave the cache short of what the caller asked for. */
107
- const parsePlatforms = (args: Args): AnyShaderPlatform[] | undefined => {
108
- const raw = flagStr(args, "platform", "platforms")
99
+ const parsePlatforms = (raw: string | undefined): AnyShaderPlatform[] | undefined => {
109
100
  if (raw === undefined) return undefined
110
101
  const known = allShaderPlatforms()
111
102
  const asked = raw.split(",").map((p) => p.trim()).filter(Boolean)
@@ -119,11 +110,10 @@ const parsePlatforms = (args: Args): AnyShaderPlatform[] | undefined => {
119
110
 
120
111
  /** Compile every project shader into the local cache — what `lecodes dev` does lazily, run
121
112
  * explicitly (e.g. to pre-warm before going offline, or to see all compile errors at once).
122
- * `--platform` narrows the targets; compiling is incremental per artifact, so a narrow run now and
123
- * a full one later add up rather than redoing each other's work. */
124
- const compile = async (args: Args) => {
125
- const platforms = parsePlatforms(args)
126
- const { root, entries, shaders } = collectShaders()
113
+ * `platforms` narrows the targets; compiling is incremental per artifact, so a narrow run now and
114
+ * a full one later add up rather than redoing each other's work. `lecodes update` runs it too. */
115
+ export const compileShaders = async (platforms?: AnyShaderPlatform[]): Promise<void> => {
116
+ const { root, shaders } = collectShaders()
127
117
  if (shaders.length === 0) {
128
118
  note("No .mat shaders in this project.")
129
119
  return
@@ -153,21 +143,50 @@ const compile = async (args: Args) => {
153
143
  note(`${shaders.length} shader${shaders.length === 1 ? "" : "s"} compiled${scope} → ${shadersCacheDir(root)}`)
154
144
  }
155
145
 
156
- const newShader = (args: Args) => {
157
- const name = args._[1]
158
- if (!name) throw new CliError(`lecodes shaders new <name> [--ref ${engineMaterialNames().join("|") || "lit"}] [--dir <folder>] [--force]`)
159
- const res = writeNewShader({ name, ref: flagStr(args, "ref"), dir: flagStr(args, "dir"), force: args.flags.force === true })
160
- const rel = (p: string) => relative(process.cwd(), p)
161
- log(`${rel(res.mat)} ${c.dim(res.ref ? `(from the engine's ${res.ref}.mat)` : "(the starter template)")}`)
162
- log(`${rel(res.asset)} ${c.dim("(defineMaterial asset — a scene takes it as `material:`)")}`)
163
- note(`next: edit it, then \`lecodes shaders compile\` (or just \`lecodes dev\`); in code: Material.load(asset('./${name.replace(/\.mat$/, "")}.mat'))`)
164
- }
146
+ const compile = defineCommand({
147
+ name: "compile",
148
+ summary: "Compile every shader into .lecodes/shaders (the pass `lecodes dev` runs on the fly)",
149
+ description: "Downloads matc on first use (~/.lecodes/matc). Every target is compiled by default (opengl, metal, desktop, desktop-vulkan, webgl-lite) because one bundle serves every host; --platform narrows it when you know the target. Incremental per artifact.",
150
+ flags: {
151
+ platform: str("only these targets, comma-separated (opengl, metal, desktop, desktop-vulkan, webgl-lite)", { value: "<a,b>" }),
152
+ },
153
+ examples: ["lecodes shaders compile", "lecodes shaders compile --platform desktop"],
154
+ run: ({ flags }) => compileShaders(parsePlatforms(flags.platform)),
155
+ })
165
156
 
166
- export const shaders = async (args: Args) => {
167
- const sub = args._[0]
168
- if (sub === undefined) return status()
169
- if (sub === "compile") return compile(args)
170
- if (sub === "update") return updateMatc()
171
- if (sub === "new") return newShader(args)
172
- throw new CliError(`Unknown subcommand "shaders ${sub}". Use \`lecodes shaders\` (status), \`lecodes shaders compile [--platform a,b]\`, \`lecodes shaders new <name> [--ref lit]\` or \`lecodes shaders update\`.`)
173
- }
157
+ const update = defineCommand({
158
+ name: "update",
159
+ summary: "Download the newest published matc (run after an engine upgrade)",
160
+ run: () => updateMatc(),
161
+ })
162
+
163
+ const newShader = defineCommand({
164
+ name: "new",
165
+ summary: "Write a fresh .mat + .material.ts: the starter, or a copy of an engine material",
166
+ usage: "<name>",
167
+ description: "The starter is an unlit material with an annotated parameter of each kind. `--ref lit|unlit|particles|…` copies the engine's own source instead, renamed after the file — the way to get vertex colours, an emissive term or a second map onto an otherwise standard material without touching the engine. A <name>.material.ts asset (defineMaterial with the declared parameters) goes next to it.",
168
+ flags: {
169
+ ref: str("start from the engine's material of that name (lit, unlit, particles, …)", { value: "<name>" }),
170
+ dir: str("write into this folder instead of the current dir", { value: "<folder>" }),
171
+ force: bool("overwrite existing files"),
172
+ },
173
+ examples: ["lecodes shaders new hologram", "lecodes shaders new rim-light --ref lit --dir assets/shaders"],
174
+ run: ({ args, flags }) => {
175
+ const name = args[0]
176
+ if (!name) throw new UsageError(`Which shader? lecodes shaders new <name> [--ref ${engineMaterialNames().join("|") || "lit"}] [--dir <folder>] [--force]`)
177
+ const res = writeNewShader({ name, ref: flags.ref, dir: flags.dir, force: flags.force })
178
+ const rel = (p: string) => relative(process.cwd(), p)
179
+ log(`${rel(res.mat)} ${c.dim(res.ref ? `(from the engine's ${res.ref}.mat)` : "(the starter template)")}`)
180
+ log(`${rel(res.asset)} ${c.dim("(defineMaterial asset — a scene takes it as `material:`)")}`)
181
+ note(`next: edit it, then \`lecodes shaders compile\` (or just \`lecodes dev\`); in code: Material.load(asset('./${name.replace(/\.mat$/, "")}.mat'))`)
182
+ },
183
+ })
184
+
185
+ export default defineCommand({
186
+ name: "shaders",
187
+ summary: "The project's custom shaders (.mat): list, compile, update matc, new",
188
+ description: "Bare `lecodes shaders` lists the project's shaders with their compile state and the matc status. matc (Filament's material compiler) caches in ~/.lecodes/matc; LECODES_MATC points at a local build.",
189
+ commands: [compile, update, newShader],
190
+ examples: ["lecodes shaders", "lecodes shaders compile", "lecodes shaders new hologram --ref lit"],
191
+ run: () => status(),
192
+ })
@@ -12,8 +12,8 @@
12
12
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
13
13
  import { basename, join, relative, resolve } from "node:path"
14
14
  import { parseShaderSchema } from "lecodes-sdk/compile"
15
- import { distRoot } from "../distRoot"
16
- import { CliError } from "../util"
15
+ import { distRoot } from "../../hosts/distRoot"
16
+ import { CliError } from "../../cli"
17
17
 
18
18
  /** where the engine's .mat sources are, or null when this CLI carries none */
19
19
  export const engineMaterialsDir = (): string | null => {
@@ -0,0 +1,79 @@
1
+ import { basename } from "node:path"
2
+ import { bool, str, type FlagValues } from "../cli"
3
+ import { loadConfig, requireApiUrl, requireToken } from "../platform/config"
4
+ import { findProjectRoot, readManifest, type Manifest } from "../project/manifest"
5
+
6
+ /*
7
+ * What several commands share: the flag groups (spread into a command's `flags:`) and the
8
+ * "where am I / who am I" lookups every platform command starts with.
9
+ */
10
+
11
+ // ---- flag groups -----------------------------------------------------------------------------
12
+
13
+ /** How the project is compiled — `compile`, `dev`, `render`, `test`, `desktop run`, `app sync` … */
14
+ export const bundleFlags = {
15
+ entry: str("compile from this entry instead of the detected one", { value: "<path>" }),
16
+ "public-url": str("resolve assets from this URL prefix (default: the API origin / the working tree)", { value: "<url>" }),
17
+ "remote-assets": bool("desktop hosts: load asset() resources from the server instead of the working tree"),
18
+ "no-header": bool("omit the metadata header"),
19
+ "no-minify": bool("keep the bundle readable (no compress / mangle)"),
20
+ "no-sourcemap": bool("don't generate / inline the source map"),
21
+ }
22
+
23
+ /** `-o` for commands whose result is a file. */
24
+ export const outFlag = { out: str("write to this file instead of stdout", { alias: "o", value: "<file>" }) }
25
+
26
+ /** The compiler's view of the bundle flags (see compile.ts `buildBundle`). */
27
+ export type BundleOptions = {
28
+ entry?: string
29
+ publicUrl?: string
30
+ remoteAssets?: boolean
31
+ header?: boolean
32
+ minify?: boolean
33
+ sourcemap?: boolean
34
+ }
35
+
36
+ export const bundleOptions = (flags: FlagValues<typeof bundleFlags>): BundleOptions => ({
37
+ entry: flags.entry,
38
+ publicUrl: flags["public-url"],
39
+ remoteAssets: flags["remote-assets"],
40
+ header: !flags["no-header"],
41
+ // Both default on; `false` only when the flag is present, so the SDK's own defaults stand.
42
+ minify: flags["no-minify"] ? false : undefined,
43
+ sourcemap: flags["no-sourcemap"] ? false : undefined,
44
+ })
45
+
46
+ // ---- where / who -----------------------------------------------------------------------------
47
+
48
+ export type Session = { apiUrl: string, token: string }
49
+
50
+ /** The logged-in session (api origin + token) or a "run `lecodes login`" error. */
51
+ export const session = (): Session => {
52
+ const config = loadConfig()
53
+ return { apiUrl: requireApiUrl(config), token: requireToken(config) }
54
+ }
55
+
56
+ export type LinkedProject = Session & { root: string, manifest: Manifest }
57
+
58
+ /** A server-backed checkout: the project root (found upward from the cwd), its manifest and the session. */
59
+ export const linkedProject = (): LinkedProject => {
60
+ const root = findProjectRoot(process.cwd())
61
+ return { root, manifest: readManifest(root), ...session() }
62
+ }
63
+
64
+ export type LocalProject = { root: string, manifest: Manifest | null, name: string }
65
+
66
+ /** Any project: a linked checkout, else the current folder standing alone (no login needed). */
67
+ export const localProject = (): LocalProject => {
68
+ try {
69
+ const root = findProjectRoot(process.cwd())
70
+ const manifest = readManifest(root)
71
+ return { root, manifest, name: manifest.name }
72
+ } catch {
73
+ const root = process.cwd()
74
+ return { root, manifest: null, name: basename(root) }
75
+ }
76
+ }
77
+
78
+ /** A folder name for a project: lower-case, dashes, never empty. */
79
+ export const slugOf = (name: string): string => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project"
@@ -1,29 +1,25 @@
1
- import { findProjectRoot, readManifest } from "../manifest"
2
- import { diffLocal, hasChanges, scanProject } from "../localFiles"
3
- import { c, info, log, type Args } from "../util"
1
+ import { c, defineCommand, info, log } from "../cli"
2
+ import { diffLocal, hasChanges, scanProject } from "../project/localFiles"
3
+ import { linkedProject } from "./shared"
4
4
 
5
- /* `lecodes status` — show local changes vs the last clone/pull/push. */
6
- export const status = async (_args: Args) => {
7
- const root = findProjectRoot(process.cwd())
8
- const manifest = readManifest(root)
9
- const scan = scanProject(root, manifest)
10
- const diff = diffLocal(manifest, scan)
5
+ /* `lecodes status` — local changes vs the last clone / pull / push. */
6
+ export default defineCommand({
7
+ name: "status",
8
+ summary: "Show local changes vs the last sync",
9
+ run: () => {
10
+ const { root, manifest } = linkedProject()
11
+ const scan = scanProject(root, manifest)
12
+ const diff = diffLocal(manifest, scan)
11
13
 
12
- info(`Project ${c.bold(manifest.name)} (${manifest.uuid})`)
13
- const ignoredNote = scan.ignored.length ? c.dim(` (${scan.ignored.length} ignored)`) : ""
14
- if (!hasChanges(diff)) {
15
- info(c.dim("Working tree clean — nothing to push.") + ignoredNote)
16
- return
17
- }
14
+ info(`Project ${c.bold(manifest.name)} (${manifest.uuid})`)
15
+ const ignoredNote = scan.ignored.length ? c.dim(` (${scan.ignored.length} ignored)`) : ""
16
+ if (!hasChanges(diff)) { info(c.dim("Working tree clean — nothing to push.") + ignoredNote); return }
18
17
 
19
- for (const f of diff.added) {
20
- if (!diff.moves.has(f.path)) log(c.green(` added ${f.path}`))
21
- }
22
- for (const f of diff.modified) log(c.yellow(` modified ${f.path}`))
23
- for (const [to, from] of diff.moves) log(c.cyan(` moved ${from} → ${to}`))
24
- const movedFrom = new Set(diff.moves.values())
25
- for (const path of diff.deleted) {
26
- if (!movedFrom.has(path)) log(c.red(` deleted ${path}`))
27
- }
28
- if (scan.ignored.length) info(c.dim(` ${scan.ignored.length} ignored (.lecodesignore)`))
29
- }
18
+ for (const f of diff.added) if (!diff.moves.has(f.path)) log(c.green(` added ${f.path}`))
19
+ for (const f of diff.modified) log(c.yellow(` modified ${f.path}`))
20
+ for (const [to, from] of diff.moves) log(c.cyan(` moved ${from} → ${to}`))
21
+ const movedFrom = new Set(diff.moves.values())
22
+ for (const path of diff.deleted) if (!movedFrom.has(path)) log(c.red(` deleted ${path}`))
23
+ if (scan.ignored.length) info(c.dim(` ${scan.ignored.length} ignored (.lecodesignore)`))
24
+ },
25
+ })