lecodes-cli 2.3.0 → 2.4.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.
@@ -100,14 +100,21 @@ export interface AppConfig {
100
100
  /** 3D buffer cap in px, e.g. [1920, 1080]: on a bigger window/monitor the 3D renders at most
101
101
  * this size (aspect kept) — the per-machine answer to "fullscreen on a 4K display is slow". */
102
102
  maxRenderSize?: [number, number],
103
- /** Frame-rate CAP for the host's render loop: `"display"` (default — follow the screen's refresh
104
- * rate, 120 on a ProMotion Mac) or a number, meaning "at most this many fps" (the screen's
105
- * maximum still wins when it is lower — vsync is the real ceiling, so this can never be a
106
- * target above it). For an app that wants to spend less: a UI-heavy app on a laptop, a game
107
- * whose feel is tuned for 60. `desktop run` forwards it as CREATOR_FRAME_RATE (a set env wins);
108
- * a built .app's host reads it from the staged app.json. Honoured by the macOS host
109
- * (2026-09-09); the Windows/Linux host is vsync-locked and ignores it for now. */
103
+ /** Frame-rate CAP the host starts with: a number = "at most this many fps", `0` or `"display"`
104
+ * (the default) = no cap — the display's own rate, or, with vsync off, as fast as the host
105
+ * renders. The app changes it at runtime with `device.setFrameRate()`; this is the value in
106
+ * force before the bundle runs. For an app that wants to spend less: a UI-heavy app on a
107
+ * laptop, a game whose feel is tuned for 60. `desktop run` / `dev --desktop` forward it as
108
+ * CREATOR_FRAME_RATE (a set env wins); a built folder's host reads it from the staged app.json.
109
+ * Headless render/test ignore it (their frames are app-time steps). */
110
110
  frameRate?: number | "display",
111
+ /** Vertical sync the host starts with (default true). `false` presents without waiting for the
112
+ * display's refresh: the lowest input latency, tearing possible — and with no `frameRate` cap
113
+ * the loop renders as fast as it can, so set one unless you are measuring. The app switches it
114
+ * at runtime with `device.setVsync()`. Forwarded as CREATOR_VSYNC (a set env wins), read from
115
+ * the staged app.json by a built folder's host. Windows (GL: the swap interval; Vulkan: the
116
+ * immediate present mode); the macOS and Linux hosts ignore it for now. */
117
+ vsync?: boolean,
111
118
  /** The mouse button acts as a finger: a left click-drag scrolls + flings like touch (the wheel
112
119
  * always scrolls, clicks are unaffected; a button inside a scrollable then gets the touch-style
113
120
  * press feedback delay). For testing the touch experience without a touchscreen, or an app
@@ -93,8 +93,9 @@ const run = defineCommand({
93
93
  // from app.json only. Both are desktop-level (the machine/app), not window-level.
94
94
  renderScale: resolveRenderScale(flags["render-scale"], appCfg?.desktop?.renderScale),
95
95
  maxRenderSize: appCfg?.desktop?.maxRenderSize,
96
- // Frame-rate cap (app.json only — like renderScale it is a property of the app, not of a run).
96
+ // Frame-rate cap + vsync (app.json only — like renderScale they are properties of the app, not of a run).
97
97
  frameRate: appCfg?.desktop?.frameRate,
98
+ vsync: appCfg?.desktop?.vsync,
98
99
  mouseEmulateTouch: appCfg?.desktop?.mouseEmulateTouch,
99
100
  virtualKeyboard: appCfg?.desktop?.virtualKeyboard,
100
101
  // --fps: the host's own corner counter. A flag and not app.json on purpose — it is something you
@@ -300,6 +300,7 @@ export default defineCommand({
300
300
  scale: resolveScale(undefined, win?.scale),
301
301
  // The frame-rate cap is forwarded too: it changes how the game FEELS, and dev should match production.
302
302
  frameRate: appCfg?.desktop?.frameRate,
303
+ vsync: appCfg?.desktop?.vsync,
303
304
  fps: flags.fps,
304
305
  }).then(
305
306
  () => note("Desktop window closed — the dev server keeps running (Ctrl+C to stop)."),
@@ -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
+ }
@@ -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"