lecodes-cli 0.20.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -57
- package/dist/index.js +5920 -5022
- package/package.json +9 -7
- package/runtime/materials/decal-relief.mat +12 -1
- package/runtime/materials/decal.mat +17 -3
- package/runtime/materials/lightmap-baked-lite.mat +6 -1
- package/runtime/materials/lightmap-baked.mat +6 -1
- package/runtime/materials/lightmap.mat +3 -0
- package/runtime/materials/particles-quad.mat +17 -9
- package/runtime/materials/particles.mat +17 -9
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/assets/createViewerLite-DHqTFhWB.js +866 -0
- package/runtime/web/assets/{index-BTB5KV9x.js → index-Q06c6oHx.js} +1 -1
- package/runtime/web/embed.js +2 -2
- 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 +133 -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} +451 -42
- package/src/commands/{appDesktop.ts → app/desktop.ts} +63 -32
- package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +24 -18
- package/src/commands/{appIcon.ts → app/icon.ts} +2 -2
- package/src/commands/{app.ts → app/index.ts} +397 -289
- package/src/commands/{appShared.ts → app/shared.ts} +30 -39
- package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +62 -28
- package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
- package/src/commands/{appTemplates.ts → app/templates/ios.ts} +1 -1
- package/src/commands/assets.ts +23 -21
- package/src/commands/clone.ts +48 -51
- package/src/commands/compile.ts +62 -88
- 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 +90 -82
- package/src/commands/dev.ts +201 -176
- package/src/commands/diff.ts +39 -51
- package/src/commands/index.ts +60 -0
- package/src/commands/{init.ts → init/index.ts} +199 -191
- package/src/commands/install.ts +80 -67
- package/src/commands/lightmap.ts +203 -171
- package/src/commands/link.ts +87 -94
- package/src/commands/login.ts +25 -23
- package/src/commands/navmesh.ts +168 -138
- 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 +158 -99
- package/src/commands/scene.ts +58 -55
- package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
- package/src/commands/{shadersNew.ts → shaders/new.ts} +2 -2
- package/src/commands/shared.ts +79 -0
- package/src/commands/status.ts +22 -26
- package/src/commands/test.ts +398 -371
- 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 +3 -3
- package/src/compile/headlessBundle.ts +133 -129
- package/src/compile/projectCompile.ts +2 -2
- package/src/compile/sceneCompile.ts +10 -8
- package/src/compile/screenEntry.ts +123 -127
- package/src/compile/shaders.ts +3 -3
- package/src/dev/androidDev.ts +1 -1
- package/src/dev/devServer.ts +3 -3
- package/src/dev/webRunner.ts +1 -1
- package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +1 -1
- package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +6 -5
- package/src/{desktopScript.ts → hosts/desktopScript.ts} +1 -1
- package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
- package/src/{matcTool.ts → hosts/matcTool.ts} +1 -1
- package/src/{peerInstall.ts → hosts/peerInstall.ts} +2 -2
- package/src/{releases.ts → hosts/releases.ts} +1 -1
- package/src/index.ts +32 -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/{projectEnv.ts → project/env.ts} +33 -2
- 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/web/assets/createViewerLite-C_bkKXLg.js +0 -852
- package/src/commands/design.ts +0 -846
- package/src/commands/update.ts +0 -211
- package/src/util.ts +0 -146
- /package/src/commands/{projectTemplates.ts → init/templates.ts} +0 -0
- /package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +0 -0
- /package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +0 -0
- /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
- /package/src/{lecodes-renderer.d.ts → declarations/lecodes-renderer.d.ts} +0 -0
- /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
- /package/src/{peers.ts → hosts/peers.ts} +0 -0
- /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
- /package/src/{ignore.ts → project/ignore.ts} +0 -0
package/src/commands/update.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process"
|
|
2
|
-
import { existsSync, readdirSync } from "node:fs"
|
|
3
|
-
import { homedir } from "node:os"
|
|
4
|
-
import { dirname, join, resolve } from "node:path"
|
|
5
|
-
import pkg from "../../package.json"
|
|
6
|
-
import { CliError, c, flagBool, log, note, type Args } from "../util"
|
|
7
|
-
import { OPTIONAL_PEERS, peerSource, peerVersion } from "../peers"
|
|
8
|
-
import { installPeers } from "../peerInstall"
|
|
9
|
-
import { updateDesktop } from "../desktopRenderer"
|
|
10
|
-
import { updateMatc } from "../matcTool"
|
|
11
|
-
import { types } from "./types"
|
|
12
|
-
import { shaders } from "./shaders"
|
|
13
|
-
import { app } from "./app"
|
|
14
|
-
|
|
15
|
-
/*
|
|
16
|
-
* `lecodes update` — one command that moves EVERYTHING a project depends on to its newest published
|
|
17
|
-
* state, in the order the pieces depend on each other:
|
|
18
|
-
*
|
|
19
|
-
* 1. desktop the LeCodes Desktop host `desktop run` / `render --desktop` use (= `desktop update`)
|
|
20
|
-
* 2. matc Filament's material compiler for local shader compiles (= `shaders update`)
|
|
21
|
-
* 3. shaders recompile the project's .mat files with that matc (= `shaders compile`)
|
|
22
|
-
* 4. types the IDE types (.lecodes/types + tsconfig) from the server (= `types`)
|
|
23
|
-
* 5. shells ios/ + android/ pins → newest SDK release + plugin tags, re-sync (= `app update`)
|
|
24
|
-
* 6. cli the npm installs — every installed optional peer (lecodes-renderer, lecodes-design,
|
|
25
|
-
* lecodes-3d-editor, lecodes-assets, @napi-rs/canvas) and the CLI itself that is behind:
|
|
26
|
-
* peers in ~/.lecodes/peers (first-use installs, peerInstall.ts) are re-installed there
|
|
27
|
-
* — the renderer at THIS CLI's version, the rest at npm's latest in range; global ones
|
|
28
|
-
* (`npm install -g`, the old way) via `npm install -g …@latest`; the CLI itself last
|
|
29
|
-
* (the standalone binary can't come from npm — it says so and does the peers only)
|
|
30
|
-
*
|
|
31
|
-
* Every step is the existing subcommand, so nothing here can drift from what the individual
|
|
32
|
-
* commands do; this only sequences them and keeps going when one fails (the summary says which).
|
|
33
|
-
* Steps that the environment pins are SKIPPED, not overridden: LECODES_DESKTOP_EXE means a local
|
|
34
|
-
* host build (engine work — the cache is irrelevant), LECODES_MATC a local matc; the project steps
|
|
35
|
-
* need a project (a dir with app.json / .lecodes above the cwd) and the shell step a shell folder.
|
|
36
|
-
*
|
|
37
|
-
* Flags: --local (types from the SDK bundled in this CLI instead of the server), --no-shells (leave
|
|
38
|
-
* ios/android pins alone — `app update` re-syncs, which compiles the bundle and, for server projects,
|
|
39
|
-
* needs a login), --check (the npm step only REPORTS what is behind and prints the install line),
|
|
40
|
-
* --no-cli (skip the npm step — offline / air-gapped machines).
|
|
41
|
-
*
|
|
42
|
-
* `lecodes types` used to be its own command; the types step here replaced it (2026-09-04).
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
/** The nearest project at or above `start`: a dir with app.json, or a .lecodes/ that holds a project's
|
|
46
|
-
* manifest or types. NOT the bare `.lecodes/` marker projectEnv uses — the CLI's own config lives in
|
|
47
|
-
* ~/.lecodes, so that rule would turn any folder under the home dir into a "project" and the types
|
|
48
|
-
* step would scaffold .lecodes/types + tsconfig.json into it. */
|
|
49
|
-
const findProjectDir = (start: string): string | null => {
|
|
50
|
-
const home = resolve(homedir())
|
|
51
|
-
let dir = resolve(start)
|
|
52
|
-
while (true) {
|
|
53
|
-
if (dir !== home && (existsSync(join(dir, "app.json")) || existsSync(join(dir, ".lecodes", "manifest.json")) || existsSync(join(dir, ".lecodes", "types")))) return dir
|
|
54
|
-
const parent = dirname(dir)
|
|
55
|
-
if (parent === dir) return null
|
|
56
|
-
dir = parent
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
type Outcome = "updated" | "ok" | "skipped" | "failed"
|
|
61
|
-
type Step = { name: string, outcome: Outcome, detail?: string }
|
|
62
|
-
|
|
63
|
-
const NPM = "https://registry.npmjs.org"
|
|
64
|
-
|
|
65
|
-
/** `latest` dist-tag of an npm package, or null when npm is unreachable / the package is unknown. */
|
|
66
|
-
const npmLatest = async (name: string): Promise<string | null> => {
|
|
67
|
-
try {
|
|
68
|
-
const res = await fetch(`${NPM}/${name}/latest`, { signal: AbortSignal.timeout(4000) })
|
|
69
|
-
if (!res.ok) return null
|
|
70
|
-
const json = await res.json() as { version?: string }
|
|
71
|
-
return json.version ?? null
|
|
72
|
-
} catch {
|
|
73
|
-
return null
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const semverNewer = (a: string, b: string): number => {
|
|
78
|
-
const pa = a.split(/[.-]/).map(Number), pb = b.split(/[.-]/).map(Number)
|
|
79
|
-
for (let i = 0; i < 3; i++) { const d = (pa[i] ?? 0) - (pb[i] ?? 0); if (d) return d }
|
|
80
|
-
return 0
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Running straight from the repo checkout (src/commands/update.ts), not a built dist/binary. */
|
|
84
|
-
const FROM_SOURCE = /[\\/]src[\\/]commands[\\/]/.test(import.meta.url)
|
|
85
|
-
/** The standalone release build: the bundle + chisel sit next to the executable (same test as index.ts). */
|
|
86
|
-
const STANDALONE = existsSync(join(dirname(process.execPath), "runtime")) &&
|
|
87
|
-
existsSync(join(dirname(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel"))
|
|
88
|
-
|
|
89
|
-
export const update = async (args: Args) => {
|
|
90
|
-
const steps: Step[] = []
|
|
91
|
-
const heading = (name: string, what: string) => log(`\n${c.bold(name)} ${c.dim(what)}`)
|
|
92
|
-
const run = async (name: string, what: string, fn: () => Promise<Outcome | [Outcome, string]>) => {
|
|
93
|
-
heading(name, what)
|
|
94
|
-
try {
|
|
95
|
-
const r = await fn()
|
|
96
|
-
const [outcome, detail] = Array.isArray(r) ? r : [r, undefined]
|
|
97
|
-
steps.push({ name, outcome, detail })
|
|
98
|
-
if (outcome === "skipped" && detail) note(`skipped — ${detail}`)
|
|
99
|
-
} catch (e) {
|
|
100
|
-
const msg = e instanceof CliError ? e.message : e instanceof Error ? e.message : String(e)
|
|
101
|
-
log(c.red("✗ ") + msg)
|
|
102
|
-
steps.push({ name, outcome: "failed", detail: msg })
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const root = findProjectDir(process.cwd())
|
|
107
|
-
|
|
108
|
-
// 1. desktop host
|
|
109
|
-
await run("desktop", "LeCodes Desktop host (desktop run / render --desktop)", async () => {
|
|
110
|
-
if (process.env.LECODES_DESKTOP_EXE) return ["skipped", `LECODES_DESKTOP_EXE=${process.env.LECODES_DESKTOP_EXE} (a local build; \`lecodes desktop update\` refreshes the cache anyway)`]
|
|
111
|
-
await updateDesktop()
|
|
112
|
-
return "ok"
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
// 2. matc
|
|
116
|
-
await run("matc", "Filament material compiler for local shader compiles", async () => {
|
|
117
|
-
if (process.env.LECODES_MATC) return ["skipped", `LECODES_MATC=${process.env.LECODES_MATC} (a local matc)`]
|
|
118
|
-
await updateMatc()
|
|
119
|
-
return "ok"
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
// 3. shaders (project)
|
|
123
|
-
await run("shaders", "recompile the project's .mat shaders with the current matc", async () => {
|
|
124
|
-
if (!root) return ["skipped", "not inside a project"]
|
|
125
|
-
await shaders({ _: ["compile"], flags: {} })
|
|
126
|
-
return "ok"
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
// 4. types (project)
|
|
130
|
-
await run("types", flagBool(args, "local") ? "IDE types from the SDK bundled in this CLI" : "IDE types from the server", async () => {
|
|
131
|
-
if (!root) return ["skipped", "not inside a project"]
|
|
132
|
-
await types({ _: [], flags: flagBool(args, "local") ? { local: true } : {} })
|
|
133
|
-
return "ok"
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
// 5. native shells (project)
|
|
137
|
-
await run("shells", "ios/ + android/ pins → newest SDK release + plugin tags, then re-sync", async () => {
|
|
138
|
-
if (!root) return ["skipped", "not inside a project"]
|
|
139
|
-
if (flagBool(args, "no-shells")) return ["skipped", "--no-shells"]
|
|
140
|
-
const present = ["ios", "android"].filter((d) => existsSync(join(root!, d)) && readdirSync(join(root!, d)).length > 0)
|
|
141
|
-
if (present.length === 0) return ["skipped", "no ios/ or android/ shell in this project (`lecodes app init ios|android`)"]
|
|
142
|
-
await app({ _: ["update"], flags: {} })
|
|
143
|
-
return "ok"
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
// 6. this CLI + its optional peers — the GLOBAL npm installs (`npm install -g`, how the CLI and its
|
|
147
|
-
// peers are installed): every installed one that is behind npm's latest moves forward. The peers go
|
|
148
|
-
// first, the CLI itself last — replacing the running CLI's files is fine (this process already
|
|
149
|
-
// holds its code), but its new version is what the next invocation runs, so nothing after it here.
|
|
150
|
-
// Peers that are NOT installed stay that way: they're optional, installing one is a choice.
|
|
151
|
-
await run("cli", "lecodes-cli + its optional peers (global npm installs)", async () => {
|
|
152
|
-
if (flagBool(args, "no-cli")) return ["skipped", "--no-cli"]
|
|
153
|
-
if (FROM_SOURCE) return ["skipped", "running from the repo checkout — the CLI and its peers are the repo's own"]
|
|
154
|
-
// Where each peer lives decides how it moves: the CLI's own prefix (~/.lecodes/peers, filled on
|
|
155
|
-
// first use) is re-installed by peerInstall — the renderer there tracks THIS CLI's version, not
|
|
156
|
-
// npm's latest, because compiler and headless host are one ABI set; a global install
|
|
157
|
-
// (`npm install -g`, the old way) moves through npm like before.
|
|
158
|
-
const rows: { name: string, installed: string | null, latest: string | null, where: "global" | "prefix" | null }[] = []
|
|
159
|
-
for (const [name, probe] of OPTIONAL_PEERS) {
|
|
160
|
-
const where = peerSource(name, probe)
|
|
161
|
-
const installed = peerVersion(name, probe)
|
|
162
|
-
const latest = !installed ? null
|
|
163
|
-
: where === "prefix" && name === "lecodes-renderer" ? pkg.version
|
|
164
|
-
: await npmLatest(name)
|
|
165
|
-
rows.push({ name, installed, latest, where })
|
|
166
|
-
}
|
|
167
|
-
rows.push({ name: "lecodes-cli", installed: pkg.version, latest: await npmLatest("lecodes-cli"), where: "global" })
|
|
168
|
-
if (rows.every((r) => r.latest === null)) return ["skipped", "npm unreachable"]
|
|
169
|
-
const behind = rows.filter((r) => r.installed && r.latest && semverNewer(r.latest, r.installed) > 0)
|
|
170
|
-
for (const r of rows) {
|
|
171
|
-
const state = !r.installed ? c.dim("not installed (optional — installed on first use)")
|
|
172
|
-
: !r.latest ? c.dim("(npm: unknown)")
|
|
173
|
-
: behind.includes(r) ? c.yellow("→ " + r.latest)
|
|
174
|
-
: c.dim("(up to date)")
|
|
175
|
-
log(" " + r.name.padEnd(18) + " " + (r.installed ?? "").padEnd(8) + " " + (r.where === "prefix" ? c.dim("peers ") : "") + state)
|
|
176
|
-
}
|
|
177
|
-
if (behind.length === 0) return "ok"
|
|
178
|
-
const prefixBehind = behind.filter((r) => r.where === "prefix")
|
|
179
|
-
const globalSpecs = behind.filter((r) => r.where !== "prefix").map((r) => r.name + "@latest")
|
|
180
|
-
if (flagBool(args, "check")) {
|
|
181
|
-
return ["updated", behind.length + " behind — run: lecodes update" + (globalSpecs.length ? " (global: npm install -g " + globalSpecs.join(" ") + ")" : "")]
|
|
182
|
-
}
|
|
183
|
-
const done: string[] = []
|
|
184
|
-
if (prefixBehind.length) {
|
|
185
|
-
await installPeers(prefixBehind.map((r) => r.name), "lecodes update", { yes: true })
|
|
186
|
-
done.push(...prefixBehind.map((r) => r.name))
|
|
187
|
-
}
|
|
188
|
-
if (STANDALONE && globalSpecs.some((s) => s.startsWith("lecodes-cli@"))) {
|
|
189
|
-
note("this is the standalone build — the CLI updates by re-installing the release archive, not through npm; peers still go through npm")
|
|
190
|
-
}
|
|
191
|
-
const toInstall = STANDALONE ? globalSpecs.filter((s) => !s.startsWith("lecodes-cli@")) : globalSpecs
|
|
192
|
-
if (toInstall.length) {
|
|
193
|
-
log(" " + c.dim("$") + " npm install -g " + toInstall.join(" "))
|
|
194
|
-
const r = spawnSync("npm", ["install", "-g", ...toInstall], { stdio: "inherit", shell: true })
|
|
195
|
-
if (r.status !== 0) throw new CliError("npm install -g exited with " + (r.status ?? "a signal"))
|
|
196
|
-
done.push(...toInstall.map((s) => s.replace("@latest", "")))
|
|
197
|
-
}
|
|
198
|
-
if (done.length === 0) return ["skipped", "only the standalone CLI itself is behind — download the release"]
|
|
199
|
-
const cliToo = done.includes("lecodes-cli")
|
|
200
|
-
return ["updated", done.join(", ") + " → latest" + (cliToo ? " (the new CLI runs from your next command)" : "")]
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
// Summary — one line per step so a scrolled-away failure can't hide.
|
|
204
|
-
log(`\n${c.bold("Summary")}`)
|
|
205
|
-
const width = Math.max(...steps.map((s) => s.name.length))
|
|
206
|
-
for (const s of steps) {
|
|
207
|
-
const mark = s.outcome === "failed" ? c.red("✗") : s.outcome === "skipped" ? c.dim("–") : c.green("✓")
|
|
208
|
-
log(` ${mark} ${s.name.padEnd(width)} ${s.outcome === "failed" ? c.red(s.detail ?? "failed") : s.outcome === "skipped" ? c.dim(s.detail ?? "skipped") : s.detail ?? s.outcome}`)
|
|
209
|
-
}
|
|
210
|
-
if (steps.some((s) => s.outcome === "failed")) process.exitCode = 1
|
|
211
|
-
}
|
package/src/util.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto"
|
|
2
|
-
import { createInterface } from "node:readline"
|
|
3
|
-
import { sanitizeAssetName } from "lecodes-sdk/compile/assetName"
|
|
4
|
-
|
|
5
|
-
/* Small dependency-free helpers shared across commands: colored logging, prompts, hashing,
|
|
6
|
-
* posix-path normalization and a minimal argv parser. */
|
|
7
|
-
|
|
8
|
-
const useColor = process.stdout.isTTY && process.env.NO_COLOR === undefined
|
|
9
|
-
const paint = (code: string, s: string) => useColor ? `\x1b[${code}m${s}\x1b[0m` : s
|
|
10
|
-
export const c = {
|
|
11
|
-
bold: (s: string) => paint("1", s),
|
|
12
|
-
dim: (s: string) => paint("2", s),
|
|
13
|
-
red: (s: string) => paint("31", s),
|
|
14
|
-
green: (s: string) => paint("32", s),
|
|
15
|
-
yellow: (s: string) => paint("33", s),
|
|
16
|
-
cyan: (s: string) => paint("36", s),
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const log = (msg = ""): void => { process.stdout.write(msg + "\n") }
|
|
20
|
-
export const info = (msg: string) => log(msg)
|
|
21
|
-
export const success = (msg: string) => log(c.green("✓ ") + msg)
|
|
22
|
-
export const warn = (msg: string) => log(c.yellow("! ") + msg)
|
|
23
|
-
|
|
24
|
-
// stderr variants — for commands that write a result (JS, data) to stdout, so diagnostics never
|
|
25
|
-
// pollute the piped output.
|
|
26
|
-
export const logErr = (msg = "") => process.stderr.write(msg + "\n")
|
|
27
|
-
export const note = (msg: string) => logErr(c.dim(msg))
|
|
28
|
-
export const warnErr = (msg: string) => logErr(c.yellow("! ") + msg)
|
|
29
|
-
|
|
30
|
-
/** Keep a label short, dropping characters from the FRONT (paths matter most at the end). */
|
|
31
|
-
const ellipsize = (s: string, max: number) => s.length <= max ? s : "…" + s.slice(s.length - max + 1)
|
|
32
|
-
|
|
33
|
-
/** A single, in-place-updating progress line (TTY only; a no-op when piped). */
|
|
34
|
-
export const progress = (done: number, total: number, label = "") => {
|
|
35
|
-
if (!process.stdout.isTTY) return
|
|
36
|
-
const pct = total > 0 ? Math.round((done / total) * 100) : 100
|
|
37
|
-
process.stdout.clearLine(0)
|
|
38
|
-
process.stdout.cursorTo(0)
|
|
39
|
-
process.stdout.write(` ${c.cyan(`${pct}`.padStart(3) + "%")} ${c.dim(`(${done}/${total})`)} ${c.dim(ellipsize(label, 48))}`)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Finish a progress line with a newline so later output starts cleanly. */
|
|
43
|
-
export const endProgress = () => { if (process.stdout.isTTY) process.stdout.write("\n") }
|
|
44
|
-
|
|
45
|
-
/** A user-facing error: index.ts prints its message without a stack trace and exits non-zero. */
|
|
46
|
-
export class CliError extends Error {
|
|
47
|
-
/** HTTP status, when the failure came from the API. Lets a caller tell "already gone" (404) from
|
|
48
|
-
* "refused" (403) — deleting something the server no longer has is a success, not an error. */
|
|
49
|
-
status?: number
|
|
50
|
-
constructor(message: string, status?: number) {
|
|
51
|
-
super(message)
|
|
52
|
-
this.status = status
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Output targets that mean "discard" — so `-o /dev/null` (or Windows NUL) doesn't create a real file.
|
|
57
|
-
const NULL_SINKS = new Set(["/dev/null", "nul", "//./nul"])
|
|
58
|
-
/** True if an `--out` value is a null device, i.e. the output should be thrown away, not written. */
|
|
59
|
-
export const isNullSink = (path: string): boolean =>
|
|
60
|
-
NULL_SINKS.has(path.trim().replace(/\\/g, "/").toLowerCase())
|
|
61
|
-
|
|
62
|
-
export const sha256 = (data: string | Uint8Array): string =>
|
|
63
|
-
createHash("sha256").update(data).digest("hex")
|
|
64
|
-
|
|
65
|
-
/** Normalize any path to a leading-slash, forward-slash, "." / ".." free key (e.g. "/a/b.ts").
|
|
66
|
-
* Each segment gets the platform's canonical spelling (sdk/compile/assetName) — macOS hands out
|
|
67
|
-
* decomposed names ("й" as и + U+0306) from readdir, which would otherwise key a file differently
|
|
68
|
-
* than the same name typed in an import or stored on the server. */
|
|
69
|
-
export const toKey = (path: string): string => {
|
|
70
|
-
const parts = path.replace(/\\/g, "/").split("/").map(p => p.trim()).filter(p => p && p !== ".")
|
|
71
|
-
if (parts.some(p => p === "..")) throw new CliError(`Unsafe path: ${path}`)
|
|
72
|
-
const names = parts.map(p => sanitizeAssetName(p))
|
|
73
|
-
if (names.some(name => !name)) throw new CliError(`Wrong path: ${path}`)
|
|
74
|
-
return "/" + names.join("/")
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const prompt = (question: string, fallback?: string): Promise<string> =>
|
|
78
|
-
new Promise(resolve => {
|
|
79
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
80
|
-
const suffix = fallback ? c.dim(` (${fallback})`) : ""
|
|
81
|
-
rl.question(`${question}${suffix}: `, answer => {
|
|
82
|
-
rl.close()
|
|
83
|
-
resolve(answer.trim() || fallback || "")
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
/** Prompt without echoing the input (for passwords / token paste). */
|
|
88
|
-
export const promptHidden = (question: string): Promise<string> =>
|
|
89
|
-
new Promise(resolve => {
|
|
90
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true })
|
|
91
|
-
rl.question(`${question}: `, answer => {
|
|
92
|
-
rl.close()
|
|
93
|
-
process.stdout.write("\n")
|
|
94
|
-
resolve(answer.trim())
|
|
95
|
-
})
|
|
96
|
-
// Mute everything written after the prompt line was emitted (the keystroke echoes).
|
|
97
|
-
;(rl as unknown as { _writeToOutput: (s: string) => void })._writeToOutput = () => {}
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
export const confirm = async (question: string, fallback = false): Promise<boolean> => {
|
|
101
|
-
const answer = (await prompt(`${question} ${fallback ? "[Y/n]" : "[y/N]"}`)).toLowerCase()
|
|
102
|
-
if (!answer) return fallback
|
|
103
|
-
return answer === "y" || answer === "yes"
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export type Args = { _: string[], flags: Record<string, string | boolean> }
|
|
107
|
-
|
|
108
|
-
/** Parse argv into positionals + flags. Supports `--key value`, `--key=value`, `--bool`, `-m msg`. */
|
|
109
|
-
// `--look-at -0.15,1,2` / `--scale -1`: a token that starts with "-" followed by a digit (or ".5")
|
|
110
|
-
// is a negative NUMBER (or a vector starting with one), not the next flag.
|
|
111
|
-
const isNegativeNumber = (s: string): boolean => /^-(\d|\.\d)/.test(s)
|
|
112
|
-
const isValue = (s: string): boolean => !s.startsWith("-") || isNegativeNumber(s)
|
|
113
|
-
|
|
114
|
-
export const parseArgs = (argv: string[]): Args => {
|
|
115
|
-
const out: Args = { _: [], flags: {} }
|
|
116
|
-
for (let i = 0; i < argv.length; i++) {
|
|
117
|
-
const arg = argv[i]
|
|
118
|
-
if (arg.startsWith("--")) {
|
|
119
|
-
const eq = arg.indexOf("=")
|
|
120
|
-
if (eq >= 0) { out.flags[arg.slice(2, eq)] = arg.slice(eq + 1); continue }
|
|
121
|
-
const key = arg.slice(2)
|
|
122
|
-
const next = argv[i + 1]
|
|
123
|
-
if (next !== undefined && isValue(next)) { out.flags[key] = next; i++ }
|
|
124
|
-
else out.flags[key] = true
|
|
125
|
-
} else if (arg.startsWith("-") && arg.length > 1 && !isNegativeNumber(arg)) {
|
|
126
|
-
const key = arg.slice(1)
|
|
127
|
-
const next = argv[i + 1]
|
|
128
|
-
if (next !== undefined && isValue(next)) { out.flags[key] = next; i++ }
|
|
129
|
-
else out.flags[key] = true
|
|
130
|
-
} else {
|
|
131
|
-
out._.push(arg)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return out
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export const flagStr = (args: Args, ...keys: string[]): string | undefined => {
|
|
138
|
-
for (const key of keys) {
|
|
139
|
-
const v = args.flags[key]
|
|
140
|
-
if (typeof v === "string") return v
|
|
141
|
-
}
|
|
142
|
-
return undefined
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export const flagBool = (args: Args, ...keys: string[]): boolean =>
|
|
146
|
-
keys.some(key => args.flags[key] === true || args.flags[key] === "true")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|