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.
Files changed (154) hide show
  1. package/README.md +56 -57
  2. package/dist/index.js +11052 -7592
  3. package/package.json +14 -8
  4. package/runtime/materials/decal-relief.mat +12 -1
  5. package/runtime/materials/decal.mat +17 -3
  6. package/runtime/materials/lightmap.mat +117 -138
  7. package/runtime/materials/particles-quad.mat +6 -2
  8. package/runtime/materials/terrain-lightmap.mat +123 -0
  9. package/runtime/materials/terrain.mat +36 -55
  10. package/runtime/sdk-types.json +1 -1
  11. package/runtime/web/VERSION.json +5 -0
  12. package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
  13. package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
  14. package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
  15. package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
  16. package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
  17. package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
  18. package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
  19. package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
  20. package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
  21. package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
  22. package/runtime/web/assets/map-CyBxOSV0.js +1 -0
  23. package/runtime/web/assets/map-XzoOFt0E.js +2 -0
  24. package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
  25. package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
  26. package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
  27. package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
  28. package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
  29. package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
  30. package/runtime/web/index.html +36 -0
  31. package/runtime/web/player.js +21 -0
  32. package/runtime/web-page/index.html +41 -0
  33. package/runtime/web-page/page.css +1 -0
  34. package/runtime/web-page/page.js +31 -0
  35. package/src/api.ts +1 -302
  36. package/src/cli/args.ts +37 -0
  37. package/src/cli/command.ts +113 -0
  38. package/src/cli/errors.ts +13 -0
  39. package/src/cli/help.ts +158 -0
  40. package/src/cli/index.ts +7 -0
  41. package/src/cli/output.ts +102 -0
  42. package/src/cli/run.ts +106 -0
  43. package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
  44. package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
  45. package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
  46. package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
  47. package/src/commands/{app.ts → app/index.ts} +400 -290
  48. package/src/commands/{appShared.ts → app/shared.ts} +43 -44
  49. package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
  50. package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
  51. package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
  52. package/src/commands/assets.ts +23 -21
  53. package/src/commands/clone.ts +48 -51
  54. package/src/commands/compile.ts +76 -89
  55. package/src/commands/create.ts +46 -47
  56. package/src/commands/design/comments.ts +361 -0
  57. package/src/commands/design/context.ts +101 -0
  58. package/src/commands/design/index.ts +391 -0
  59. package/src/commands/design/snapshot.ts +134 -0
  60. package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
  61. package/src/commands/desktop.ts +94 -82
  62. package/src/commands/dev.ts +260 -182
  63. package/src/commands/diff.ts +39 -51
  64. package/src/commands/index.ts +64 -0
  65. package/src/commands/{init.ts → init/index.ts} +195 -191
  66. package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
  67. package/src/commands/install.ts +80 -67
  68. package/src/commands/lightmap.ts +543 -252
  69. package/src/commands/link.ts +87 -94
  70. package/src/commands/login.ts +25 -23
  71. package/src/commands/navmesh.ts +169 -139
  72. package/src/commands/plugin.ts +46 -0
  73. package/src/commands/pn.ts +201 -244
  74. package/src/commands/pull.ts +49 -56
  75. package/src/commands/push.ts +48 -54
  76. package/src/commands/render.ts +176 -111
  77. package/src/commands/scene.ts +63 -55
  78. package/src/commands/serve.ts +58 -0
  79. package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
  80. package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
  81. package/src/commands/shared.ts +79 -0
  82. package/src/commands/status.ts +22 -26
  83. package/src/commands/test.ts +94 -63
  84. package/src/commands/thumbs.ts +185 -175
  85. package/src/commands/update/index.ts +226 -0
  86. package/src/commands/{types.ts → update/types.ts} +31 -22
  87. package/src/compile/collect.ts +5 -5
  88. package/src/compile/collectLocal.ts +3 -3
  89. package/src/compile/designCompile.ts +4 -4
  90. package/src/compile/headlessBundle.ts +16 -12
  91. package/src/compile/nativeStack.ts +1 -1
  92. package/src/compile/projectCompile.ts +12 -4
  93. package/src/compile/sceneCompile.ts +14 -26
  94. package/src/compile/screenEntry.ts +123 -127
  95. package/src/compile/shaders.ts +3 -3
  96. package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
  97. package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
  98. package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
  99. package/src/dev/androidDev.ts +1 -1
  100. package/src/dev/clientTemplates.ts +18 -14
  101. package/src/dev/devServer.ts +78 -41
  102. package/src/dev/iosDev.ts +180 -0
  103. package/src/dev/webRunner.ts +83 -15
  104. package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
  105. package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
  106. package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
  107. package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
  108. package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
  109. package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
  110. package/src/{peers.ts → hosts/peers.ts} +2 -2
  111. package/src/{releases.ts → hosts/releases.ts} +1 -1
  112. package/src/index.ts +35 -480
  113. package/src/platform/api.ts +302 -0
  114. package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
  115. package/src/{config.ts → platform/config.ts} +1 -1
  116. package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
  117. package/src/plugin/contract.ts +438 -0
  118. package/src/plugin/emitKotlin.ts +357 -0
  119. package/src/plugin/emitSdk.ts +259 -0
  120. package/src/plugin/emitSwift.ts +336 -0
  121. package/src/plugin/emitWeb.ts +383 -0
  122. package/src/plugin/generate.ts +157 -0
  123. package/src/plugin/lines.ts +28 -0
  124. package/src/plugin/lower.ts +231 -0
  125. package/src/plugin/manifest.ts +192 -0
  126. package/src/{projectEnv.ts → project/env.ts} +33 -2
  127. package/src/{ignore.ts → project/ignore.ts} +164 -163
  128. package/src/{localFiles.ts → project/localFiles.ts} +1 -1
  129. package/src/{manifest.ts → project/manifest.ts} +1 -1
  130. package/src/{project.ts → project/materialize.ts} +2 -2
  131. package/src/project/paths.ts +20 -0
  132. package/src/{textDiff.ts → project/textDiff.ts} +1 -1
  133. package/src/{types.ts → project/types.ts} +0 -0
  134. package/runtime/materials/lightmap-baked-lite.mat +0 -175
  135. package/runtime/materials/lightmap-baked.mat +0 -176
  136. package/runtime/scene-harness.json +0 -1
  137. package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
  138. package/runtime/web/assets/basis-C64VHDVD.js +0 -1
  139. package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  140. package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
  141. package/runtime/web/assets/draco-BiISTFcR.js +0 -118
  142. package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  143. package/runtime/web/assets/index-BMt7AnC5.js +0 -2
  144. package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
  145. package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
  146. package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
  147. package/runtime/web/embed.html +0 -32
  148. package/runtime/web/embed.js +0 -171
  149. package/src/commands/design.ts +0 -846
  150. package/src/commands/update.ts +0 -211
  151. package/src/util.ts +0 -146
  152. /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
  153. /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
  154. /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
