lecodes-cli 2.4.1 → 2.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +36134 -11203
- package/package.json +10 -9
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/assets/ar-D6oDL4pw.js +1 -0
- package/runtime/web/assets/ar-mVXX3kRQ.js +2 -0
- package/runtime/web/assets/qrAnchorWorker-Cb5E8cF-.js +1 -0
- package/runtime/web/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
- package/runtime/web/index.html +1 -0
- package/runtime/web/player.js +12 -12
- package/runtime/web-lite/assets/{index-B2zEJlWS.js → index-oPsDD47E.js} +1 -1
- package/runtime/web-lite/assets/scene3d-BAl_QQyi.js +1105 -0
- package/runtime/web-lite/features.json +2 -2
- package/runtime/web-lite/index.html +2 -1
- package/runtime/web-lite/lite.js +9 -9
- package/runtime/web-lite/plugins/ar/_virtual_lecodes-half-resources-CNYmVR88.js +1 -0
- package/runtime/web-lite/plugins/ar/assets/qrAnchorWorker-DcxVKAqo.js +1 -0
- package/runtime/web-lite/plugins/ar/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
- package/runtime/web-lite/plugins/ar/index-C2uuuLhz.js +2 -0
- package/runtime/web-lite/plugins.json +1 -1
- package/src/commands/app/android.ts +68 -19
- package/src/commands/app/index.ts +84 -17
- package/src/commands/app/shared.ts +157 -24
- package/src/commands/app/splash.ts +251 -0
- package/src/commands/app/templates/android.ts +148 -25
- package/src/commands/app/templates/ios.ts +123 -12
- package/src/commands/init/index.ts +195 -195
- package/src/commands/plugin.ts +3 -3
- package/src/commands/push.ts +2 -2
- package/src/commands/shared.ts +79 -79
- package/src/commands/thumbs.ts +185 -185
- package/src/commands/web.ts +12 -5
- package/src/compile/collect.ts +2 -3
- package/src/compile/collectLocal.ts +13 -2
- package/src/compile/designCompile.ts +2 -3
- package/src/compile/projectCompile.ts +3 -0
- package/src/compile/screenEntry.ts +123 -123
- package/src/dev/devServer.ts +63 -7
- package/src/dev/https.ts +50 -0
- package/src/dev/serve.ts +15 -9
- package/src/dev/webRunner.ts +11 -3
- package/src/platform/serverDiff.ts +2 -3
- package/src/plugin/emitPackage.ts +18 -5
- package/src/plugin/emitWeb.ts +17 -8
- package/src/plugin/firstParty.ts +30 -0
- package/src/plugin/generate.ts +52 -24
- package/src/plugin/manifest.ts +66 -7
- package/src/project/ignore.ts +164 -164
- package/src/project/localFiles.ts +44 -7
- package/src/web/build.ts +33 -9
- package/src/web/halves.ts +76 -0
- package/runtime/web/assets/ar-CHvaraG4.js +0 -1
- package/runtime/web-lite/assets/scene3d-Bew8g7Ws.js +0 -1081
- package/runtime/web-lite/plugins/ar/index-CHvaraG4.js +0 -1
|
@@ -1,195 +1,195 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
-
import { basename, dirname, join, resolve } from "node:path"
|
|
3
|
-
import { LECODES_DIR } from "../../project/manifest"
|
|
4
|
-
import { materializeTypesLocal, writeTsconfig } from "../../project/types"
|
|
5
|
-
import { DEFAULT_IGNORE_FILE, ensureGitignore } from "../../project/ignore"
|
|
6
|
-
import { appClaudeMd, CLAUDE_SETTINGS, STARTER_FLOW } from "./templates"
|
|
7
|
-
import { bool, c, defineCommand, log, note, success, warnErr } from "../../cli"
|
|
8
|
-
|
|
9
|
-
/*
|
|
10
|
-
* `lecodes init [dir]` — scaffold a brand-new LOCAL project: no account, no login, no server.
|
|
11
|
-
*
|
|
12
|
-
* Writes a starter entry, a CLAUDE.md (the headless render/test workflow for AI assistants) with
|
|
13
|
-
* a passing starter flow test, the SDK's import-free type surface + a tsconfig (so VS Code gives
|
|
14
|
-
* full IntelliSense offline), and a `.lecodesignore`. The result runs immediately with `lecodes dev`
|
|
15
|
-
* (hot reload on a device) and compiles with `lecodes compile -o app.js` (a self-contained bundle
|
|
16
|
-
* for LeCodes Desktop). It's the offline counterpart to `lecodes create`, which makes a project on
|
|
17
|
-
* the server; a local project needs no manifest — `dev`/`compile` compile straight from the tree.
|
|
18
|
-
*
|
|
19
|
-
* Idempotent: existing files are kept (never clobbered), so it's safe to re-run or run in a folder
|
|
20
|
-
* that already has content. Only the generated tsconfig + `.lecodes/types/` are rewritten each run.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const STARTER = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
|
|
24
|
-
// Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
|
|
25
|
-
|
|
26
|
-
let count = 0
|
|
27
|
-
|
|
28
|
-
const counter = UIText("Taps: 0").style({ color: "#9fb0d0", fontSize: 22, marginTop: 18 })
|
|
29
|
-
|
|
30
|
-
const button = UIButton(UIText("Tap me").style({ color: "#ffffff", fontSize: 22 }))
|
|
31
|
-
.style({
|
|
32
|
-
backgroundColor: "#4f7cff",
|
|
33
|
-
borderRadius: 14,
|
|
34
|
-
paddingVertical: 16,
|
|
35
|
-
paddingHorizontal: 40,
|
|
36
|
-
marginTop: 24,
|
|
37
|
-
alignItems: "center",
|
|
38
|
-
justifyContent: "center",
|
|
39
|
-
$hovered: { backgroundColor: "#6b91ff" }, // the mouse over it (desktop); $pressed is the press
|
|
40
|
-
})
|
|
41
|
-
.named("tap") // names make elements targetable in tests/*.flow.json (see CLAUDE.md)
|
|
42
|
-
.onClick(() => {
|
|
43
|
-
count++
|
|
44
|
-
counter.text = "Taps: " + count
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
const screen = UIScreen(
|
|
48
|
-
UIColumn(UIText("Hello, LeCodes").style({ color: "#ffffff", fontSize: 32 }), button, counter)
|
|
49
|
-
.style({ backgroundColor: "#2b3550", borderRadius: 24, padding: 40, alignItems: "center" }),
|
|
50
|
-
)
|
|
51
|
-
.style({ backgroundColor: "#1e2230", alignItems: "center", justifyContent: "center", padding: 40 })
|
|
52
|
-
.named("home") // the screen root's name is what tests' \`expect: { screen: … }\` reads
|
|
53
|
-
|
|
54
|
-
screen.open()
|
|
55
|
-
`
|
|
56
|
-
|
|
57
|
-
/** `--desktop`: declare the desktop target up front. Window sizes are logical units (the host
|
|
58
|
-
* scales for HiDPI); `dev --desktop` / `desktop run` / `render --desktop` all read this, and a
|
|
59
|
-
* desktop-only app.json makes headless render/test default to the window size too. */
|
|
60
|
-
const appJsonDesktop = (name: string) => JSON.stringify({
|
|
61
|
-
name,
|
|
62
|
-
desktop: {
|
|
63
|
-
window: { width: 1280, height: 800, minWidth: 640, minHeight: 480 },
|
|
64
|
-
},
|
|
65
|
-
}, null, 2) + "\n"
|
|
66
|
-
|
|
67
|
-
const readmeTemplate = (name: string) => `# ${name}
|
|
68
|
-
|
|
69
|
-
A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
|
|
70
|
-
no imports needed. \`main.ts\` is the entry point.
|
|
71
|
-
|
|
72
|
-
## Run it
|
|
73
|
-
|
|
74
|
-
\`\`\`
|
|
75
|
-
lecodes dev # live on your phone: scan the QR with the LeCodes app, hot reload
|
|
76
|
-
lecodes compile -o app.js # bundle to a single self-contained JS file
|
|
77
|
-
\`\`\`
|
|
78
|
-
|
|
79
|
-
## Test it (no device needed)
|
|
80
|
-
|
|
81
|
-
\`\`\`
|
|
82
|
-
lecodes render --png shot.png # headless screenshot of the running app
|
|
83
|
-
lecodes test # run tests/*.flow.json — the starter ships with a passing one
|
|
84
|
-
\`\`\`
|
|
85
|
-
|
|
86
|
-
Scenario format + conventions: see [CLAUDE.md](./CLAUDE.md) (written for AI assistants, useful
|
|
87
|
-
for humans too).
|
|
88
|
-
|
|
89
|
-
## Put it on the platform
|
|
90
|
-
|
|
91
|
-
This project is local-only. To give it a home on le.codes (web editor, sharing, OTA updates):
|
|
92
|
-
|
|
93
|
-
\`\`\`
|
|
94
|
-
lecodes login # once per machine
|
|
95
|
-
lecodes link # create a project from this folder and connect it
|
|
96
|
-
lecodes push -m "initial" # upload it as the first checkpoint (--publish to go live)
|
|
97
|
-
\`\`\`
|
|
98
|
-
|
|
99
|
-
Already made the project in the web editor? \`lecodes link --project <uuid>\` adopts it — it
|
|
100
|
-
shows what your next push would change there before connecting anything.
|
|
101
|
-
|
|
102
|
-
## Go native
|
|
103
|
-
|
|
104
|
-
\`\`\`
|
|
105
|
-
lecodes app init ios # generate a native Xcode shell in ios/
|
|
106
|
-
lecodes app sync # embed the compiled bundle + plugins into it
|
|
107
|
-
lecodes app open # open it in Xcode, build & run
|
|
108
|
-
\`\`\`
|
|
109
|
-
|
|
110
|
-
## Layout
|
|
111
|
-
|
|
112
|
-
- \`main.ts\` — the app's entry point. Add more .ts files freely and import them from
|
|
113
|
-
\`main.ts\`; a file named \`main.ts\` is always the entry.
|
|
114
|
-
- \`tests/\` — headless flow tests (\`lecodes test\`); failing steps dump the frame to
|
|
115
|
-
\`tests/.artifacts/\`
|
|
116
|
-
- \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
|
|
117
|
-
- \`assets/\` — images/fonts the app references (created on demand)
|
|
118
|
-
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
|
|
119
|
-
They come from the SDK bundled in the CLI; \`lecodes update\` refreshes them from the platform.
|
|
120
|
-
- \`.lecodes/\` is gitignored — per-checkout state (those types + \`manifest.json\`, this machine's
|
|
121
|
-
sync cursor once linked). After a fresh git clone: \`lecodes update\`, then
|
|
122
|
-
\`lecodes link --project <uuid>\` to re-attach.
|
|
123
|
-
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
124
|
-
|
|
125
|
-
Full command list: \`lecodes --help\`.
|
|
126
|
-
`
|
|
127
|
-
|
|
128
|
-
export default defineCommand({
|
|
129
|
-
name: "init",
|
|
130
|
-
summary: "Scaffold a new LOCAL project (no login, no server)",
|
|
131
|
-
usage: "[dir]",
|
|
132
|
-
description: "Writes a starter main.ts, a CLAUDE.md + a passing flow test (the headless render / test loop), the IDE types + tsconfig, and a .lecodesignore. Run it with `lecodes dev` or `lecodes compile`. Idempotent: existing files are kept.",
|
|
133
|
-
flags: { desktop: bool("also write an app.json declaring the desktop target (window size; `dev --desktop`, `desktop run` and headless render / test pick it up)") },
|
|
134
|
-
examples: ["lecodes init myapp", "lecodes init --desktop"],
|
|
135
|
-
run: ({ args, flags }) => init(args[0], flags.desktop),
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
const init = (dir: string | undefined, forDesktop: boolean): void => {
|
|
139
|
-
const dirArg = (dir ?? ".").replace(/[/\\]+$/, "") || "."
|
|
140
|
-
const root = resolve(dirArg)
|
|
141
|
-
const name = basename(root)
|
|
142
|
-
|
|
143
|
-
mkdirSync(root, { recursive: true })
|
|
144
|
-
|
|
145
|
-
let created = 0
|
|
146
|
-
const write = (rel: string, content: string) => {
|
|
147
|
-
const abs = join(root, rel)
|
|
148
|
-
if (existsSync(abs)) { note(`kept ${rel}`); return }
|
|
149
|
-
mkdirSync(dirname(abs), { recursive: true })
|
|
150
|
-
writeFileSync(abs, content)
|
|
151
|
-
log(`${c.green("created")} ${rel}`)
|
|
152
|
-
created++
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Starter entry — a root file named main.ts is always the detected entrypoint.
|
|
156
|
-
write("main.ts", STARTER)
|
|
157
|
-
write("README.md", readmeTemplate(name))
|
|
158
|
-
// --desktop: an app.json declaring the desktop target (window config in logical units).
|
|
159
|
-
if (forDesktop) write("app.json", appJsonDesktop(name))
|
|
160
|
-
// AI-assistant guidance + a passing flow test: the headless loop is discoverable from day one.
|
|
161
|
-
write("CLAUDE.md", appClaudeMd(name))
|
|
162
|
-
write(".claude/settings.json", CLAUDE_SETTINGS)
|
|
163
|
-
write("tests/counter.flow.json", STARTER_FLOW)
|
|
164
|
-
// Keep local render/preview outputs (screenshot.png, shots/) out of any future push.
|
|
165
|
-
write(".lecodesignore", DEFAULT_IGNORE_FILE)
|
|
166
|
-
// .lecodes/ is per-checkout state (generated types + the push cursor) — keep it out of git.
|
|
167
|
-
const gi = ensureGitignore(root)
|
|
168
|
-
if (gi === "created") { log(`${c.green("created")} .gitignore`); created++ }
|
|
169
|
-
else if (gi === "updated") log(`${c.green("updated")} .gitignore ${c.dim("(+ .lecodes/)")}`)
|
|
170
|
-
else note("kept .gitignore")
|
|
171
|
-
|
|
172
|
-
// IDE types: the SDK's import-free global surface + a tsconfig, so VS Code resolves UIScreen,
|
|
173
|
-
// Scene2D, setLoop, … with no server. Regenerated every run to pick up CLI upgrades.
|
|
174
|
-
const wroteTypes = materializeTypesLocal(root)
|
|
175
|
-
writeTsconfig(root)
|
|
176
|
-
log(`${c.green("written")} tsconfig.json${wroteTypes ? ` + ${LECODES_DIR}/types/` : ""} (IDE types)`)
|
|
177
|
-
if (!wroteTypes) {
|
|
178
|
-
warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.")
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
log("")
|
|
182
|
-
success(created
|
|
183
|
-
? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.`
|
|
184
|
-
: `Project ${c.bold(name)} already initialized.`)
|
|
185
|
-
const cd = dirArg === "." ? "" : `cd ${dirArg} && `
|
|
186
|
-
if (forDesktop) {
|
|
187
|
-
log(` ${c.bold(cd + "lecodes dev --desktop")} run it in a desktop window with hot reload`)
|
|
188
|
-
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (desktop-sized)`)
|
|
189
|
-
log(` ${c.bold(cd + "lecodes desktop run")} open the compiled app in the desktop host`)
|
|
190
|
-
} else {
|
|
191
|
-
log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`)
|
|
192
|
-
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`)
|
|
193
|
-
log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path"
|
|
3
|
+
import { LECODES_DIR } from "../../project/manifest"
|
|
4
|
+
import { materializeTypesLocal, writeTsconfig } from "../../project/types"
|
|
5
|
+
import { DEFAULT_IGNORE_FILE, ensureGitignore } from "../../project/ignore"
|
|
6
|
+
import { appClaudeMd, CLAUDE_SETTINGS, STARTER_FLOW } from "./templates"
|
|
7
|
+
import { bool, c, defineCommand, log, note, success, warnErr } from "../../cli"
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* `lecodes init [dir]` — scaffold a brand-new LOCAL project: no account, no login, no server.
|
|
11
|
+
*
|
|
12
|
+
* Writes a starter entry, a CLAUDE.md (the headless render/test workflow for AI assistants) with
|
|
13
|
+
* a passing starter flow test, the SDK's import-free type surface + a tsconfig (so VS Code gives
|
|
14
|
+
* full IntelliSense offline), and a `.lecodesignore`. The result runs immediately with `lecodes dev`
|
|
15
|
+
* (hot reload on a device) and compiles with `lecodes compile -o app.js` (a self-contained bundle
|
|
16
|
+
* for LeCodes Desktop). It's the offline counterpart to `lecodes create`, which makes a project on
|
|
17
|
+
* the server; a local project needs no manifest — `dev`/`compile` compile straight from the tree.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent: existing files are kept (never clobbered), so it's safe to re-run or run in a folder
|
|
20
|
+
* that already has content. Only the generated tsconfig + `.lecodes/types/` are rewritten each run.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const STARTER = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
|
|
24
|
+
// Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
|
|
25
|
+
|
|
26
|
+
let count = 0
|
|
27
|
+
|
|
28
|
+
const counter = UIText("Taps: 0").style({ color: "#9fb0d0", fontSize: 22, marginTop: 18 })
|
|
29
|
+
|
|
30
|
+
const button = UIButton(UIText("Tap me").style({ color: "#ffffff", fontSize: 22 }))
|
|
31
|
+
.style({
|
|
32
|
+
backgroundColor: "#4f7cff",
|
|
33
|
+
borderRadius: 14,
|
|
34
|
+
paddingVertical: 16,
|
|
35
|
+
paddingHorizontal: 40,
|
|
36
|
+
marginTop: 24,
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
justifyContent: "center",
|
|
39
|
+
$hovered: { backgroundColor: "#6b91ff" }, // the mouse over it (desktop); $pressed is the press
|
|
40
|
+
})
|
|
41
|
+
.named("tap") // names make elements targetable in tests/*.flow.json (see CLAUDE.md)
|
|
42
|
+
.onClick(() => {
|
|
43
|
+
count++
|
|
44
|
+
counter.text = "Taps: " + count
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const screen = UIScreen(
|
|
48
|
+
UIColumn(UIText("Hello, LeCodes").style({ color: "#ffffff", fontSize: 32 }), button, counter)
|
|
49
|
+
.style({ backgroundColor: "#2b3550", borderRadius: 24, padding: 40, alignItems: "center" }),
|
|
50
|
+
)
|
|
51
|
+
.style({ backgroundColor: "#1e2230", alignItems: "center", justifyContent: "center", padding: 40 })
|
|
52
|
+
.named("home") // the screen root's name is what tests' \`expect: { screen: … }\` reads
|
|
53
|
+
|
|
54
|
+
screen.open()
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
/** `--desktop`: declare the desktop target up front. Window sizes are logical units (the host
|
|
58
|
+
* scales for HiDPI); `dev --desktop` / `desktop run` / `render --desktop` all read this, and a
|
|
59
|
+
* desktop-only app.json makes headless render/test default to the window size too. */
|
|
60
|
+
const appJsonDesktop = (name: string) => JSON.stringify({
|
|
61
|
+
name,
|
|
62
|
+
desktop: {
|
|
63
|
+
window: { width: 1280, height: 800, minWidth: 640, minHeight: 480 },
|
|
64
|
+
},
|
|
65
|
+
}, null, 2) + "\n"
|
|
66
|
+
|
|
67
|
+
const readmeTemplate = (name: string) => `# ${name}
|
|
68
|
+
|
|
69
|
+
A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
|
|
70
|
+
no imports needed. \`main.ts\` is the entry point.
|
|
71
|
+
|
|
72
|
+
## Run it
|
|
73
|
+
|
|
74
|
+
\`\`\`
|
|
75
|
+
lecodes dev # live on your phone: scan the QR with the LeCodes app, hot reload
|
|
76
|
+
lecodes compile -o app.js # bundle to a single self-contained JS file
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
## Test it (no device needed)
|
|
80
|
+
|
|
81
|
+
\`\`\`
|
|
82
|
+
lecodes render --png shot.png # headless screenshot of the running app
|
|
83
|
+
lecodes test # run tests/*.flow.json — the starter ships with a passing one
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
Scenario format + conventions: see [CLAUDE.md](./CLAUDE.md) (written for AI assistants, useful
|
|
87
|
+
for humans too).
|
|
88
|
+
|
|
89
|
+
## Put it on the platform
|
|
90
|
+
|
|
91
|
+
This project is local-only. To give it a home on le.codes (web editor, sharing, OTA updates):
|
|
92
|
+
|
|
93
|
+
\`\`\`
|
|
94
|
+
lecodes login # once per machine
|
|
95
|
+
lecodes link # create a project from this folder and connect it
|
|
96
|
+
lecodes push -m "initial" # upload it as the first checkpoint (--publish to go live)
|
|
97
|
+
\`\`\`
|
|
98
|
+
|
|
99
|
+
Already made the project in the web editor? \`lecodes link --project <uuid>\` adopts it — it
|
|
100
|
+
shows what your next push would change there before connecting anything.
|
|
101
|
+
|
|
102
|
+
## Go native
|
|
103
|
+
|
|
104
|
+
\`\`\`
|
|
105
|
+
lecodes app init ios # generate a native Xcode shell in ios/
|
|
106
|
+
lecodes app sync # embed the compiled bundle + plugins into it
|
|
107
|
+
lecodes app open # open it in Xcode, build & run
|
|
108
|
+
\`\`\`
|
|
109
|
+
|
|
110
|
+
## Layout
|
|
111
|
+
|
|
112
|
+
- \`main.ts\` — the app's entry point. Add more .ts files freely and import them from
|
|
113
|
+
\`main.ts\`; a file named \`main.ts\` is always the entry.
|
|
114
|
+
- \`tests/\` — headless flow tests (\`lecodes test\`); failing steps dump the frame to
|
|
115
|
+
\`tests/.artifacts/\`
|
|
116
|
+
- \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
|
|
117
|
+
- \`assets/\` — images/fonts the app references (created on demand)
|
|
118
|
+
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
|
|
119
|
+
They come from the SDK bundled in the CLI; \`lecodes update\` refreshes them from the platform.
|
|
120
|
+
- \`.lecodes/\` is gitignored — per-checkout state (those types + \`manifest.json\`, this machine's
|
|
121
|
+
sync cursor once linked). After a fresh git clone: \`lecodes update\`, then
|
|
122
|
+
\`lecodes link --project <uuid>\` to re-attach.
|
|
123
|
+
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
124
|
+
|
|
125
|
+
Full command list: \`lecodes --help\`.
|
|
126
|
+
`
|
|
127
|
+
|
|
128
|
+
export default defineCommand({
|
|
129
|
+
name: "init",
|
|
130
|
+
summary: "Scaffold a new LOCAL project (no login, no server)",
|
|
131
|
+
usage: "[dir]",
|
|
132
|
+
description: "Writes a starter main.ts, a CLAUDE.md + a passing flow test (the headless render / test loop), the IDE types + tsconfig, and a .lecodesignore. Run it with `lecodes dev` or `lecodes compile`. Idempotent: existing files are kept.",
|
|
133
|
+
flags: { desktop: bool("also write an app.json declaring the desktop target (window size; `dev --desktop`, `desktop run` and headless render / test pick it up)") },
|
|
134
|
+
examples: ["lecodes init myapp", "lecodes init --desktop"],
|
|
135
|
+
run: ({ args, flags }) => init(args[0], flags.desktop),
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const init = (dir: string | undefined, forDesktop: boolean): void => {
|
|
139
|
+
const dirArg = (dir ?? ".").replace(/[/\\]+$/, "") || "."
|
|
140
|
+
const root = resolve(dirArg)
|
|
141
|
+
const name = basename(root)
|
|
142
|
+
|
|
143
|
+
mkdirSync(root, { recursive: true })
|
|
144
|
+
|
|
145
|
+
let created = 0
|
|
146
|
+
const write = (rel: string, content: string) => {
|
|
147
|
+
const abs = join(root, rel)
|
|
148
|
+
if (existsSync(abs)) { note(`kept ${rel}`); return }
|
|
149
|
+
mkdirSync(dirname(abs), { recursive: true })
|
|
150
|
+
writeFileSync(abs, content)
|
|
151
|
+
log(`${c.green("created")} ${rel}`)
|
|
152
|
+
created++
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Starter entry — a root file named main.ts is always the detected entrypoint.
|
|
156
|
+
write("main.ts", STARTER)
|
|
157
|
+
write("README.md", readmeTemplate(name))
|
|
158
|
+
// --desktop: an app.json declaring the desktop target (window config in logical units).
|
|
159
|
+
if (forDesktop) write("app.json", appJsonDesktop(name))
|
|
160
|
+
// AI-assistant guidance + a passing flow test: the headless loop is discoverable from day one.
|
|
161
|
+
write("CLAUDE.md", appClaudeMd(name))
|
|
162
|
+
write(".claude/settings.json", CLAUDE_SETTINGS)
|
|
163
|
+
write("tests/counter.flow.json", STARTER_FLOW)
|
|
164
|
+
// Keep local render/preview outputs (screenshot.png, shots/) out of any future push.
|
|
165
|
+
write(".lecodesignore", DEFAULT_IGNORE_FILE)
|
|
166
|
+
// .lecodes/ is per-checkout state (generated types + the push cursor) — keep it out of git.
|
|
167
|
+
const gi = ensureGitignore(root)
|
|
168
|
+
if (gi === "created") { log(`${c.green("created")} .gitignore`); created++ }
|
|
169
|
+
else if (gi === "updated") log(`${c.green("updated")} .gitignore ${c.dim("(+ .lecodes/)")}`)
|
|
170
|
+
else note("kept .gitignore")
|
|
171
|
+
|
|
172
|
+
// IDE types: the SDK's import-free global surface + a tsconfig, so VS Code resolves UIScreen,
|
|
173
|
+
// Scene2D, setLoop, … with no server. Regenerated every run to pick up CLI upgrades.
|
|
174
|
+
const wroteTypes = materializeTypesLocal(root)
|
|
175
|
+
writeTsconfig(root)
|
|
176
|
+
log(`${c.green("written")} tsconfig.json${wroteTypes ? ` + ${LECODES_DIR}/types/` : ""} (IDE types)`)
|
|
177
|
+
if (!wroteTypes) {
|
|
178
|
+
warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
log("")
|
|
182
|
+
success(created
|
|
183
|
+
? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.`
|
|
184
|
+
: `Project ${c.bold(name)} already initialized.`)
|
|
185
|
+
const cd = dirArg === "." ? "" : `cd ${dirArg} && `
|
|
186
|
+
if (forDesktop) {
|
|
187
|
+
log(` ${c.bold(cd + "lecodes dev --desktop")} run it in a desktop window with hot reload`)
|
|
188
|
+
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (desktop-sized)`)
|
|
189
|
+
log(` ${c.bold(cd + "lecodes desktop run")} open the compiled app in the desktop host`)
|
|
190
|
+
} else {
|
|
191
|
+
log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`)
|
|
192
|
+
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`)
|
|
193
|
+
log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`)
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/commands/plugin.ts
CHANGED
|
@@ -14,17 +14,17 @@ const gen = defineCommand({
|
|
|
14
14
|
name: "gen",
|
|
15
15
|
summary: "Generate a plugin's files from its contract.d.ts (--check: fail on a stale one)",
|
|
16
16
|
usage: "[path]",
|
|
17
|
-
description: "The path is one plugin (a directory with a contract.d.ts) or a repo that keeps its plugins in plugins/<id>/; the current directory when omitted.\n\nWrites sdk/<id>.gen.ts — the wrapper an app calls: the wire call by call, the class with the wire's methods as its own, and the public export when the plugin has no hand-written sdk/<id>.ts. Where the plugin has an ios/ folder, writes ios/<Name>.gen.swift — the contract's types, the protocol the plugin implements and the glue that reads a call off the wire. Where it has an android/ folder, writes the same as <Name>.gen.kt into the folder of the plugin's Kotlin package (android/ is laid out as a Gradle module: the package is the folder the path under android/src/main/java narrows down to). Where it has a web/ folder, writes web/<id>.gen.ts — the same for the web half, in a file that imports nothing; a web half that imports a LeCodes package is refused, because it would run on that host only. A repo of plugins also gets its Package.swift from the manifests: the iOS halves as one SwiftPM package, a target and a product per plugin, the SDK as a package on disk (LECODES_IOS_SDK_PATH, else the repo's package.json lecodes.iosSdkPath, else ../lecodes) — what an Xcode project links the halves as (a shell vendors the sources instead). A contract the generator does not understand is an error with its line, and every problem of a file is reported at once.\n\nGenerated files are committed. Reading a contract needs TypeScript in the plugin's repo.",
|
|
17
|
+
description: "The path is one plugin (a directory with a contract.d.ts) or a repo that keeps its plugins in plugins/<id>/; the current directory when omitted.\n\nWrites sdk/<id>.gen.ts — the wrapper an app calls: the wire call by call, the class with the wire's methods as its own, and the public export when the plugin has no hand-written sdk/<id>.ts. Where the plugin has an ios/ folder, writes ios/<Name>.gen.swift — the contract's types, the protocol the plugin implements and the glue that reads a call off the wire. Where it has an android/ folder, writes the same as <Name>.gen.kt into the folder of the plugin's Kotlin package (android/ is laid out as a Gradle module: the package is the folder the path under android/src/main/java narrows down to). Where it has a web/ folder, writes web/<id>.gen.ts — the same for the web half, in a file that imports nothing; a web half that imports a LeCodes package is refused, because it would run on that host only. A repo of plugins also gets its Package.swift from the manifests: the iOS halves as one SwiftPM package, a target and a product per plugin, the SDK as a package on disk (LECODES_IOS_SDK_PATH, else the repo's package.json lecodes.iosSdkPath, else ../lecodes) — what an Xcode project links the halves as (a shell vendors the sources instead). A contract the generator does not understand is an error with its line, and every problem of a file is reported at once.\n\nA HALF FOLDER of a project — a manifest with `implements: \"<first-party id>\"`, no contract.d.ts and no sdk/ of its own — is generated from the contract of the plugin it implements (the one the SDK carries): `lecodes plugin gen plugins/ar-8thwall` writes web/ar.gen.ts, the half registers the service \"ar\", and app.json names the folder for that platform (`\"ar\": { \"web\": \"./plugins/ar-8thwall\" }`).\n\nGenerated files are committed. Reading a contract needs TypeScript in the plugin's repo.",
|
|
18
18
|
flags: {
|
|
19
19
|
check: bool("write nothing; exit 1 when a generated file is missing or stale"),
|
|
20
20
|
},
|
|
21
|
-
examples: ["lecodes plugin gen", "lecodes plugin gen plugins/camera", "lecodes plugin gen --check"],
|
|
21
|
+
examples: ["lecodes plugin gen", "lecodes plugin gen plugins/camera", "lecodes plugin gen plugins/ar-8thwall", "lecodes plugin gen --check"],
|
|
22
22
|
run: ({ args, flags }) => {
|
|
23
23
|
const report = generate(args[0] ?? ".", { check: flags.check })
|
|
24
24
|
|
|
25
25
|
for (const p of report.plugins) {
|
|
26
26
|
const channels = p.contract.channels.map(ch => `${ch.kind} "${ch.name}" v${ch.version}`).join(", ")
|
|
27
|
-
info(`${c.bold(p.id)} ${c.dim(channels)}`)
|
|
27
|
+
info(`${c.bold(p.id)} ${c.dim(channels)}${p.implements ? c.dim(` (implements ${p.implements}: the SDK's contract)`) : ""}`)
|
|
28
28
|
for (const f of p.written) log(c.green(` written ${f}`))
|
|
29
29
|
for (const f of p.stale) log(c.red(` stale ${f}`))
|
|
30
30
|
for (const f of p.fresh) log(c.dim(` fresh ${f}`))
|
package/src/commands/push.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { basename } from "node:path"
|
|
|
3
3
|
import { CliError, bool, c, confirm, defineCommand, info, log, prompt, str, success, warn } from "../cli"
|
|
4
4
|
import { getCommits, getProject, publishProject, sync, uploadBlob, type Project, type SyncFileInput, type SyncFolderInput, type SyncResponse } from "../platform/api"
|
|
5
5
|
import { computeServerDiff } from "../platform/serverDiff"
|
|
6
|
-
import {
|
|
6
|
+
import { diffLocal, hasChanges, isTextKind, scanProject } from "../project/localFiles"
|
|
7
7
|
import { writeManifest, type FileKind, type Manifest, type ManifestFile } from "../project/manifest"
|
|
8
8
|
import { buildPaths } from "../project/materialize"
|
|
9
9
|
import { qrUrl, reportRelease, withSchemaConfirm } from "./releases"
|
|
@@ -107,7 +107,7 @@ export const runPush = async (opts: PushOptions): Promise<void> => {
|
|
|
107
107
|
let uploaded = 0
|
|
108
108
|
for (const file of scan.files) {
|
|
109
109
|
const source = sourceFor(file.path)
|
|
110
|
-
const kind: FileKind = source?.type ??
|
|
110
|
+
const kind: FileKind = source?.type ?? file.kind
|
|
111
111
|
const entry: SyncFileInput = { path: file.path, type: kind }
|
|
112
112
|
if (source) entry.assetId = source.id
|
|
113
113
|
if (isTextKind(kind)) entry.text = readFileSync(file.absPath, "utf8")
|
package/src/commands/shared.ts
CHANGED
|
@@ -1,79 +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
|
+
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"
|