lecodes-cli 0.20.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -57
- package/dist/index.js +11052 -7592
- package/package.json +14 -8
- package/runtime/materials/decal-relief.mat +12 -1
- package/runtime/materials/decal.mat +17 -3
- package/runtime/materials/lightmap.mat +117 -138
- package/runtime/materials/particles-quad.mat +6 -2
- package/runtime/materials/terrain-lightmap.mat +123 -0
- package/runtime/materials/terrain.mat +36 -55
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +5 -0
- package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
- package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
- package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
- package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
- package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
- package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
- package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
- package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
- package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
- package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
- package/runtime/web/assets/map-CyBxOSV0.js +1 -0
- package/runtime/web/assets/map-XzoOFt0E.js +2 -0
- package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
- package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
- package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
- package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
- package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
- package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
- package/runtime/web/index.html +36 -0
- package/runtime/web/player.js +21 -0
- package/runtime/web-page/index.html +41 -0
- package/runtime/web-page/page.css +1 -0
- package/runtime/web-page/page.js +31 -0
- package/src/api.ts +1 -302
- package/src/cli/args.ts +37 -0
- package/src/cli/command.ts +113 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/help.ts +158 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/output.ts +102 -0
- package/src/cli/run.ts +106 -0
- package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
- package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
- package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
- package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
- package/src/commands/{app.ts → app/index.ts} +400 -290
- package/src/commands/{appShared.ts → app/shared.ts} +43 -44
- package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
- package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
- package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
- package/src/commands/assets.ts +23 -21
- package/src/commands/clone.ts +48 -51
- package/src/commands/compile.ts +76 -89
- package/src/commands/create.ts +46 -47
- package/src/commands/design/comments.ts +361 -0
- package/src/commands/design/context.ts +101 -0
- package/src/commands/design/index.ts +391 -0
- package/src/commands/design/snapshot.ts +134 -0
- package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
- package/src/commands/desktop.ts +94 -82
- package/src/commands/dev.ts +260 -182
- package/src/commands/diff.ts +39 -51
- package/src/commands/index.ts +64 -0
- package/src/commands/{init.ts → init/index.ts} +195 -191
- package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
- package/src/commands/install.ts +80 -67
- package/src/commands/lightmap.ts +543 -252
- package/src/commands/link.ts +87 -94
- package/src/commands/login.ts +25 -23
- package/src/commands/navmesh.ts +169 -139
- package/src/commands/plugin.ts +46 -0
- package/src/commands/pn.ts +201 -244
- package/src/commands/pull.ts +49 -56
- package/src/commands/push.ts +48 -54
- package/src/commands/render.ts +176 -111
- package/src/commands/scene.ts +63 -55
- package/src/commands/serve.ts +58 -0
- package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
- package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
- package/src/commands/shared.ts +79 -0
- package/src/commands/status.ts +22 -26
- package/src/commands/test.ts +94 -63
- package/src/commands/thumbs.ts +185 -175
- package/src/commands/update/index.ts +226 -0
- package/src/commands/{types.ts → update/types.ts} +31 -22
- package/src/compile/collect.ts +5 -5
- package/src/compile/collectLocal.ts +3 -3
- package/src/compile/designCompile.ts +4 -4
- package/src/compile/headlessBundle.ts +16 -12
- package/src/compile/nativeStack.ts +1 -1
- package/src/compile/projectCompile.ts +12 -4
- package/src/compile/sceneCompile.ts +14 -26
- package/src/compile/screenEntry.ts +123 -127
- package/src/compile/shaders.ts +3 -3
- package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
- package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
- package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
- package/src/dev/androidDev.ts +1 -1
- package/src/dev/clientTemplates.ts +18 -14
- package/src/dev/devServer.ts +78 -41
- package/src/dev/iosDev.ts +180 -0
- package/src/dev/webRunner.ts +83 -15
- package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
- package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
- package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
- package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
- package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
- package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
- package/src/{peers.ts → hosts/peers.ts} +2 -2
- package/src/{releases.ts → hosts/releases.ts} +1 -1
- package/src/index.ts +35 -480
- package/src/platform/api.ts +302 -0
- package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
- package/src/{config.ts → platform/config.ts} +1 -1
- package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
- package/src/plugin/contract.ts +438 -0
- package/src/plugin/emitKotlin.ts +357 -0
- package/src/plugin/emitSdk.ts +259 -0
- package/src/plugin/emitSwift.ts +336 -0
- package/src/plugin/emitWeb.ts +383 -0
- package/src/plugin/generate.ts +157 -0
- package/src/plugin/lines.ts +28 -0
- package/src/plugin/lower.ts +231 -0
- package/src/plugin/manifest.ts +192 -0
- package/src/{projectEnv.ts → project/env.ts} +33 -2
- package/src/{ignore.ts → project/ignore.ts} +164 -163
- package/src/{localFiles.ts → project/localFiles.ts} +1 -1
- package/src/{manifest.ts → project/manifest.ts} +1 -1
- package/src/{project.ts → project/materialize.ts} +2 -2
- package/src/project/paths.ts +20 -0
- package/src/{textDiff.ts → project/textDiff.ts} +1 -1
- package/src/{types.ts → project/types.ts} +0 -0
- package/runtime/materials/lightmap-baked-lite.mat +0 -175
- package/runtime/materials/lightmap-baked.mat +0 -176
- package/runtime/scene-harness.json +0 -1
- package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/runtime/web/assets/basis-C64VHDVD.js +0 -1
- package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
- package/runtime/web/assets/draco-BiISTFcR.js +0 -118
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-BMt7AnC5.js +0 -2
- package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
- package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
- package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
- package/runtime/web/embed.html +0 -32
- package/runtime/web/embed.js +0 -171
- package/src/commands/design.ts +0 -846
- package/src/commands/update.ts +0 -211
- package/src/util.ts +0 -146
- /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
- /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
- /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
package/src/api.ts
CHANGED
|
@@ -1,302 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/* Thin client over the backend HTTP API. All project/account calls go through `${apiUrl}/api/...`;
|
|
4
|
-
* blob downloads hit `${apiUrl}<src>` (the public /uploads/* route). Mirrors the contract the
|
|
5
|
-
* frontend uses (see packages/frontend/src/api). */
|
|
6
|
-
|
|
7
|
-
export type ProjectAsset = {
|
|
8
|
-
id: number
|
|
9
|
-
name: string
|
|
10
|
-
type: "folder" | "file" | "text" | "shader" | "scene"
|
|
11
|
-
parentId: number | null
|
|
12
|
-
meta?: Record<string, unknown> | null
|
|
13
|
-
text?: string | null
|
|
14
|
-
file?: { src: string, previewSrc?: string | null, size?: number } | null
|
|
15
|
-
shaders?: { platform: string, src: string }[]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type Project = {
|
|
19
|
-
uuid: string
|
|
20
|
-
name: string
|
|
21
|
-
role: "owner" | "developer" | "viewer"
|
|
22
|
-
assets: ProjectAsset[]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type CommitsResponse = { dirty: boolean, headId: number | null }
|
|
26
|
-
|
|
27
|
-
export type SyncFileInput = {
|
|
28
|
-
path: string
|
|
29
|
-
assetId?: number
|
|
30
|
-
type: "text" | "shader" | "scene" | "resource"
|
|
31
|
-
text?: string | null
|
|
32
|
-
blobFileId?: number | null
|
|
33
|
-
}
|
|
34
|
-
export type SyncFolderInput = { path: string }
|
|
35
|
-
export type SyncResponse = {
|
|
36
|
-
created: boolean
|
|
37
|
-
headId: number
|
|
38
|
-
seq?: number
|
|
39
|
-
nodes: { id: number, path: string, type: string, versionId: number | null }[]
|
|
40
|
-
/** Set when the push requested `publish` — whether the deploy (compile → live) succeeded. */
|
|
41
|
-
published?: boolean
|
|
42
|
-
/** A human-readable reason when `publish` was requested but the deploy failed (push still landed). */
|
|
43
|
-
publishError?: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const errorMessage = (body: unknown, status: number): string => {
|
|
47
|
-
if (typeof body === "string" && body) return body
|
|
48
|
-
if (body && typeof body === "object") {
|
|
49
|
-
const b = body as Record<string, unknown>
|
|
50
|
-
if (typeof b.error === "string") return b.error
|
|
51
|
-
if (b.error && typeof b.error === "object") {
|
|
52
|
-
const first = Object.values(b.error)[0] as unknown
|
|
53
|
-
if (typeof first === "string") return first
|
|
54
|
-
if (first && typeof first === "object" && typeof (first as { message?: unknown }).message === "string") {
|
|
55
|
-
return (first as { message: string }).message
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (typeof b.message === "string") return b.message
|
|
59
|
-
}
|
|
60
|
-
return `Request failed with status ${status}`
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
type RequestOptions = { method?: string, body?: unknown, token?: string }
|
|
64
|
-
|
|
65
|
-
export const apiRequest = async <T = unknown>(apiUrl: string, path: string, opts: RequestOptions = {}): Promise<T> => {
|
|
66
|
-
const headers: Record<string, string> = {}
|
|
67
|
-
const isForm = opts.body instanceof FormData
|
|
68
|
-
if (opts.token) headers["Authorization"] = "Bearer " + opts.token
|
|
69
|
-
if (opts.body !== undefined && !isForm) headers["Content-Type"] = "application/json"
|
|
70
|
-
|
|
71
|
-
let resp: Response
|
|
72
|
-
try {
|
|
73
|
-
resp = await fetch(`${apiUrl}/api${path}`, {
|
|
74
|
-
method: opts.method ?? (opts.body !== undefined ? "POST" : "GET"),
|
|
75
|
-
headers,
|
|
76
|
-
body: opts.body === undefined ? undefined : isForm ? (opts.body as FormData) : JSON.stringify(opts.body),
|
|
77
|
-
})
|
|
78
|
-
} catch (e) {
|
|
79
|
-
throw new CliError(`Cannot reach ${apiUrl} — is the server URL correct and online?`)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const contentType = resp.headers.get("content-type") ?? ""
|
|
83
|
-
const parsed: unknown = contentType.startsWith("application/json") ? await resp.json()
|
|
84
|
-
: contentType.startsWith("text/") ? await resp.text()
|
|
85
|
-
: null
|
|
86
|
-
|
|
87
|
-
if (resp.status >= 400) throw new CliError(errorMessage(parsed, resp.status), resp.status)
|
|
88
|
-
return parsed as T
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export const login = (apiUrl: string, login: string, password: string) =>
|
|
92
|
-
apiRequest<{ accessToken: string, refreshToken: string }>(apiUrl, "/account/login", { body: { login, password } })
|
|
93
|
-
|
|
94
|
-
export const createAccessToken = (apiUrl: string, jwt: string, name: string) =>
|
|
95
|
-
apiRequest<{ id: number, name: string, prefix: string, token: string }>(apiUrl, "/account/tokens", { token: jwt, body: { name } })
|
|
96
|
-
|
|
97
|
-
export const getAccount = (apiUrl: string, token: string) =>
|
|
98
|
-
apiRequest<{ id: number, name: string, email: string }>(apiUrl, "/account", { token })
|
|
99
|
-
|
|
100
|
-
export const getProject = (apiUrl: string, token: string, uuid: string) =>
|
|
101
|
-
apiRequest<Project>(apiUrl, `/projects/${uuid}`, { token })
|
|
102
|
-
|
|
103
|
-
/** Create a new (empty) project on the server. Returns its uuid. */
|
|
104
|
-
export const createProject = (apiUrl: string, token: string, name: string, teamUuid?: string) =>
|
|
105
|
-
apiRequest<{ uuid: string }>(apiUrl, "/projects", { token, body: { name, teamUuid } })
|
|
106
|
-
|
|
107
|
-
export const getCommits = (apiUrl: string, token: string, uuid: string) =>
|
|
108
|
-
apiRequest<CommitsResponse>(apiUrl, `/projects/${uuid}/commits`, { token })
|
|
109
|
-
|
|
110
|
-
export const getTypes = (apiUrl: string) =>
|
|
111
|
-
apiRequest<Record<string, string>>(apiUrl, "/types")
|
|
112
|
-
|
|
113
|
-
export const uploadBlob = (apiUrl: string, token: string, uuid: string, name: string, bytes: Uint8Array) => {
|
|
114
|
-
const form = new FormData()
|
|
115
|
-
// Cast is type-only (TS 5.7 typed-array generics vs the Blob signature); Blob accepts a Uint8Array.
|
|
116
|
-
form.append("file", new Blob([bytes as unknown as ArrayBuffer]), name)
|
|
117
|
-
return apiRequest<{ id: number, src: string, size: number }>(apiUrl, `/projects/${uuid}/blobs`, { token, body: form })
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export const sync = (apiUrl: string, token: string, uuid: string, payload: { message: string, files: SyncFileInput[], folders: SyncFolderInput[], publish?: boolean }) =>
|
|
121
|
-
apiRequest<SyncResponse>(apiUrl, `/projects/${uuid}/sync`, { token, body: payload })
|
|
122
|
-
|
|
123
|
-
/** Deploy the project's current server state (compile → live bundle). Used by `push --publish` when
|
|
124
|
-
* there are no file changes to sync. */
|
|
125
|
-
export const publishProject = (apiUrl: string, token: string, uuid: string) =>
|
|
126
|
-
apiRequest<{ published: boolean, size: number }>(apiUrl, `/projects/${uuid}/publish`, { token, body: {} })
|
|
127
|
-
|
|
128
|
-
export type DesignShare = {
|
|
129
|
-
token: string | null
|
|
130
|
-
url: string | null
|
|
131
|
-
/** Whether share-link visitors may WRITE comments (reading is never gated on it). */
|
|
132
|
-
commentsOpen?: boolean
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Current public share-link state for a project's design board. */
|
|
136
|
-
export const getDesignShare = (apiUrl: string, token: string, uuid: string) =>
|
|
137
|
-
apiRequest<DesignShare>(apiUrl, `/projects/${uuid}/design/share`, { token })
|
|
138
|
-
|
|
139
|
-
/** Enable the design share link (idempotent). */
|
|
140
|
-
export const enableDesignShare = (apiUrl: string, token: string, uuid: string) =>
|
|
141
|
-
apiRequest<DesignShare>(apiUrl, `/projects/${uuid}/design/share`, { token, body: {} })
|
|
142
|
-
|
|
143
|
-
/** Revoke the design share link. */
|
|
144
|
-
export const disableDesignShare = (apiUrl: string, token: string, uuid: string) =>
|
|
145
|
-
apiRequest<DesignShare>(apiUrl, `/projects/${uuid}/design/share`, { token, method: "DELETE" })
|
|
146
|
-
|
|
147
|
-
/** Let / stop share-link visitors WRITING comments. Reading the existing conversation is unaffected,
|
|
148
|
-
* so switching this off never hides feedback that already arrived. */
|
|
149
|
-
export const setDesignCommentsOpen = (apiUrl: string, token: string, uuid: string, open: boolean) =>
|
|
150
|
-
apiRequest<{ commentsOpen: boolean }>(apiUrl, `/projects/${uuid}/design/comments/open`, { token, body: { open } })
|
|
151
|
-
|
|
152
|
-
/* ---- Design-board comments (docs/design-comments-plan.md) ---------------------------------
|
|
153
|
-
* The platform is the source of truth; `design/comments/` (one file per thread) is the mirror the
|
|
154
|
-
* running `lecodes design` sync — or `comments pull` — fills. The shapes below are the same
|
|
155
|
-
* document the design package's core defines —
|
|
156
|
-
* spelled out here, like every other DTO in this file, because `lecodes-design` is an optional
|
|
157
|
-
* peer the CLI must typecheck and run without. */
|
|
158
|
-
|
|
159
|
-
export type CommentAnchor = {
|
|
160
|
-
screen?: string
|
|
161
|
-
state?: string
|
|
162
|
-
element?: string
|
|
163
|
-
point?: [number, number]
|
|
164
|
-
world?: [number, number]
|
|
165
|
-
}
|
|
166
|
-
export type CommentMessage = {
|
|
167
|
-
id: string
|
|
168
|
-
author: string
|
|
169
|
-
/** Provenance: `share` = unverified stranger on the link, `ai` = Claude on a member's behalf. */
|
|
170
|
-
origin: "share" | "editor" | "cli" | "ai"
|
|
171
|
-
at: string
|
|
172
|
-
body: string
|
|
173
|
-
editedAt?: string
|
|
174
|
-
mine?: boolean
|
|
175
|
-
}
|
|
176
|
-
export type CommentThread = {
|
|
177
|
-
id: string
|
|
178
|
-
number: number
|
|
179
|
-
anchor: CommentAnchor
|
|
180
|
-
resolved: boolean
|
|
181
|
-
resolvedAt?: string | null
|
|
182
|
-
createdAt: string
|
|
183
|
-
updatedAt: string
|
|
184
|
-
commitId?: number | null
|
|
185
|
-
messages: CommentMessage[]
|
|
186
|
-
mine?: boolean
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export type DesignCommentsSnapshot = {
|
|
190
|
-
threads: CommentThread[]
|
|
191
|
-
cursor: string | null
|
|
192
|
-
canComment: boolean
|
|
193
|
-
shared?: boolean
|
|
194
|
-
commentsOpen?: boolean
|
|
195
|
-
/** The project's thread-number allocator, so a pull can't hand out a used number. */
|
|
196
|
-
nextNumber?: number
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export const listDesignComments = (apiUrl: string, token: string, uuid: string) =>
|
|
200
|
-
apiRequest<DesignCommentsSnapshot>(apiUrl, `/projects/${uuid}/design/comments`, { token })
|
|
201
|
-
|
|
202
|
-
/** `origin` is provenance, carried up so a reply Claude wrote doesn't reach the reviewer looking
|
|
203
|
-
* like one the developer typed. The account is the identity either way; the server rejects any
|
|
204
|
-
* value but `cli` / `ai` / `editor`. */
|
|
205
|
-
export const createDesignComment = (
|
|
206
|
-
apiUrl: string, token: string, uuid: string,
|
|
207
|
-
input: { anchor: CommentAnchor, body: string, origin?: CommentMessage["origin"] },
|
|
208
|
-
) => apiRequest<CommentThread>(apiUrl, `/projects/${uuid}/design/comments`, { token, body: input })
|
|
209
|
-
|
|
210
|
-
export const replyDesignComment = (
|
|
211
|
-
apiUrl: string, token: string, uuid: string, threadId: string, body: string,
|
|
212
|
-
origin?: CommentMessage["origin"],
|
|
213
|
-
) => apiRequest<CommentThread>(
|
|
214
|
-
apiUrl, `/projects/${uuid}/design/comments/${threadId}/messages`, { token, body: { body, origin } })
|
|
215
|
-
|
|
216
|
-
export const resolveDesignComment = (apiUrl: string, token: string, uuid: string, threadId: string, resolved: boolean) =>
|
|
217
|
-
apiRequest<CommentThread>(apiUrl, `/projects/${uuid}/design/comments/${threadId}`, { token, method: "PATCH", body: { resolved } })
|
|
218
|
-
|
|
219
|
-
export const deleteDesignComment = (apiUrl: string, token: string, uuid: string, threadId: string) =>
|
|
220
|
-
apiRequest<{ ok: boolean }>(apiUrl, `/projects/${uuid}/design/comments/${threadId}`, { token, method: "DELETE" })
|
|
221
|
-
|
|
222
|
-
/** Re-anchor a pin. The anchor goes up whole — `element` is replaced, never merged, or a stale name
|
|
223
|
-
* silently re-anchors the pin to whatever else still answers to it. */
|
|
224
|
-
export const moveDesignComment = (
|
|
225
|
-
apiUrl: string, token: string, uuid: string, threadId: string, anchor: CommentAnchor,
|
|
226
|
-
) => apiRequest<CommentThread>(apiUrl, `/projects/${uuid}/design/comments/${threadId}`, {
|
|
227
|
-
token,
|
|
228
|
-
method: "PATCH",
|
|
229
|
-
body: { screen: anchor.screen, state: anchor.state, element: anchor.element, point: anchor.point },
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
export const editDesignCommentMessage = (
|
|
233
|
-
apiUrl: string, token: string, uuid: string, threadId: string, messageId: string, body: string,
|
|
234
|
-
) => apiRequest<CommentThread>(
|
|
235
|
-
apiUrl, `/projects/${uuid}/design/comments/${threadId}/messages/${messageId}`, { token, method: "PATCH", body: { body } })
|
|
236
|
-
|
|
237
|
-
export const deleteDesignCommentMessage = (
|
|
238
|
-
apiUrl: string, token: string, uuid: string, threadId: string, messageId: string,
|
|
239
|
-
) => apiRequest<{ threadRemoved: boolean }>(
|
|
240
|
-
apiUrl, `/projects/${uuid}/design/comments/${threadId}/messages/${messageId}`, { token, method: "DELETE" })
|
|
241
|
-
|
|
242
|
-
/* ---- Push notifications (`lecodes pn`, docs/push-plan.md) ---------------------------------
|
|
243
|
-
* Every call here authenticates with the stored PAT: /push/send accepts a developer-role user
|
|
244
|
-
* token exactly like a project push key, so test sends need no key at all. Keys are minted for
|
|
245
|
-
* ONE purpose — the developer's own production server — and only an owner can manage them. */
|
|
246
|
-
|
|
247
|
-
export type PushSendPayload = {
|
|
248
|
-
to?: string[]
|
|
249
|
-
users?: string[]
|
|
250
|
-
title: string
|
|
251
|
-
body?: string
|
|
252
|
-
url?: string
|
|
253
|
-
data?: Record<string, unknown>
|
|
254
|
-
badge?: number
|
|
255
|
-
}
|
|
256
|
-
export type PushSendResult = { sent: number, failed: { address: string, reason: string }[] }
|
|
257
|
-
|
|
258
|
-
export type PushDevice = {
|
|
259
|
-
address: string
|
|
260
|
-
platform: "apns" | "fcm" | "webpush"
|
|
261
|
-
environment: "production" | "sandbox"
|
|
262
|
-
hostApp: string
|
|
263
|
-
user: string | null
|
|
264
|
-
createdAt: string
|
|
265
|
-
lastSeenAt: string
|
|
266
|
-
disabledAt: string | null
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export type PushKey = {
|
|
270
|
-
id: number
|
|
271
|
-
name: string
|
|
272
|
-
prefix: string
|
|
273
|
-
createdAt: string
|
|
274
|
-
lastUsedAt: string | null
|
|
275
|
-
}
|
|
276
|
-
/** Only the mint response carries `key` — the plaintext exists in this one response and nowhere else. */
|
|
277
|
-
export type CreatedPushKey = PushKey & { key: string }
|
|
278
|
-
|
|
279
|
-
export const sendPush = (apiUrl: string, token: string, uuid: string, payload: PushSendPayload) =>
|
|
280
|
-
apiRequest<PushSendResult>(apiUrl, `/projects/${uuid}/push/send`, { token, body: payload })
|
|
281
|
-
|
|
282
|
-
export const listPushDevices = (apiUrl: string, token: string, uuid: string, user?: string) =>
|
|
283
|
-
apiRequest<{ devices: PushDevice[], truncated: boolean }>(
|
|
284
|
-
apiUrl, `/projects/${uuid}/push/registrations${user ? `?user=${encodeURIComponent(user)}` : ""}`, { token })
|
|
285
|
-
|
|
286
|
-
export const forgetPushDevice = (apiUrl: string, token: string, uuid: string, address: string) =>
|
|
287
|
-
apiRequest<void>(apiUrl, `/projects/${uuid}/push/registrations/${encodeURIComponent(address)}`, { token, method: "DELETE" })
|
|
288
|
-
|
|
289
|
-
export const listPushKeys = (apiUrl: string, token: string, uuid: string) =>
|
|
290
|
-
apiRequest<PushKey[]>(apiUrl, `/projects/${uuid}/push/keys`, { token })
|
|
291
|
-
|
|
292
|
-
export const createPushKey = (apiUrl: string, token: string, uuid: string, name?: string) =>
|
|
293
|
-
apiRequest<CreatedPushKey>(apiUrl, `/projects/${uuid}/push/keys`, { token, body: { name } })
|
|
294
|
-
|
|
295
|
-
export const revokePushKey = (apiUrl: string, token: string, uuid: string, id: number) =>
|
|
296
|
-
apiRequest<void>(apiUrl, `/projects/${uuid}/push/keys/${id}`, { token, method: "DELETE" })
|
|
297
|
-
|
|
298
|
-
export const downloadBlob = async (apiUrl: string, src: string): Promise<Uint8Array> => {
|
|
299
|
-
const resp = await fetch(`${apiUrl}${src}`)
|
|
300
|
-
if (!resp.ok) throw new CliError(`Failed to download ${src} (status ${resp.status})`)
|
|
301
|
-
return new Uint8Array(await resp.arrayBuffer())
|
|
302
|
-
}
|
|
1
|
+
export * from "./platform/api"
|
package/src/cli/args.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The argv grammar, shared by every command:
|
|
3
|
+
* --key value --key=value --flag -k value -k positionals anywhere
|
|
4
|
+
* A token that starts with "-" followed by a digit (or ".5") is a negative NUMBER (or a vector
|
|
5
|
+
* starting with one: `--look-at -0.15,1,2`), never the next flag. A repeated flag keeps the LAST
|
|
6
|
+
* value (list-valued flags take commas). `--` ends flag parsing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type RawFlags = Record<string, string | boolean>
|
|
10
|
+
export type Parsed = { positional: string[], flags: RawFlags }
|
|
11
|
+
|
|
12
|
+
const isNegativeNumber = (s: string): boolean => /^-(\d|\.\d)/.test(s)
|
|
13
|
+
const isValue = (s: string): boolean => !s.startsWith("-") || isNegativeNumber(s)
|
|
14
|
+
|
|
15
|
+
export const parseArgv = (argv: string[]): Parsed => {
|
|
16
|
+
const out: Parsed = { positional: [], flags: {} }
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const arg = argv[i]
|
|
19
|
+
if (arg === "--") { out.positional.push(...argv.slice(i + 1)); break }
|
|
20
|
+
const long = arg.startsWith("--"), short = !long && arg.startsWith("-") && arg.length > 1 && !isNegativeNumber(arg)
|
|
21
|
+
if (!long && !short) { out.positional.push(arg); continue }
|
|
22
|
+
if (long) {
|
|
23
|
+
const eq = arg.indexOf("=")
|
|
24
|
+
if (eq >= 0) { out.flags[arg.slice(2, eq)] = arg.slice(eq + 1); continue }
|
|
25
|
+
}
|
|
26
|
+
const key = arg.slice(long ? 2 : 1)
|
|
27
|
+
const next = argv[i + 1]
|
|
28
|
+
if (next !== undefined && isValue(next)) { out.flags[key] = next; i++ }
|
|
29
|
+
else out.flags[key] = true
|
|
30
|
+
}
|
|
31
|
+
return out
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Output targets that mean "discard" — so `-o /dev/null` (or Windows NUL) doesn't create a real file.
|
|
35
|
+
const NULL_SINKS = new Set(["/dev/null", "nul", "//./nul"])
|
|
36
|
+
/** True if an `--out` value is a null device, i.e. the output should be thrown away, not written. */
|
|
37
|
+
export const isNullSink = (path: string): boolean => NULL_SINKS.has(path.trim().replace(/\\/g, "/").toLowerCase())
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { CliError } from "./errors"
|
|
2
|
+
import type { RawFlags } from "./args"
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* A command is DATA: its name, one summary line, the flags it takes, its subcommands, and `run`.
|
|
6
|
+
* The runner (run.ts) builds the command tree from it, the help (help.ts) renders it, and the
|
|
7
|
+
* flags arrive in `run` already typed — a misspelled flag name in the code is a compile error, a
|
|
8
|
+
* misspelled one on the command line is a warning naming the real ones.
|
|
9
|
+
*
|
|
10
|
+
* export default defineCommand({
|
|
11
|
+
* name: "push",
|
|
12
|
+
* summary: "Push local changes as one new checkpoint",
|
|
13
|
+
* flags: {
|
|
14
|
+
* message: str("checkpoint message", { alias: "m", value: "<text>" }),
|
|
15
|
+
* force: bool("push even if the server moved on"),
|
|
16
|
+
* },
|
|
17
|
+
* run: async ({ flags }) => { … flags.message: string | undefined, flags.force: boolean … },
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* Flag names are their command-line spelling (`"no-header"`, `"public-url"`); read them as
|
|
21
|
+
* `flags["no-header"]`. Groups of flags several commands share are plain objects spread into
|
|
22
|
+
* `flags:` (see `../commands/shared`).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type FlagSpec =
|
|
26
|
+
| { type: "string", help: string, value?: string, alias?: string, default?: string, optional?: boolean }
|
|
27
|
+
| { type: "number", help: string, value?: string, alias?: string, default?: number }
|
|
28
|
+
| { type: "boolean", help: string, alias?: string }
|
|
29
|
+
|
|
30
|
+
export type Flags = Record<string, FlagSpec>
|
|
31
|
+
|
|
32
|
+
type Opts<D> = { alias?: string, value?: string, default?: D }
|
|
33
|
+
// The helpers are generic in their options so a literal `default` survives into the flag's type:
|
|
34
|
+
// `num("…", { default: 4488 })` reads as `number`, a plain `num("…")` as `number | undefined`.
|
|
35
|
+
/** A string-valued flag (`--out <file>`). `value` is the placeholder shown in help. `optional: true`
|
|
36
|
+
* lets the flag stand alone (`--png` as well as `--png shot.png`): a bare one reads as `""`. */
|
|
37
|
+
export const str = <O extends Opts<string> & { optional?: boolean }>(help: string, opts?: O): { type: "string", help: string } & O => ({ type: "string", help, ...(opts as O) })
|
|
38
|
+
/** A numeric flag — parsed and validated for you (`--port 4499`). */
|
|
39
|
+
export const num = <O extends Opts<number>>(help: string, opts?: O): { type: "number", help: string } & O => ({ type: "number", help, ...(opts as O) })
|
|
40
|
+
/** A switch (`--force`). */
|
|
41
|
+
export const bool = <O extends { alias?: string }>(help: string, opts?: O): { type: "boolean", help: string } & O => ({ type: "boolean", help, ...(opts as O) })
|
|
42
|
+
|
|
43
|
+
type ValueOf<S extends FlagSpec> =
|
|
44
|
+
S extends { type: "boolean" } ? boolean
|
|
45
|
+
: S extends { type: "number" } ? (S extends { default: number } ? number : number | undefined)
|
|
46
|
+
: S extends { default: string } ? string : string | undefined
|
|
47
|
+
|
|
48
|
+
export type FlagValues<F extends Flags> = { [K in keyof F]: ValueOf<F[K]> }
|
|
49
|
+
|
|
50
|
+
export type Context<F extends Flags = Flags> = {
|
|
51
|
+
/** Positional arguments after the command path. */
|
|
52
|
+
args: string[]
|
|
53
|
+
/** The declared flags, typed and validated. */
|
|
54
|
+
flags: FlagValues<F>
|
|
55
|
+
/** Every flag as parsed, declared or not — for helpers shared between commands. Prefer `flags`. */
|
|
56
|
+
raw: RawFlags
|
|
57
|
+
/** The argv after the command path, untouched — what a `passthrough` command forwards. */
|
|
58
|
+
argv: string[]
|
|
59
|
+
/** The command path that was run (`["app", "keystore", "create"]`). */
|
|
60
|
+
path: string[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type Command<F extends Flags = Flags> = {
|
|
64
|
+
name: string
|
|
65
|
+
/** One line for the command list. */
|
|
66
|
+
summary: string
|
|
67
|
+
/** The positional syntax after the name: `"<uuid|url> [dir]"`. Omit for none. */
|
|
68
|
+
usage?: string
|
|
69
|
+
/** Paragraphs for `lecodes <name> --help`; blank lines separate paragraphs. */
|
|
70
|
+
description?: string
|
|
71
|
+
flags?: F
|
|
72
|
+
commands?: Command<any>[]
|
|
73
|
+
examples?: string[]
|
|
74
|
+
/** Other names that run this command (kept out of the help). */
|
|
75
|
+
aliases?: string[]
|
|
76
|
+
/** Left out of every command list (still runs). */
|
|
77
|
+
hidden?: boolean
|
|
78
|
+
/** The command parses its own argv (`ctx.argv`): unknown flags are not checked. */
|
|
79
|
+
passthrough?: boolean
|
|
80
|
+
/** What runs. A command with subcommands and no `run` prints its help when called bare. */
|
|
81
|
+
run?: (ctx: Context<F>) => Promise<void> | void
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const defineCommand = <F extends Flags>(cmd: Command<F>): Command<F> => cmd
|
|
85
|
+
|
|
86
|
+
/** The typed values for a flag set from the raw parse; numbers validated, defaults applied. */
|
|
87
|
+
export const readFlags = <F extends Flags>(spec: F, raw: RawFlags, path: string[]): FlagValues<F> => {
|
|
88
|
+
const out: Record<string, unknown> = {}
|
|
89
|
+
const where = `lecodes ${path.join(" ")}`
|
|
90
|
+
for (const [name, s] of Object.entries(spec)) {
|
|
91
|
+
const present = raw[name] !== undefined ? raw[name] : s.alias !== undefined && raw[s.alias] !== undefined ? raw[s.alias] : undefined
|
|
92
|
+
if (s.type === "boolean") { out[name] = present === true || present === "true"; continue }
|
|
93
|
+
if (present === undefined) { out[name] = s.default; continue }
|
|
94
|
+
if (present === true) {
|
|
95
|
+
if (s.type === "number" && s.default !== undefined) { out[name] = s.default; continue }
|
|
96
|
+
if (s.type === "string" && s.optional) { out[name] = ""; continue }
|
|
97
|
+
throw new CliError(`${where}: --${name} needs a value${s.value ? ` (${s.value})` : ""}`)
|
|
98
|
+
}
|
|
99
|
+
if (s.type === "number") {
|
|
100
|
+
const n = Number(present)
|
|
101
|
+
if (!Number.isFinite(n)) throw new CliError(`${where}: --${name} must be a number (got "${present}")`)
|
|
102
|
+
out[name] = n
|
|
103
|
+
} else out[name] = present
|
|
104
|
+
}
|
|
105
|
+
return out as FlagValues<F>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Flags on the line that no spec names — reported as a warning by the runner. */
|
|
109
|
+
export const unknownFlags = (spec: Flags, raw: RawFlags): string[] => {
|
|
110
|
+
const known = new Set<string>()
|
|
111
|
+
for (const [name, s] of Object.entries(spec)) { known.add(name); if (s.alias) known.add(s.alias) }
|
|
112
|
+
return Object.keys(raw).filter(k => !known.has(k))
|
|
113
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A user-facing error: the runner prints its message without a stack trace and exits non-zero. */
|
|
2
|
+
export class CliError extends Error {
|
|
3
|
+
/** HTTP status, when the failure came from the API. Lets a caller tell "already gone" (404) from
|
|
4
|
+
* "refused" (403) — deleting something the server no longer has is a success, not an error. */
|
|
5
|
+
status?: number
|
|
6
|
+
constructor(message: string, status?: number) {
|
|
7
|
+
super(message)
|
|
8
|
+
this.status = status
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A "how do I call this" mistake: the runner prints the message AND the command's help. */
|
|
13
|
+
export class UsageError extends CliError {}
|
package/src/cli/help.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { Command, FlagSpec } from "./command"
|
|
2
|
+
import { c } from "./output"
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Two levels of help, both rendered from the command objects:
|
|
6
|
+
* lecodes --help the command list, grouped, one line each
|
|
7
|
+
* lecodes <cmd> [sub] --help usage, description, options, subcommands, examples of that node
|
|
8
|
+
* Nothing here is hand-written per command; a command's help is its definition.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type Group = { title: string, commands: Command<any>[] }
|
|
12
|
+
|
|
13
|
+
const WIDTH = Math.min(100, Math.max(60, process.stdout.columns ?? 90))
|
|
14
|
+
|
|
15
|
+
/** Wrap a paragraph to `width`, continuation lines indented by `indent`. */
|
|
16
|
+
const wrap = (text: string, width: number, indent = ""): string[] => {
|
|
17
|
+
const lines: string[] = []
|
|
18
|
+
for (const para of text.split("\n")) {
|
|
19
|
+
if (!para.trim()) { lines.push(""); continue }
|
|
20
|
+
let line = ""
|
|
21
|
+
for (const word of para.split(/\s+/)) {
|
|
22
|
+
if (line && (line + " " + word).length > width) { lines.push(line); line = indent + word }
|
|
23
|
+
else line = line ? line + " " + word : word
|
|
24
|
+
}
|
|
25
|
+
if (line) lines.push(line)
|
|
26
|
+
}
|
|
27
|
+
return lines
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `-o, --out <file>` for a flag. */
|
|
31
|
+
const flagLabel = (name: string, s: FlagSpec): string => {
|
|
32
|
+
const alias = s.alias ? `-${s.alias}, ` : ""
|
|
33
|
+
const value = s.type === "boolean" ? "" : ` ${s.value ?? (s.type === "number" ? "<n>" : "<value>")}`
|
|
34
|
+
return `${alias}--${name}${value}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Two-column rows: label, then help wrapped into the remaining width. */
|
|
38
|
+
const columns = (rows: [string, string][], indent = " "): string[] => {
|
|
39
|
+
if (rows.length === 0) return []
|
|
40
|
+
const labelWidth = Math.min(30, Math.max(...rows.map(([l]) => l.length)))
|
|
41
|
+
const out: string[] = []
|
|
42
|
+
for (const [label, help] of rows) {
|
|
43
|
+
const textWidth = WIDTH - indent.length - labelWidth - 2
|
|
44
|
+
const text = wrap(help, textWidth)
|
|
45
|
+
if (label.length > labelWidth) {
|
|
46
|
+
out.push(indent + label)
|
|
47
|
+
for (const t of text) out.push(indent + " ".repeat(labelWidth + 2) + t)
|
|
48
|
+
} else {
|
|
49
|
+
out.push(indent + label.padEnd(labelWidth) + " " + (text[0] ?? ""))
|
|
50
|
+
for (const t of text.slice(1)) out.push(indent + " ".repeat(labelWidth + 2) + t)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const visible = (cmds: Command<any>[] | undefined): Command<any>[] => (cmds ?? []).filter(x => !x.hidden)
|
|
57
|
+
|
|
58
|
+
/** The usage line: `lecodes app keystore create [--alias <a>] …` shortened to `[options]`. */
|
|
59
|
+
const usageLine = (path: string[], cmd: Command<any>): string => {
|
|
60
|
+
const parts = ["lecodes", ...path]
|
|
61
|
+
if (cmd.usage) parts.push(cmd.usage)
|
|
62
|
+
if (visible(cmd.commands).length) parts.push(cmd.run ? "[command]" : "<command>")
|
|
63
|
+
if (cmd.flags && Object.keys(cmd.flags).length) parts.push("[options]")
|
|
64
|
+
return parts.join(" ")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `lecodes <path> --help`. */
|
|
68
|
+
export const commandHelp = (path: string[], cmd: Command<any>): string => {
|
|
69
|
+
const lines: string[] = []
|
|
70
|
+
lines.push(`${c.bold(usageLine(path, cmd))}`)
|
|
71
|
+
lines.push(...wrap(cmd.summary, WIDTH - 2).map(l => " " + l))
|
|
72
|
+
if (cmd.description) { lines.push(""); lines.push(...wrap(cmd.description, WIDTH - 2).map(l => l ? " " + l : "")) }
|
|
73
|
+
const subs = visible(cmd.commands)
|
|
74
|
+
if (subs.length) {
|
|
75
|
+
lines.push("", c.bold("Commands"))
|
|
76
|
+
lines.push(...columns(subs.map(s => [s.usage ? `${s.name} ${s.usage}` : s.name, s.summary])))
|
|
77
|
+
lines.push("", c.dim(` lecodes ${path.join(" ")} <command> --help for a command's options`))
|
|
78
|
+
}
|
|
79
|
+
const flags = Object.entries(cmd.flags ?? {}) as [string, FlagSpec][]
|
|
80
|
+
if (flags.length) {
|
|
81
|
+
lines.push("", c.bold("Options"))
|
|
82
|
+
lines.push(...columns(flags.map(([n, s]) => {
|
|
83
|
+
const def = "default" in s && s.default !== undefined ? c.dim(` (default ${s.default})`) : ""
|
|
84
|
+
return [flagLabel(n, s), s.help + def]
|
|
85
|
+
})))
|
|
86
|
+
}
|
|
87
|
+
if (cmd.examples?.length) {
|
|
88
|
+
lines.push("", c.bold("Examples"))
|
|
89
|
+
for (const e of cmd.examples) lines.push(" " + e)
|
|
90
|
+
}
|
|
91
|
+
return lines.join("\n")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `lecodes --help`: the grouped command list. */
|
|
95
|
+
export const globalHelp = (version: string, groups: Group[], footer = ""): string => {
|
|
96
|
+
const lines: string[] = []
|
|
97
|
+
lines.push(`${c.bold("lecodes")} ${version} — build, run and ship LeCodes projects from your machine.`)
|
|
98
|
+
lines.push("", `${c.bold("Usage:")} lecodes <command> [options]`)
|
|
99
|
+
for (const g of groups) {
|
|
100
|
+
const cmds = visible(g.commands)
|
|
101
|
+
if (!cmds.length) continue
|
|
102
|
+
lines.push("", c.bold(g.title))
|
|
103
|
+
lines.push(...columns(cmds.map(x => [x.usage ? `${x.name} ${x.usage}` : x.name, x.summary])))
|
|
104
|
+
}
|
|
105
|
+
lines.push("", `Run ${c.bold("lecodes <command> --help")} for a command's options and examples.`)
|
|
106
|
+
if (footer) lines.push("", ...wrap(footer, WIDTH))
|
|
107
|
+
return lines.join("\n")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** One command in `docs/commands.json`: `path` is what follows `lecodes` ("desktop run"). */
|
|
111
|
+
export type CommandIndexEntry = { path: string, usage: string, summary: string, commands: CommandIndexEntry[] }
|
|
112
|
+
export type CommandIndex = { groups: { title: string, slug: string, commands: CommandIndexEntry[] }[] }
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The tree as DATA — `docs/commands.json`, next to the Markdown reference and generated by the same
|
|
116
|
+
* script. Its reader is the le.codes site: /llms.txt lists every command with its summary line from
|
|
117
|
+
* here, and links each group to the matching `## ` section of the reference by `slug`. A summary
|
|
118
|
+
* written for `lecodes --help` is therefore also what a coding agent decides on — keep it a full
|
|
119
|
+
* answer to "what is this for", not a label.
|
|
120
|
+
*/
|
|
121
|
+
export const commandIndex = (groups: Group[]): CommandIndex => {
|
|
122
|
+
const entry = (path: string[], cmd: Command<any>): CommandIndexEntry => ({
|
|
123
|
+
path: path.join(" "), usage: cmd.usage ?? "", summary: cmd.summary,
|
|
124
|
+
commands: visible(cmd.commands).map(s => entry([...path, s.name], s)),
|
|
125
|
+
})
|
|
126
|
+
return {
|
|
127
|
+
groups: groups.map(g => ({
|
|
128
|
+
title: g.title,
|
|
129
|
+
slug: g.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""),
|
|
130
|
+
commands: visible(g.commands).map(cmd => entry([cmd.name], cmd)),
|
|
131
|
+
})),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The whole tree as Markdown — `docs/commands.md` is generated from this (scripts/gen-docs.ts). */
|
|
136
|
+
export const markdownReference = (groups: Group[]): string => {
|
|
137
|
+
const out: string[] = ["# lecodes — command reference", "", "Generated from the command definitions by `bun run docs`; do not edit by hand.", ""]
|
|
138
|
+
const section = (path: string[], cmd: Command<any>, depth: number): void => {
|
|
139
|
+
out.push(`${"#".repeat(Math.min(6, depth))} \`${["lecodes", ...path].join(" ")}${cmd.usage ? " " + cmd.usage : ""}\``, "")
|
|
140
|
+
out.push(cmd.summary + (cmd.description ? "\n\n" + cmd.description : ""), "")
|
|
141
|
+
const flags = Object.entries(cmd.flags ?? {}) as [string, FlagSpec][]
|
|
142
|
+
if (flags.length) {
|
|
143
|
+
out.push("| Option | Meaning |", "|---|---|")
|
|
144
|
+
for (const [n, s] of flags) {
|
|
145
|
+
const def = "default" in s && s.default !== undefined ? ` (default \`${s.default}\`)` : ""
|
|
146
|
+
out.push(`| \`${flagLabel(n, s)}\` | ${s.help.replace(/\|/g, "\\|")}${def} |`)
|
|
147
|
+
}
|
|
148
|
+
out.push("")
|
|
149
|
+
}
|
|
150
|
+
if (cmd.examples?.length) { out.push("```", ...cmd.examples, "```", "") }
|
|
151
|
+
for (const s of visible(cmd.commands)) section([...path, s.name], s, depth + 1)
|
|
152
|
+
}
|
|
153
|
+
for (const g of groups) {
|
|
154
|
+
out.push(`## ${g.title}`, "")
|
|
155
|
+
for (const cmd of visible(g.commands)) section([cmd.name], cmd, 3)
|
|
156
|
+
}
|
|
157
|
+
return out.join("\n")
|
|
158
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/* The command framework — what every command file imports. See CONTRIBUTING.md. */
|
|
2
|
+
export { isNullSink, parseArgv, type RawFlags } from "./args"
|
|
3
|
+
export { bool, defineCommand, num, readFlags, str, type Command, type Context, type Flags, type FlagValues } from "./command"
|
|
4
|
+
export { CliError, UsageError } from "./errors"
|
|
5
|
+
export { commandHelp, commandIndex, globalHelp, markdownReference, type CommandIndex, type Group } from "./help"
|
|
6
|
+
export { c, confirm, endProgress, info, log, logErr, note, printedWidth, progress, prompt, promptHidden, relativeTime, success, table, warn, warnErr } from "./output"
|
|
7
|
+
export { reportError, runCli, type Cli } from "./run"
|