lecodes-cli 0.20.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -57
- package/dist/index.js +11052 -7592
- package/package.json +14 -8
- package/runtime/materials/decal-relief.mat +12 -1
- package/runtime/materials/decal.mat +17 -3
- package/runtime/materials/lightmap.mat +117 -138
- package/runtime/materials/particles-quad.mat +6 -2
- package/runtime/materials/terrain-lightmap.mat +123 -0
- package/runtime/materials/terrain.mat +36 -55
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +5 -0
- package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
- package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
- package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
- package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
- package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
- package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
- package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
- package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
- package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
- package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
- package/runtime/web/assets/map-CyBxOSV0.js +1 -0
- package/runtime/web/assets/map-XzoOFt0E.js +2 -0
- package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
- package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
- package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
- package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
- package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
- package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
- package/runtime/web/index.html +36 -0
- package/runtime/web/player.js +21 -0
- package/runtime/web-page/index.html +41 -0
- package/runtime/web-page/page.css +1 -0
- package/runtime/web-page/page.js +31 -0
- package/src/api.ts +1 -302
- package/src/cli/args.ts +37 -0
- package/src/cli/command.ts +113 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/help.ts +158 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/output.ts +102 -0
- package/src/cli/run.ts +106 -0
- package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
- package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
- package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
- package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
- package/src/commands/{app.ts → app/index.ts} +400 -290
- package/src/commands/{appShared.ts → app/shared.ts} +43 -44
- package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
- package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
- package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
- package/src/commands/assets.ts +23 -21
- package/src/commands/clone.ts +48 -51
- package/src/commands/compile.ts +76 -89
- package/src/commands/create.ts +46 -47
- package/src/commands/design/comments.ts +361 -0
- package/src/commands/design/context.ts +101 -0
- package/src/commands/design/index.ts +391 -0
- package/src/commands/design/snapshot.ts +134 -0
- package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
- package/src/commands/desktop.ts +94 -82
- package/src/commands/dev.ts +260 -182
- package/src/commands/diff.ts +39 -51
- package/src/commands/index.ts +64 -0
- package/src/commands/{init.ts → init/index.ts} +195 -191
- package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
- package/src/commands/install.ts +80 -67
- package/src/commands/lightmap.ts +543 -252
- package/src/commands/link.ts +87 -94
- package/src/commands/login.ts +25 -23
- package/src/commands/navmesh.ts +169 -139
- package/src/commands/plugin.ts +46 -0
- package/src/commands/pn.ts +201 -244
- package/src/commands/pull.ts +49 -56
- package/src/commands/push.ts +48 -54
- package/src/commands/render.ts +176 -111
- package/src/commands/scene.ts +63 -55
- package/src/commands/serve.ts +58 -0
- package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
- package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
- package/src/commands/shared.ts +79 -0
- package/src/commands/status.ts +22 -26
- package/src/commands/test.ts +94 -63
- package/src/commands/thumbs.ts +185 -175
- package/src/commands/update/index.ts +226 -0
- package/src/commands/{types.ts → update/types.ts} +31 -22
- package/src/compile/collect.ts +5 -5
- package/src/compile/collectLocal.ts +3 -3
- package/src/compile/designCompile.ts +4 -4
- package/src/compile/headlessBundle.ts +16 -12
- package/src/compile/nativeStack.ts +1 -1
- package/src/compile/projectCompile.ts +12 -4
- package/src/compile/sceneCompile.ts +14 -26
- package/src/compile/screenEntry.ts +123 -127
- package/src/compile/shaders.ts +3 -3
- package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
- package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
- package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
- package/src/dev/androidDev.ts +1 -1
- package/src/dev/clientTemplates.ts +18 -14
- package/src/dev/devServer.ts +78 -41
- package/src/dev/iosDev.ts +180 -0
- package/src/dev/webRunner.ts +83 -15
- package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
- package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
- package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
- package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
- package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
- package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
- package/src/{peers.ts → hosts/peers.ts} +2 -2
- package/src/{releases.ts → hosts/releases.ts} +1 -1
- package/src/index.ts +35 -480
- package/src/platform/api.ts +302 -0
- package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
- package/src/{config.ts → platform/config.ts} +1 -1
- package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
- package/src/plugin/contract.ts +438 -0
- package/src/plugin/emitKotlin.ts +357 -0
- package/src/plugin/emitSdk.ts +259 -0
- package/src/plugin/emitSwift.ts +336 -0
- package/src/plugin/emitWeb.ts +383 -0
- package/src/plugin/generate.ts +157 -0
- package/src/plugin/lines.ts +28 -0
- package/src/plugin/lower.ts +231 -0
- package/src/plugin/manifest.ts +192 -0
- package/src/{projectEnv.ts → project/env.ts} +33 -2
- package/src/{ignore.ts → project/ignore.ts} +164 -163
- package/src/{localFiles.ts → project/localFiles.ts} +1 -1
- package/src/{manifest.ts → project/manifest.ts} +1 -1
- package/src/{project.ts → project/materialize.ts} +2 -2
- package/src/project/paths.ts +20 -0
- package/src/{textDiff.ts → project/textDiff.ts} +1 -1
- package/src/{types.ts → project/types.ts} +0 -0
- package/runtime/materials/lightmap-baked-lite.mat +0 -175
- package/runtime/materials/lightmap-baked.mat +0 -176
- package/runtime/scene-harness.json +0 -1
- package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/runtime/web/assets/basis-C64VHDVD.js +0 -1
- package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
- package/runtime/web/assets/draco-BiISTFcR.js +0 -118
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-BMt7AnC5.js +0 -2
- package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
- package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
- package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
- package/runtime/web/embed.html +0 -32
- package/runtime/web/embed.js +0 -171
- package/src/commands/design.ts +0 -846
- package/src/commands/update.ts +0 -211
- package/src/util.ts +0 -146
- /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
- /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
- /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { CliError, bool, c, defineCommand, log, note, num, str, warnErr, type FlagValues } from "../../cli"
|
|
4
|
+
import { compileDesignScreen } from "../../compile/designCompile"
|
|
5
|
+
import { loadPeer } from "../../hosts/peerInstall"
|
|
6
|
+
import { disableDesignShare, enableDesignShare, setDesignCommentsOpen } from "../../platform/api"
|
|
7
|
+
import { openBrowser } from "../../platform/browserAuth"
|
|
8
|
+
import { readMetaFrames } from "../../project/designMeta"
|
|
9
|
+
import { materializeTypesLocal, writeTsconfig } from "../../project/types"
|
|
10
|
+
import { session } from "../shared"
|
|
11
|
+
import comments, { startCommentSync } from "./comments"
|
|
12
|
+
import { DEFAULT_PORT, dirFlag, getContext, loadDesignServer, optionalPlatform, threadRef, type DesignContext } from "./context"
|
|
13
|
+
import snapshot from "./snapshot"
|
|
14
|
+
import { CLAUDE_MD, DESIGN_MACROS_DTS, HOME_SCREEN_TS, META_JSON, META_JSON_DESKTOP, README_HINT, SPEC_MD, TABS_TS, TOKENS_TS } from "./templates"
|
|
15
|
+
|
|
16
|
+
export { flushDesignComments, type CommentFlush } from "./comments"
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* `lecodes design` — the LeCodes Design canvas: prototype an app as live screens + a flow graph.
|
|
20
|
+
*
|
|
21
|
+
* design [serve] start the dev server + browser canvas (needs `lecodes-design`)
|
|
22
|
+
* design init scaffold the design folder (screens/, shared/, meta.json, spec.md, CLAUDE.md)
|
|
23
|
+
* design snapshot render screens headless to JSON/PNG (snapshot.ts, needs `lecodes-headless`)
|
|
24
|
+
* design arrange auto-layout the flow graph (--new: only place unplaced screens)
|
|
25
|
+
* design check static coherence: spec.md links, flow warnings, untagged screens
|
|
26
|
+
* design share toggle the public share link (design must be pushed first)
|
|
27
|
+
* design comments the board's review comments + add / reply / resolve / pull (comments.ts)
|
|
28
|
+
*
|
|
29
|
+
* The design folder (default `design/`) holds one file per screen (screens/<id>.ts default-exports
|
|
30
|
+
* a UIScreen), shared tokens/components (shared/), and meta.json — the human-owned graph of
|
|
31
|
+
* positions, descriptions and edges. Screens compile per-file through the normal project pipeline
|
|
32
|
+
* and render via canvas-ui, live in the browser and headless for AI feedback.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** First non-empty line, for one-line echoes of a comment body. */
|
|
36
|
+
const firstLine = (body: string, max = 64): string => {
|
|
37
|
+
const line = body.split("\n").find((l) => l.trim())?.trim() ?? ""
|
|
38
|
+
return line.length > max ? line.slice(0, max - 1) + "…" : line
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---- serve -------------------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
const serveFlags = {
|
|
44
|
+
...dirFlag,
|
|
45
|
+
port: num("dev-server port", { default: DEFAULT_PORT }),
|
|
46
|
+
"no-open": bool("don't open the browser"),
|
|
47
|
+
"no-sync": bool("don't reconcile comments with the platform while serving"),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const serve = async (flags: FlagValues<typeof serveFlags>): Promise<void> => {
|
|
51
|
+
const ctx = getContext(flags.dir)
|
|
52
|
+
const designServer = await loadDesignServer(ctx)
|
|
53
|
+
|
|
54
|
+
// The MCP render_screen tool renders headless through lecodes-headless, injected here so the
|
|
55
|
+
// design server keeps no dependency on it. Absent renderer → the tool returns an install hint.
|
|
56
|
+
// Opportunistic: never triggers an install from inside the MCP server (the tool reports the hint).
|
|
57
|
+
const renderer = await loadPeer<typeof import("lecodes-headless/headless")>("lecodes-headless", "headless", { for: "lecodes design (render_screen)", install: false })
|
|
58
|
+
const renderScreen = renderer
|
|
59
|
+
? async (id: string, format: "json" | "png" | "both", state?: string) => {
|
|
60
|
+
const js = await compileDesignScreen({
|
|
61
|
+
root: ctx.root, manifest: ctx.manifest, name: ctx.name, designDir: ctx.designDir, designKey: ctx.designKey, screenId: id, state, assets: "disk",
|
|
62
|
+
})
|
|
63
|
+
const frames = readMetaFrames(ctx.designDir)
|
|
64
|
+
const [width, height] = frames.screens[id]?.size ?? frames.defaultSize
|
|
65
|
+
const warnings: string[] = []
|
|
66
|
+
let json: string | undefined
|
|
67
|
+
let png: Buffer | undefined
|
|
68
|
+
if (format === "json" || format === "both") {
|
|
69
|
+
const res = await renderer.renderToJson(js, { width, height, localAssets: true })
|
|
70
|
+
json = res.text
|
|
71
|
+
warnings.push(...res.warnings)
|
|
72
|
+
}
|
|
73
|
+
if (format === "png" || format === "both") {
|
|
74
|
+
const res = await renderer.renderToPng(js, { width, height, localAssets: true, scale: 2 })
|
|
75
|
+
png = res.png
|
|
76
|
+
warnings.push(...res.warnings)
|
|
77
|
+
}
|
|
78
|
+
return { json, png, warnings }
|
|
79
|
+
}
|
|
80
|
+
: undefined
|
|
81
|
+
|
|
82
|
+
const server = await designServer.startDesignServer({
|
|
83
|
+
root: ctx.root,
|
|
84
|
+
designDir: ctx.designDir,
|
|
85
|
+
name: ctx.name,
|
|
86
|
+
port: flags.port,
|
|
87
|
+
log: note,
|
|
88
|
+
// Who owns the conversation: linked, the files mirror the platform's open threads and local
|
|
89
|
+
// writes queue for `lecodes push`; standalone, these files are the record.
|
|
90
|
+
linked: !!optionalPlatform(ctx),
|
|
91
|
+
compileScreen: async (id: string, state?: string) => ({
|
|
92
|
+
js: await compileDesignScreen({
|
|
93
|
+
root: ctx.root, manifest: ctx.manifest, name: ctx.name, designDir: ctx.designDir, designKey: ctx.designKey, screenId: id, state, assets: "server",
|
|
94
|
+
}),
|
|
95
|
+
warnings: [],
|
|
96
|
+
}),
|
|
97
|
+
renderScreen,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Reconcile comments with the platform while we run, so review feedback from the share link shows
|
|
101
|
+
// up on the board (and in comments/) without a manual pull; answers go back with `lecodes push`.
|
|
102
|
+
// `--no-sync` opts out.
|
|
103
|
+
const sync = flags["no-sync"] ? null : startCommentSync(ctx, designServer)
|
|
104
|
+
|
|
105
|
+
log(`${c.bold("LeCodes Design")} — ${ctx.name}`)
|
|
106
|
+
log(`Canvas: ${c.bold(server.url)}`)
|
|
107
|
+
log(`MCP: ${c.bold(server.url + "/mcp")}${renderer ? "" : c.dim(" (render_screen off — npm install -g lecodes-headless)")}`)
|
|
108
|
+
log(`Watching: ${ctx.dirName}/ (screens hot-reload on save; Ctrl+C to stop)`)
|
|
109
|
+
if (sync) log(`Comments: ${c.dim("in sync with the platform — replies and resolves go back automatically")}`)
|
|
110
|
+
if (!flags["no-open"]) openBrowser(server.url)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const serveCmd = defineCommand({
|
|
114
|
+
name: "serve",
|
|
115
|
+
summary: "Start the dev server + browser canvas (what bare `lecodes design` does)",
|
|
116
|
+
flags: serveFlags,
|
|
117
|
+
examples: ["lecodes design serve --port 4480 --no-open"],
|
|
118
|
+
run: ({ flags }) => serve(flags),
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// ---- init --------------------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/** Register the design server as a project-scoped MCP server so Claude Code auto-discovers it.
|
|
124
|
+
* Merges into an existing .mcp.json (never clobbers a hand-broken one). */
|
|
125
|
+
const writeMcpConfig = (ctx: DesignContext, port: number): void => {
|
|
126
|
+
const path = join(ctx.root, ".mcp.json")
|
|
127
|
+
const entry = { type: "http", url: `http://localhost:${port}/mcp` }
|
|
128
|
+
let config: { mcpServers?: Record<string, unknown> } = {}
|
|
129
|
+
if (existsSync(path)) {
|
|
130
|
+
try {
|
|
131
|
+
config = JSON.parse(readFileSync(path, "utf8"))
|
|
132
|
+
} catch {
|
|
133
|
+
warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.")
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
config.mcpServers ??= {}
|
|
138
|
+
const existing = config.mcpServers["lecodes-design"] as { url?: string } | undefined
|
|
139
|
+
if (existing?.url === entry.url) { note("kept .mcp.json (lecodes-design MCP server)"); return }
|
|
140
|
+
config.mcpServers["lecodes-design"] = entry
|
|
141
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
|
142
|
+
log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const init = defineCommand({
|
|
146
|
+
name: "init",
|
|
147
|
+
summary: "Scaffold the design folder (screens/, shared/, meta.json, spec.md, CLAUDE.md)",
|
|
148
|
+
description: "Existing files are kept. Also ignores .snapshots/ and comments/ in .lecodesignore, registers the board's MCP server in .mcp.json (so Claude Code reaches it), and writes the IDE types + a design-scoped tsconfig so screens typecheck standalone or in a project.",
|
|
149
|
+
flags: {
|
|
150
|
+
...dirFlag,
|
|
151
|
+
port: num("dev-server port to register in .mcp.json", { default: DEFAULT_PORT }),
|
|
152
|
+
desktop: bool("desktop-frame board (meta.json defaultSize [1280, 800] instead of the phone frame)"),
|
|
153
|
+
},
|
|
154
|
+
examples: ["lecodes design init", "lecodes design init --desktop"],
|
|
155
|
+
run: async ({ flags }) => {
|
|
156
|
+
const ctx = getContext(flags.dir)
|
|
157
|
+
// --desktop: scaffold a desktop-frame board (meta.defaultSize [1280, 800]; every consumer —
|
|
158
|
+
// board tiles, arrange, snapshots, render_screen — resolves frames from it).
|
|
159
|
+
const files: [string, string][] = [
|
|
160
|
+
["meta.json", flags.desktop ? META_JSON_DESKTOP : META_JSON],
|
|
161
|
+
["spec.md", SPEC_MD],
|
|
162
|
+
["README.md", README_HINT],
|
|
163
|
+
["CLAUDE.md", CLAUDE_MD],
|
|
164
|
+
[join("shared", "tokens.ts"), TOKENS_TS],
|
|
165
|
+
[join("shared", "tabs.ts"), TABS_TS],
|
|
166
|
+
[join("screens", "home.ts"), HOME_SCREEN_TS],
|
|
167
|
+
]
|
|
168
|
+
mkdirSync(join(ctx.designDir, "screens"), { recursive: true })
|
|
169
|
+
mkdirSync(join(ctx.designDir, "shared"), { recursive: true })
|
|
170
|
+
let created = 0
|
|
171
|
+
for (const [rel, content] of files) {
|
|
172
|
+
const abs = join(ctx.designDir, rel)
|
|
173
|
+
if (existsSync(abs)) { note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`); continue }
|
|
174
|
+
writeFileSync(abs, content)
|
|
175
|
+
log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`)
|
|
176
|
+
created++
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Neither of these is a project file: snapshots are local render artifacts, and comments/
|
|
180
|
+
// mirrors rows the platform already owns (pushing it back would just duplicate the database).
|
|
181
|
+
const ignorePath = join(ctx.root, ".lecodesignore")
|
|
182
|
+
const ignoreLines = [ `${ctx.dirName}/.snapshots/`, `${ctx.dirName}/comments/` ]
|
|
183
|
+
const current = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : ""
|
|
184
|
+
const present = new Set(current.split(/\r?\n/))
|
|
185
|
+
const missing = ignoreLines.filter((line) => !present.has(line))
|
|
186
|
+
if (missing.length) {
|
|
187
|
+
appendFileSync(ignorePath, (current && !current.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n")
|
|
188
|
+
for (const line of missing) log(`${c.green("ignored")} ${line} (in .lecodesignore)`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Let Claude Code reach the running board over MCP (render/check/state/activity tools).
|
|
192
|
+
writeMcpConfig(ctx, flags.port)
|
|
193
|
+
|
|
194
|
+
// IDE types: materialize the SDK's import-free type surface + a design-scoped tsconfig so screens
|
|
195
|
+
// typecheck honestly (globals like UIScreen / UINode / UINodeChild resolve), standalone or in a
|
|
196
|
+
// project. .lecodes/ and tsconfig.json are skipped by the push scanner.
|
|
197
|
+
const wroteTypes = materializeTypesLocal(ctx.designDir)
|
|
198
|
+
writeTsconfig(ctx.designDir)
|
|
199
|
+
// Design-only compiler globals (assetIcon) that aren't in the shipped SDK type surface. Written
|
|
200
|
+
// alongside the bundle so the editor resolves them; rewritten every init to pick up upgrades.
|
|
201
|
+
mkdirSync(join(ctx.designDir, ".lecodes", "types"), { recursive: true })
|
|
202
|
+
writeFileSync(join(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS)
|
|
203
|
+
log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`)
|
|
204
|
+
if (!wroteTypes) warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.")
|
|
205
|
+
|
|
206
|
+
log("")
|
|
207
|
+
log(created ? "Design canvas ready." : "Design canvas already initialized.")
|
|
208
|
+
log(` ${c.bold("lecodes design")} start the canvas (browser)`)
|
|
209
|
+
log(` ${c.bold("lecodes design snapshot")} render screens headless (for AI feedback)`)
|
|
210
|
+
log(` Point Claude Code at ${c.bold(ctx.dirName + "/CLAUDE.md")} — it documents the conventions.`)
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// ---- arrange -----------------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Auto-layout the flow graph in meta.json. Full arrange re-lays-out every screen and always
|
|
218
|
+
* applies (the board offers Undo; the structure-aware score delta is printed); `--new` places only
|
|
219
|
+
* screens that have no explicit `pos` yet and never moves the rest — the mode Claude uses after
|
|
220
|
+
* adding screens. If the dev server is running it picks up the meta.json change and refreshes.
|
|
221
|
+
*/
|
|
222
|
+
const arrange = defineCommand({
|
|
223
|
+
name: "arrange",
|
|
224
|
+
summary: "Auto-layout the flow graph in meta.json",
|
|
225
|
+
description: "Re-lays-out every screen and always applies (the board offers Undo; the layout score delta is printed, lower is better). A running dev server picks the change up and refreshes.",
|
|
226
|
+
flags: { ...dirFlag, new: bool("only place screens without a pos yet; never move the rest") },
|
|
227
|
+
examples: ["lecodes design arrange", "lecodes design arrange --new"],
|
|
228
|
+
run: async ({ flags }) => {
|
|
229
|
+
const ctx = getContext(flags.dir)
|
|
230
|
+
const designServer = await loadDesignServer(ctx)
|
|
231
|
+
const onlyNew = flags.new
|
|
232
|
+
const summary = designServer.arrangeDesign(ctx.designDir, { onlyNew })
|
|
233
|
+
const score = summary.score ? ` (score ${summary.score.before.toFixed(0)} → ${summary.score.after.toFixed(0)}, lower is better)` : ""
|
|
234
|
+
if (!summary.changed) {
|
|
235
|
+
log(onlyNew ? "No unplaced screens — nothing to arrange." : `Already in its arranged shape — nothing moved${score}.`)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
log(`${c.green("✓")} ${onlyNew ? "Placed" : "Arranged"} ${summary.moved} screen${summary.moved === 1 ? "" : "s"} in ${ctx.dirName}/meta.json${score}.`)
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
// ---- share -------------------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Toggle the project's public design share link on the platform. The design must already be pushed
|
|
246
|
+
* (`lecodes push`) — this only flips the link, it doesn't upload anything. `--off` revokes it;
|
|
247
|
+
* `--comments on|off` flips whether link visitors may WRITE comments (reading is never affected).
|
|
248
|
+
*/
|
|
249
|
+
const share = defineCommand({
|
|
250
|
+
name: "share",
|
|
251
|
+
summary: "Turn the public share link on (or --off); --comments on|off gates visitors' comments",
|
|
252
|
+
description: "Needs a cloned project whose design is already pushed — this only flips the link, nothing is uploaded. Anyone with the link can view the mockups; the app code stays private.",
|
|
253
|
+
flags: {
|
|
254
|
+
...dirFlag,
|
|
255
|
+
off: bool("revoke the share link"),
|
|
256
|
+
comments: str("let link visitors leave comments (existing ones stay readable either way)", { value: "<on|off>" }),
|
|
257
|
+
},
|
|
258
|
+
examples: ["lecodes design share", "lecodes design share --comments off", "lecodes design share --off"],
|
|
259
|
+
run: async ({ flags }) => {
|
|
260
|
+
const ctx = getContext(flags.dir)
|
|
261
|
+
if (!ctx.manifest) {
|
|
262
|
+
throw new CliError("`lecodes design share` needs a cloned project. Run it inside one, and push the design first (lecodes push).")
|
|
263
|
+
}
|
|
264
|
+
const { apiUrl, token } = session()
|
|
265
|
+
const uuid = ctx.manifest.uuid
|
|
266
|
+
|
|
267
|
+
if (flags.comments !== undefined) {
|
|
268
|
+
const value = flags.comments.trim().toLowerCase()
|
|
269
|
+
if (value !== "on" && value !== "off") {
|
|
270
|
+
throw new CliError(`--comments takes "on" or "off" (got "${flags.comments}").`)
|
|
271
|
+
}
|
|
272
|
+
const { commentsOpen } = await setDesignCommentsOpen(apiUrl, token, uuid, value === "on")
|
|
273
|
+
log(`${c.green("✓")} Comments on the shared design are ${commentsOpen ? "ON" : "OFF"}.`)
|
|
274
|
+
if (!commentsOpen) note("Existing comments stay readable — this only stops new ones.")
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (flags.off) {
|
|
279
|
+
await disableDesignShare(apiUrl, token, uuid)
|
|
280
|
+
log(`${c.green("✓")} Design sharing is off.`)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
const res = await enableDesignShare(apiUrl, token, uuid)
|
|
284
|
+
log(`${c.bold("Design shared")} — anyone with this link can view the mockups (your app code stays private):`)
|
|
285
|
+
log(` ${c.bold(res.url ?? "")}`)
|
|
286
|
+
log(` ${c.dim(`comments: ${res.commentsOpen === false ? "off" : "on"}`)}`)
|
|
287
|
+
note('Turn it off with: lecodes design share --off · comments: --comments off')
|
|
288
|
+
},
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// ---- check -------------------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Static coherence check for a design folder — the terminal/CI sibling of the `check_concept` MCP
|
|
295
|
+
* tool. Reports the flow warnings meta.json/screens/spec.md already carry (broken edges, missing
|
|
296
|
+
* screens, edges targeting an undeclared `@state`, duplicate state names, dangling `[[spec]]` links)
|
|
297
|
+
* plus concept completeness (spec.md present? screens with no role?) and discovered states. Hard
|
|
298
|
+
* warnings exit non-zero; soft hints never fail the check. No compile, no render — fast, and needs
|
|
299
|
+
* only lecodes-design (not the renderer).
|
|
300
|
+
*/
|
|
301
|
+
const check = defineCommand({
|
|
302
|
+
name: "check",
|
|
303
|
+
summary: "Static coherence check: flow warnings, spec.md links, states, tab bars, open comments",
|
|
304
|
+
description: "The terminal / CI sibling of the board's check_concept tool. Hard warnings (broken edges, missing screens, undeclared states, dangling [[spec]] links) exit non-zero; hints and open comments never fail it. No compile, no render — needs only lecodes-design.",
|
|
305
|
+
flags: dirFlag,
|
|
306
|
+
examples: ["lecodes design check"],
|
|
307
|
+
run: async ({ flags }) => {
|
|
308
|
+
const ctx = getContext(flags.dir)
|
|
309
|
+
const designServer = await loadDesignServer(ctx)
|
|
310
|
+
const state = designServer.readDesignState(ctx.designDir)
|
|
311
|
+
const concept = designServer.conceptReport(ctx.designDir, state.screens)
|
|
312
|
+
|
|
313
|
+
if (state.warnings.length) {
|
|
314
|
+
log(c.bold(`Warnings (${state.warnings.length})`))
|
|
315
|
+
for (const w of state.warnings) log(` ${c.yellow("!")} ${w}`)
|
|
316
|
+
log("")
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
log(c.bold("Concept"))
|
|
320
|
+
log(` spec.md: ${concept.hasSpec ? c.green("present") : c.dim("missing (optional)")}`)
|
|
321
|
+
if (concept.screenRefs.length) {
|
|
322
|
+
const resolved = concept.screenRefs.filter((r) => r.exists).length
|
|
323
|
+
log(` [[links]]: ${resolved}/${concept.screenRefs.length} resolve`)
|
|
324
|
+
}
|
|
325
|
+
if (concept.screensWithoutRole.length) log(` ${c.dim("no role:")} ${concept.screensWithoutRole.join(", ")}`)
|
|
326
|
+
log("")
|
|
327
|
+
|
|
328
|
+
// States: list multi-state screens (canonical highlighted) + soft "not discoverable" hints.
|
|
329
|
+
const stateful = state.screens.filter((s) => s.states.length > 1)
|
|
330
|
+
const undiscoverable = state.screens.filter((s) => s.statesWarning)
|
|
331
|
+
if (stateful.length || undiscoverable.length) {
|
|
332
|
+
log(c.bold("States"))
|
|
333
|
+
for (const s of stateful) {
|
|
334
|
+
log(` ${s.id}: ${s.states.map((st) => st === s.canonical ? c.green(st) : st).join(", ")}`)
|
|
335
|
+
}
|
|
336
|
+
for (const s of undiscoverable) log(` ${c.yellow("!")} ${s.id}: ${s.statesWarning}`)
|
|
337
|
+
log("")
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Tab bars: the source-discovered hubs (defineTabs) — tabs in order + how many screens sit on
|
|
341
|
+
// each bar. Problems (missing roots, unknown tabs, bar-implied edges) surface in Warnings above.
|
|
342
|
+
if (state.hubs.length) {
|
|
343
|
+
log(c.bold("Tab bars"))
|
|
344
|
+
for (const h of state.hubs) {
|
|
345
|
+
const mounted = Object.keys(h.lanes).length
|
|
346
|
+
log(` ${h.name} (${h.file}): ${h.tabs.join(" · ")} — ${mounted} screen${mounted === 1 ? "" : "s"} on the bar`)
|
|
347
|
+
}
|
|
348
|
+
log("")
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Review comments: open threads are outstanding work on this design, so a coherence check that
|
|
352
|
+
// stayed silent about them would report "no problems" over a board full of unanswered feedback.
|
|
353
|
+
// They never fail the check — feedback isn't an error, and the file may be a stale mirror. An
|
|
354
|
+
// older lecodes-design without the comment store simply omits the section rather than failing.
|
|
355
|
+
const commentThreads = typeof designServer.listDesignComments === "function"
|
|
356
|
+
? designServer.listDesignComments(ctx.designDir).threads
|
|
357
|
+
: []
|
|
358
|
+
if (commentThreads.length) {
|
|
359
|
+
const open = commentThreads.filter((t) => !t.resolved)
|
|
360
|
+
log(c.bold("Comments"))
|
|
361
|
+
log(` ${open.length} open · ${commentThreads.length} total`)
|
|
362
|
+
for (const thread of open.slice(0, 5)) {
|
|
363
|
+
const where = thread.anchor.screen ?? "(board)"
|
|
364
|
+
log(` ${c.dim(`#${thread.number}`)} ${where} ${c.dim(firstLine(thread.messages[0]?.body ?? ""))}`)
|
|
365
|
+
}
|
|
366
|
+
if (open.length > 5) log(` ${c.dim(`…and ${open.length - 5} more — lecodes design comments`)}`)
|
|
367
|
+
log("")
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const hard = state.warnings.length
|
|
371
|
+
if (hard === 0) {
|
|
372
|
+
const n = state.screens.length
|
|
373
|
+
log(`${c.green("✓")} No coherence problems${n ? ` across ${n} screen${n === 1 ? "" : "s"}` : ""}.`)
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
log(`${c.red("✗")} ${hard} coherence warning${hard === 1 ? "" : "s"} — see above.`)
|
|
377
|
+
process.exitCode = 1
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
// ---- the tree ----------------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
export default defineCommand({
|
|
384
|
+
name: "design",
|
|
385
|
+
summary: "LeCodes Design — a live canvas of screens for AI-driven prototyping",
|
|
386
|
+
description: "Bare `lecodes design` starts the canvas (= `design serve`). Needs the optional lecodes-design package (installed on first use); works inside a cloned project or standalone in any folder.\n\nThe design folder (default design/) holds one file per screen (screens/<id>.ts default-exports a UIScreen), shared tokens and components (shared/), and meta.json — the human-owned graph of positions, descriptions and edges. Screens compile per file through the normal project pipeline and render live in the browser and headless for AI feedback.",
|
|
387
|
+
flags: serveFlags,
|
|
388
|
+
commands: [serveCmd, init, snapshot, arrange, check, share, comments],
|
|
389
|
+
examples: ["lecodes design init && lecodes design", "lecodes design --port 4480 --no-open", "lecodes design snapshot --png"],
|
|
390
|
+
run: ({ flags }) => serve(flags),
|
|
391
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { CliError, bool, c, defineCommand, log, logErr, note, str, warnErr } from "../../cli"
|
|
4
|
+
import { compileDesignScreen } from "../../compile/designCompile"
|
|
5
|
+
import { loadPeer } from "../../hosts/peerInstall"
|
|
6
|
+
import { readMetaFrames } from "../../project/designMeta"
|
|
7
|
+
import { dirFlag, getContext, type DesignServer } from "./context"
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* `lecodes design snapshot [screen…]` — render design screens headless through lecodes-headless,
|
|
11
|
+
* the way the board's MCP render_screen tool does, for AI feedback without a browser. One requested
|
|
12
|
+
* screen prints its JSON to stdout; anything else lands as files in <dir>/.snapshots/ (or --out-dir).
|
|
13
|
+
* States: `--state <s>` renders one named state of each screen, `--all-states` every discovered
|
|
14
|
+
* one (the canonical state also written unsuffixed, so `<id>.json` always exists).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Screen ids from <designDir>/screens/*.ts. */
|
|
18
|
+
const listScreens = (designDir: string): string[] => {
|
|
19
|
+
const dir = join(designDir, "screens")
|
|
20
|
+
if (!existsSync(dir)) return []
|
|
21
|
+
return readdirSync(dir)
|
|
22
|
+
.filter((f) => f.endsWith(".ts") && !f.startsWith("."))
|
|
23
|
+
.map((f) => f.slice(0, -3))
|
|
24
|
+
.sort()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default defineCommand({
|
|
28
|
+
name: "snapshot",
|
|
29
|
+
summary: "Render design screens headless to JSON / PNG (needs lecodes-headless)",
|
|
30
|
+
usage: "[screen…]",
|
|
31
|
+
description: "One screen → its JSON on stdout; otherwise one file per screen in <dir>/.snapshots/ (<id>.json, <id>@<state>.json, PNGs with --png). --state and --all-states need lecodes-design to discover a screen's states from its source. A stuck screen (runaway loop) is killed after 120 s.",
|
|
32
|
+
flags: {
|
|
33
|
+
...dirFlag,
|
|
34
|
+
state: str("render this state of every (state) => UIScreen screen", { value: "<name>" }),
|
|
35
|
+
"all-states": bool("render every discovered state of each screen"),
|
|
36
|
+
png: str("also rasterize PNGs at 2× (needs @napi-rs/canvas); a value is accepted and ignored", { optional: true, value: "[file]" }),
|
|
37
|
+
"out-dir": str("output folder (default <dir>/.snapshots)", { value: "<dir>" }),
|
|
38
|
+
logs: bool("forward the screens' console output (to stderr)"),
|
|
39
|
+
},
|
|
40
|
+
examples: [
|
|
41
|
+
"lecodes design snapshot",
|
|
42
|
+
"lecodes design snapshot login",
|
|
43
|
+
"lecodes design snapshot --all-states --png",
|
|
44
|
+
"lecodes design snapshot login --state error",
|
|
45
|
+
],
|
|
46
|
+
run: async ({ args, flags }) => {
|
|
47
|
+
const ctx = getContext(flags.dir)
|
|
48
|
+
const all = listScreens(ctx.designDir)
|
|
49
|
+
if (all.length === 0) throw new CliError(`No screens in ${ctx.dirName}/screens/ — run "lecodes design init" first.`)
|
|
50
|
+
for (const id of args) {
|
|
51
|
+
if (!all.includes(id)) throw new CliError(`No screen "${id}" (have: ${all.join(", ")}).`)
|
|
52
|
+
}
|
|
53
|
+
const targets = args.length ? args : all
|
|
54
|
+
|
|
55
|
+
const stateFlag = flags.state
|
|
56
|
+
const allStates = flags["all-states"]
|
|
57
|
+
if (stateFlag !== undefined && allStates) throw new CliError("Use either --state <name> or --all-states, not both.")
|
|
58
|
+
|
|
59
|
+
const renderer = await loadPeer<typeof import("lecodes-headless/headless")>("lecodes-headless", "headless", { for: "lecodes design render" })
|
|
60
|
+
|
|
61
|
+
// Enumerating / validating states parses the screen source — that discovery lives in the design
|
|
62
|
+
// package. The canonical-only path (no flag) needs no discovery, so base snapshot stays lean.
|
|
63
|
+
let discover: ((source: string) => { states: string[], canonical: string }) | null = null
|
|
64
|
+
if (stateFlag !== undefined || allStates) {
|
|
65
|
+
const ds = await loadPeer<DesignServer>("lecodes-design", "server", { for: "lecodes design render --state" })
|
|
66
|
+
discover = (src) => { const d = ds.discoverScreenStates(src); return { states: d.states, canonical: d.canonical } }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// A stuck screen (runaway loop) shouldn't hang the CLI forever.
|
|
70
|
+
const killer = setTimeout(() => {
|
|
71
|
+
warnErr("Snapshot timed out after 120s.")
|
|
72
|
+
process.exit(1)
|
|
73
|
+
}, 120000)
|
|
74
|
+
killer.unref?.()
|
|
75
|
+
|
|
76
|
+
const png = flags.png !== undefined
|
|
77
|
+
const frames = readMetaFrames(ctx.designDir)
|
|
78
|
+
const outDir = flags["out-dir"] ?? join(ctx.designDir, ".snapshots")
|
|
79
|
+
const single = targets.length === 1 && args.length === 1 && !png && !allStates && !flags["out-dir"]
|
|
80
|
+
const onConsole = flags.logs
|
|
81
|
+
? (level: string, text: string) => logErr(`${c.dim(`[${level}]`)} ${text}`)
|
|
82
|
+
: undefined
|
|
83
|
+
|
|
84
|
+
if (!single) mkdirSync(outDir, { recursive: true })
|
|
85
|
+
|
|
86
|
+
for (const id of targets) {
|
|
87
|
+
const [ width, height ] = frames.screens[id]?.size ?? frames.defaultSize
|
|
88
|
+
|
|
89
|
+
// What to render, and the file base(s) for each. `undefined` state = a bare (canonical) render.
|
|
90
|
+
// The canonical state also writes the unsuffixed `<id>` for backward compatibility.
|
|
91
|
+
let plan: { state?: string, bases: string[] }[] = [{ state: undefined, bases: [id] }]
|
|
92
|
+
if (discover) {
|
|
93
|
+
const { states, canonical } = discover(readFileSync(join(ctx.designDir, "screens", `${id}.ts`), "utf8"))
|
|
94
|
+
if (stateFlag !== undefined) {
|
|
95
|
+
if (!states.includes(stateFlag)) {
|
|
96
|
+
if (args.length === 1) throw new CliError(`No state "${stateFlag}" on ${id} (has: ${states.join(", ")}).`)
|
|
97
|
+
warnErr(`${id}: no state "${stateFlag}" (has: ${states.join(", ")}) — skipped.`)
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
plan = [{ state: stateFlag, bases: [`${id}@${stateFlag}`] }]
|
|
101
|
+
} else {
|
|
102
|
+
plan = states.map((st) => ({ state: st, bases: st === canonical ? [`${id}@${st}`, id] : [`${id}@${st}`] }))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const { state, bases } of plan) {
|
|
107
|
+
const tag = state ? `${id}@${state}` : id
|
|
108
|
+
note(`Rendering ${tag}…`)
|
|
109
|
+
const js = await compileDesignScreen({
|
|
110
|
+
root: ctx.root, manifest: ctx.manifest, name: ctx.name, designDir: ctx.designDir, designKey: ctx.designKey, screenId: id, state, assets: "disk",
|
|
111
|
+
})
|
|
112
|
+
const result = await renderer.renderToJson(js, { width, height, localAssets: true, onConsole })
|
|
113
|
+
for (const w of result.warnings) warnErr(`${tag}: ${w}`)
|
|
114
|
+
|
|
115
|
+
if (single) {
|
|
116
|
+
const text = result.text.endsWith("\n") ? result.text : result.text + "\n"
|
|
117
|
+
process.stdout.write(text)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
const pngResult = png ? await renderer.renderToPng(js, { width, height, localAssets: true, scale: 2, onConsole }) : null
|
|
121
|
+
if (pngResult) for (const w of pngResult.warnings) warnErr(`${tag}: ${w}`)
|
|
122
|
+
|
|
123
|
+
for (const base of bases) {
|
|
124
|
+
writeFileSync(join(outDir, `${base}.json`), result.text)
|
|
125
|
+
log(`${c.green("✓")} ${tag} → ${join(outDir, `${base}.json`)}`)
|
|
126
|
+
if (pngResult) {
|
|
127
|
+
writeFileSync(join(outDir, `${base}.png`), pngResult.png)
|
|
128
|
+
log(`${c.green("✓")} ${tag} → ${join(outDir, `${base}.png`)}`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
})
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* platform editor's "Create design" button. Imported by a RELATIVE path through the package's
|
|
4
4
|
* node_modules link (not as `lecodes-design/templates`) on purpose: the CLI build externalizes the
|
|
5
5
|
* `lecodes-design` package (it's an optional runtime install for `design serve`), but `init` must
|
|
6
|
-
* work without it — a relative import gets bundled into the binary like any local module. The
|
|
7
|
-
*
|
|
6
|
+
* work without it — a relative import gets bundled into the binary like any local module. The
|
|
7
|
+
* package comes from the `lecodes-design` dev dependency (an npm range; `bun link lecodes-design`
|
|
8
|
+
* swaps in a sibling checkout). */
|
|
8
9
|
|
|
9
10
|
export {
|
|
10
11
|
META_JSON, META_JSON_DESKTOP, SPEC_MD, TOKENS_TS, TABS_TS, HOME_SCREEN_TS, CLAUDE_MD, DESIGN_MACROS_DTS, README_HINT,
|
|
11
|
-
} from "
|
|
12
|
+
} from "../../../node_modules/lecodes-design/src/templates"
|