@@ -1,16 +1,17 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import { basename, join, resolve } from "node:path"
3
- import { createProject, getCommits, getProject, type Project, type ProjectAsset } from "../api"
4
- import { loadConfig, requireApiUrl, requireToken } from "../config"
5
- import { ensureGitignore, writeDefaultIgnore } from "../ignore"
6
- import { scanProject } from "../localFiles"
7
- import { findProjectRoot, LECODES_DIR, writeManifest, type FileKind, type ManifestFile, type Manifest } from "../manifest"
8
- import { buildPaths } from "../project"
9
- import { computeServerDiff } from "../serverDiff"
10
- import { materializeTypes, writeTsconfig } from "../types"
11
- import { CliError, c, confirm, flagBool, flagStr, info, log, note, sha256, success, warn, type Args } from "../util"
3
+ import { CliError, bool, c, confirm, defineCommand, info, log, note, str, success, warn } from "../cli"
4
+ import { createProject, getCommits, getProject, type Project, type ProjectAsset } from "../platform/api"
5
+ import { computeServerDiff } from "../platform/serverDiff"
6
+ import { ensureGitignore, writeDefaultIgnore } from "../project/ignore"
7
+ import { scanProject } from "../project/localFiles"
8
+ import { findProjectRoot, LECODES_DIR, writeManifest, type FileKind, type Manifest, type ManifestFile } from "../project/manifest"
9
+ import { buildPaths } from "../project/materialize"
10
+ import { sha256 } from "../project/paths"
11
+ import { materializeTypes, writeTsconfig } from "../project/types"
12
12
  import { parseUuid } from "./clone"
13
- import { push } from "./push"
13
+ import { pushFlags, runPush } from "./push"
14
+ import { session } from "./shared"
14
15
 
15
16
  /*
16
17
  * `lecodes link [dir]` — connect an existing local folder (typically one made by `lecodes init`) to
@@ -38,110 +39,102 @@ const manifestFromServer = (project: Project): Record<string, ManifestFile> => {
38
39
  const files: Record<string, ManifestFile> = {}
39
40
  for (const asset of project.assets) {
40
41
  if (asset.type === "folder") continue
41
- files[pathById.get(asset.id)!] = {
42
- id: asset.id,
43
- type: kindOf(asset.type),
44
- sha: asset.type === "file" ? "" : sha256(asset.text ?? ""),
45
- }
42
+ files[pathById.get(asset.id)!] = { id: asset.id, type: kindOf(asset.type), sha: asset.type === "file" ? "" : sha256(asset.text ?? "") }
46
43
  }
47
44
  return files
48
45
  }
49
46
 
50
47
  /** Show what a push from this folder would do to the project's server tree, and get a yes. */
51
- const confirmAdoption = async (root: string, project: Project, files: Record<string, ManifestFile>, args: Args): Promise<boolean> => {
48
+ const confirmAdoption = async (root: string, project: Project, files: Record<string, ManifestFile>, skipAsk: boolean): Promise<boolean> => {
52
49
  const count = Object.keys(files).length
53
- if (count === 0) {
54
- info(c.dim(`Project ${c.bold(project.name)} is empty on the server — nothing to overwrite.`))
55
- return true
56
- }
50
+ if (count === 0) { info(c.dim(`Project ${c.bold(project.name)} is empty on the server — nothing to overwrite.`)); return true }
57
51
 
58
52
  const scan = scanProject(root, { files } as Manifest)
59
53
  const diff = computeServerDiff(project, scan)
60
- if (diff.length === 0) {
61
- info(c.dim(`Project ${c.bold(project.name)} (${count} file(s)) already matches this folder.`))
62
- return true
63
- }
54
+ if (diff.length === 0) { info(c.dim(`Project ${c.bold(project.name)} (${count} file(s)) already matches this folder.`)); return true }
64
55
 
65
56
  warn(`${c.bold(project.name)} already has ${count} file(s). Your next push would change the server:`)
66
57
  const label: Record<string, string> = { added: c.green(" create "), removed: c.red(" delete "), modified: c.yellow(" update ") }
67
58
  for (const e of diff) log(label[e.status] + e.path + (e.binary ? c.dim(" (binary)") : ""))
68
59
  log("")
69
60
  note("`lecodes pull` after linking takes the server's version instead.")
70
-
71
- if (flagBool(args, "y", "yes") || flagBool(args, "force") || !process.stdin.isTTY) return true
61
+ if (skipAsk || !process.stdin.isTTY) return true
72
62
  return confirm("Link this folder to that project?", false)
73
63
  }
74
64
 
75
- export const link = async (args: Args) => {
76
- const config = loadConfig()
77
- const apiUrl = requireApiUrl(config)
78
- const token = requireToken(config)
79
-
80
- const dirArg = args._[0]
81
- const root = dirArg ? resolve(dirArg) : process.cwd()
82
- if (!existsSync(root)) throw new CliError(`No such directory: ${root}`)
83
-
84
- // Already linked? Re-linking silently would orphan the old project's history.
85
- if (existsSync(join(root, LECODES_DIR, "manifest.json")) && !flagBool(args, "force")) {
86
- throw new CliError("This folder is already linked (.lecodes/manifest.json exists) — use `lecodes push`, or `lecodes link --force` to re-link it to another project.")
87
- }
88
- // …or sitting inside a linked project, where a nested manifest would shadow the real root.
89
- let enclosing: string | null = null
90
- try { enclosing = findProjectRoot(root) } catch { /* not inside a project — the normal case */ }
91
- if (enclosing && enclosing !== root) {
92
- throw new CliError(`This folder is inside the linked project at ${enclosing} — push from there instead.`)
93
- }
94
-
95
- const projectRef = flagStr(args, "project")
96
- let uuid: string
97
- let name: string
98
- let files: Record<string, ManifestFile>
99
-
100
- if (projectRef) {
101
- uuid = parseUuid(projectRef)
102
- const project = await getProject(apiUrl, token, uuid)
103
- if (project.role === "viewer") {
104
- throw new CliError(`You have view-only access to ${project.name} — linking would give you a checkout you can't push.`)
65
+ export default defineCommand({
66
+ name: "link",
67
+ summary: "Connect an existing local folder to the platform (the reverse of clone)",
68
+ usage: "[dir]",
69
+ description: "Creates a project on the server, writes .lecodes/manifest.json and refreshes the types; nothing is uploaded until you push, so `status` shows what would go up first. `--project` adopts an EXISTING project instead: the manifest is built from its server tree and the command prints exactly what your next push would create / update / delete there before linking.",
70
+ flags: {
71
+ name: str("project name (default: the folder name)", { value: "<s>" }),
72
+ team: str("create it inside a team", { value: "<uuid>" }),
73
+ project: str("adopt an existing project instead of creating one", { value: "<uuid|url>" }),
74
+ push: bool("push right after linking (--publish to deploy it live in the same step)"),
75
+ "no-types": bool("skip the .d.ts / tsconfig refresh"),
76
+ ...pushFlags,
77
+ },
78
+ examples: ["lecodes link", "lecodes link --project <uuid> && lecodes status", 'lecodes link --push -m "initial"'],
79
+ run: async ({ args, flags }) => {
80
+ const { apiUrl, token } = session()
81
+ const dirArg = args[0]
82
+ const root = dirArg ? resolve(dirArg) : process.cwd()
83
+ if (!existsSync(root)) throw new CliError(`No such directory: ${root}`)
84
+
85
+ // Already linked? Re-linking silently would orphan the old project's history.
86
+ if (existsSync(join(root, LECODES_DIR, "manifest.json")) && !flags.force) {
87
+ throw new CliError("This folder is already linked (.lecodes/manifest.json exists) — use `lecodes push`, or `lecodes link --force` to re-link it to another project.")
88
+ }
89
+ // …or sitting inside a linked project, where a nested manifest would shadow the real root.
90
+ let enclosing: string | null = null
91
+ try { enclosing = findProjectRoot(root) } catch { /* not inside a project — the normal case */ }
92
+ if (enclosing && enclosing !== root) throw new CliError(`This folder is inside the linked project at ${enclosing} — push from there instead.`)
93
+
94
+ let uuid: string
95
+ let name: string
96
+ let files: Record<string, ManifestFile>
97
+ if (flags.project) {
98
+ uuid = parseUuid(flags.project)
99
+ const project = await getProject(apiUrl, token, uuid)
100
+ if (project.role === "viewer") throw new CliError(`You have view-only access to ${project.name} — linking would give you a checkout you can't push.`)
101
+ files = manifestFromServer(project)
102
+ if (!(await confirmAdoption(root, project, files, flags.yes || flags.force))) { info(c.dim("Aborted — nothing was linked.")); return }
103
+ name = project.name
104
+ } else {
105
+ name = flags.name ?? basename(root)
106
+ info(c.dim(`Creating project ${c.bold(name)}…`))
107
+ uuid = (await createProject(apiUrl, token, name, flags.team)).uuid
108
+ files = {} // fresh project: the whole folder is a pending add
105
109
  }
106
- files = manifestFromServer(project)
107
- if (!(await confirmAdoption(root, project, files, args))) { info(c.dim("Aborted — nothing was linked.")); return }
108
- name = project.name
109
- } else {
110
- name = flagStr(args, "name") ?? basename(root)
111
- info(c.dim(`Creating project ${c.bold(name)}…`))
112
- uuid = (await createProject(apiUrl, token, name, flagStr(args, "team"))).uuid
113
- files = {} // fresh project: the whole folder is a pending add
114
- }
115
110
 
116
- const commits = await getCommits(apiUrl, token, uuid)
117
- writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files })
118
- log(`${c.green("linked")} ${c.bold(name)} ${c.dim(uuid)}`)
111
+ const commits = await getCommits(apiUrl, token, uuid)
112
+ writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files })
113
+ log(`${c.green("linked")} ${c.bold(name)} ${c.dim(uuid)}`)
119
114
 
120
- // Now that there IS a server, take its type surface — it matches the runtime the project will
121
- // actually run on, unlike the CLI-bundled copy `lecodes init` wrote.
122
- if (!flagBool(args, "no-types")) {
123
- await materializeTypes(root, apiUrl)
124
- writeTsconfig(root)
125
- log(`${c.green("written")} ${LECODES_DIR}/types/ + tsconfig.json ${c.dim("(from the server)")}`)
126
- }
127
- writeDefaultIgnore(root)
128
- if (ensureGitignore(root)) log(`${c.green("written")} .gitignore ${c.dim("(.lecodes/ is per-checkout state)")}`)
115
+ // Now that there IS a server, take its type surface — it matches the runtime the project will
116
+ // actually run on, unlike the CLI-bundled copy `lecodes init` wrote.
117
+ if (!flags["no-types"]) {
118
+ await materializeTypes(root, apiUrl)
119
+ writeTsconfig(root)
120
+ log(`${c.green("written")} ${LECODES_DIR}/types/ + tsconfig.json ${c.dim("(from the server)")}`)
121
+ }
122
+ writeDefaultIgnore(root)
123
+ if (ensureGitignore(root)) log(`${c.green("written")} .gitignore ${c.dim("(.lecodes/ is per-checkout state)")}`)
124
+
125
+ if (flags.push) {
126
+ log("")
127
+ if (root !== process.cwd()) process.chdir(root) // push works from the current project root
128
+ await runPush(flags)
129
+ return
130
+ }
129
131
 
130
- if (flagBool(args, "push")) {
132
+ const pending = scanProject(root, { files } as Manifest).files.filter((f) => { const known = files[f.path]; return !known || known.sha !== f.sha }).length
131
133
  log("")
132
- if (root !== process.cwd()) process.chdir(root) // push works from the current project root
133
- await push(args)
134
- return
135
- }
136
-
137
- const pending = scanProject(root, { files } as Manifest).files.filter((f) => {
138
- const known = files[f.path]
139
- return !known || known.sha !== f.sha
140
- }).length
141
- log("")
142
- success(`Connected to ${c.bold(name)}.`)
143
- const cd = dirArg ? `cd ${dirArg} && ` : ""
144
- log(` ${c.bold(cd + "lecodes status")} ${pending} file(s) would go up`)
145
- log(` ${c.bold(cd + 'lecodes push -m "initial"')} push them as the first checkpoint`)
146
- log(` ${c.dim("(add --publish to deploy it live in the same step)")}`)
147
- }
134
+ success(`Connected to ${c.bold(name)}.`)
135
+ const cd = dirArg ? `cd ${dirArg} && ` : ""
136
+ log(` ${c.bold(cd + "lecodes status")} ${pending} file(s) would go up`)
137
+ log(` ${c.bold(cd + 'lecodes push -m "initial"')} push them as the first checkpoint`)
138
+ log(` ${c.dim("(add --publish to deploy it live in the same step)")}`)
139
+ },
140
+ })
@@ -1,24 +1,26 @@
1
- import { getAccount } from "../api"
2
- import { DEFAULT_API, loadConfig, normalizeApiUrl, saveConfig } from "../config"
3
- import { browserLogin } from "../browserAuth"
4
- import { flagStr, success, c, type Args } from "../util"
1
+ import { c, defineCommand, str, success } from "../cli"
2
+ import { getAccount } from "../platform/api"
3
+ import { browserLogin } from "../platform/browserAuth"
4
+ import { DEFAULT_API, loadConfig, normalizeApiUrl, saveConfig } from "../platform/config"
5
5
 
6
- /*
7
- * `lecodes login` — authorize this machine and store a personal access token in ~/.lecodes.
8
- * - default: open the browser, sign in / authorize on le.codes, capture the token.
9
- * - `--token <pat>`: store a token created in the web UI (headless / CI).
10
- * - `--api <url>`: API origin (defaults to https://le.codes).
11
- * - `--web <url>`: app origin for the browser flow (defaults to the API origin).
12
- */
13
- export const login = async (args: Args) => {
14
- const config = loadConfig()
15
- const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? config.apiUrl ?? DEFAULT_API)
16
- const webBase = normalizeApiUrl(flagStr(args, "web") ?? apiUrl)
17
-
18
- const token = flagStr(args, "token") ?? await browserLogin(webBase)
19
-
20
- // Verify the token works against the API before persisting it.
21
- const account = await getAccount(apiUrl, token)
22
- saveConfig({ apiUrl, token })
23
- success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`)
24
- }
6
+ /* `lecodes login` — authorize this machine; the token lands in ~/.lecodes/config.json. */
7
+ export default defineCommand({
8
+ name: "login",
9
+ summary: "Authorize in the browser and store an access token",
10
+ description: "Opens the browser to sign in / authorize on le.codes and captures the token. `--token` stores a token created in the web UI instead (headless / CI). LECODES_API / LECODES_TOKEN in the environment override the stored values.",
11
+ flags: {
12
+ token: str("store a personal access token from the web UI instead of the browser flow", { value: "<pat>" }),
13
+ api: str(`API origin (default: the stored one, else ${DEFAULT_API})`, { value: "<url>" }),
14
+ web: str("app origin for the browser flow (default: the API origin)", { value: "<url>" }),
15
+ },
16
+ run: async ({ flags }) => {
17
+ const config = loadConfig()
18
+ const apiUrl = normalizeApiUrl(flags.api ?? config.apiUrl ?? DEFAULT_API)
19
+ const webBase = normalizeApiUrl(flags.web ?? apiUrl)
20
+ const token = flags.token ?? await browserLogin(webBase)
21
+ // Verify the token works against the API before persisting it.
22
+ const account = await getAccount(apiUrl, token)
23
+ saveConfig({ apiUrl, token })
24
+ success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`)
25
+ },
26
+ })