@solidrt/cli 0.0.51 → 0.0.52

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 (93) hide show
  1. package/AGENTS.md +68 -38
  2. package/README.md +86 -15
  3. package/agents/assets.md +19 -5
  4. package/agents/debugging.md +169 -17
  5. package/dist/console.srtapp +94123 -73
  6. package/dist/server.js +3936 -0
  7. package/package.json +10 -11
  8. package/src/android/docs.md +21 -0
  9. package/src/android/main.ts +286 -0
  10. package/src/{bundler.ts → bundle/bundler.ts} +67 -55
  11. package/src/bundle/docs.md +12 -0
  12. package/src/{commands/bundle.ts → bundle/main.ts} +67 -25
  13. package/src/check/docs.md +10 -0
  14. package/src/check/main.ts +85 -0
  15. package/src/{commands/check.ts → check/typecheck.ts} +11 -41
  16. package/src/client/docs.md +9 -0
  17. package/src/client/main.ts +33 -0
  18. package/src/console/docs.md +14 -0
  19. package/src/console/main.ts +21 -0
  20. package/src/demo/docs.md +27 -0
  21. package/src/demo/main.ts +67 -0
  22. package/src/init/docs.md +11 -0
  23. package/src/{commands/init.ts → init/main.ts} +21 -15
  24. package/src/init/scaffold/AGENTS.md +98 -0
  25. package/src/init/scaffold/package.json +23 -0
  26. package/{scaffold → src/init/scaffold}/templates/components/index.tsx +2 -3
  27. package/{scaffold → src/init/scaffold}/templates/default/index.tsx +2 -3
  28. package/src/lib/args.ts +194 -0
  29. package/src/{artifacts.ts → lib/artifacts.ts} +41 -1
  30. package/src/{dev-dir.ts → lib/dev-dir.ts} +10 -8
  31. package/src/{fonts.ts → lib/fonts.ts} +12 -26
  32. package/src/lib/mode.ts +77 -0
  33. package/src/{project.ts → lib/project.ts} +109 -61
  34. package/src/lib/registry.ts +120 -0
  35. package/src/lib/server-bundle.ts +24 -0
  36. package/src/lib/usage.ts +117 -0
  37. package/src/lib/util.ts +36 -0
  38. package/src/main.ts +109 -31
  39. package/src/mcp/docs.md +22 -0
  40. package/src/mcp/main.ts +719 -0
  41. package/src/pack/docs.md +18 -0
  42. package/src/{pack-folder.ts → pack/layout.ts} +13 -22
  43. package/src/{commands/pack.ts → pack/main.ts} +28 -17
  44. package/src/{packer.ts → pack/trailer.ts} +32 -42
  45. package/src/render/docs.md +19 -0
  46. package/src/{commands/render.ts → render/main.ts} +17 -15
  47. package/src/server/args.ts +126 -0
  48. package/src/server/binaries.ts +47 -0
  49. package/src/server/config.ts +54 -0
  50. package/{server → src/server}/control.ts +239 -81
  51. package/src/server/docs.md +51 -0
  52. package/src/server/line-editor.ts +200 -0
  53. package/src/server/main.ts +473 -0
  54. package/src/server/mode.ts +92 -0
  55. package/src/server/rebuild.ts +90 -0
  56. package/src/server/registry.ts +138 -0
  57. package/src/server/repl.ts +223 -0
  58. package/src/server/state.ts +54 -0
  59. package/{server → src/server}/tsconfig.json +1 -1
  60. package/{server → src/server}/tunnel.ts +6 -6
  61. package/src/server/watcher.ts +121 -0
  62. package/src/tool/main.ts +70 -0
  63. package/src/types/bundle.d.ts +24 -0
  64. package/src/types/control.d.ts +90 -0
  65. package/src/types/registry.d.ts +16 -0
  66. package/scaffold/AGENTS.md +0 -185
  67. package/scaffold/package.json +0 -22
  68. package/scaffold/templates/components/icon.tsx +0 -48
  69. package/scaffold/templates/default/icon.tsx +0 -48
  70. package/server/main.ts +0 -308
  71. package/server/rebuild.ts +0 -76
  72. package/server/state.ts +0 -94
  73. package/src/args.ts +0 -210
  74. package/src/bundle-cli.ts +0 -13
  75. package/src/commands/client.ts +0 -34
  76. package/src/commands/mcp.ts +0 -617
  77. package/src/commands/server.ts +0 -73
  78. package/src/dev-android.ts +0 -176
  79. package/src/dev-client.ts +0 -29
  80. package/src/dev-server.ts +0 -302
  81. package/src/repl.ts +0 -249
  82. package/src/util.ts +0 -122
  83. package/src/watcher.ts +0 -73
  84. /package/src/{untyped-deps.d.ts → bundle/untyped-deps.d.ts} +0 -0
  85. /package/src/{prompt.ts → init/prompt.ts} +0 -0
  86. /package/{scaffold → src/init/scaffold}/gitignore +0 -0
  87. /package/{scaffold → src/init/scaffold}/icon.svg +0 -0
  88. /package/{scaffold → src/init/scaffold}/mcp.json +0 -0
  89. /package/{scaffold → src/init/scaffold}/tsconfig.json +0 -0
  90. /package/{server → src/server}/cache.ts +0 -0
  91. /package/{server → src/server}/proxy.ts +0 -0
  92. /package/{server → src/server}/qr.ts +0 -0
  93. /package/{server → src/server}/remap.ts +0 -0
@@ -1,15 +1,18 @@
1
- import { values, source, isPrebuilt } from "../args"
1
+ import { values, source, isPrebuilt } from "../lib/args"
2
2
  import {
3
3
  bundleFlux,
4
- bundleIsolatesDir,
5
4
  bundleSolid,
5
+ bundleWith,
6
6
  compileToBytecode,
7
7
  findFluxIsolates,
8
+ isolateManifestAssets,
8
9
  readPrebuiltIsolates,
9
10
  walkFiles,
10
11
  writeIsolates,
11
- } from "../bundler"
12
- import { projectDirFor } from "../project"
12
+ } from "./bundler"
13
+ import { resolveMode } from "../lib/mode"
14
+ import { buildManifest } from "../lib/project"
15
+ import type { BundleOutput } from "../types/bundle"
13
16
  import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs"
14
17
  import { basename, dirname, join, resolve } from "node:path"
15
18
 
@@ -23,12 +26,13 @@ function writeStdout(data: string): Promise<void> {
23
26
  }
24
27
 
25
28
  // The bundle output dir (okf/backlog/build-output-dirs.md): the bundle flow's
26
- // subdir of the build root, or an explicit --output dir. Only reused when it
27
- // is empty or already a bundle output (a *.srt.* or *.flux.* bundle at top
28
- // level) - the writePackFolder rule - so it never writes into an unrelated
29
- // directory.
30
- function ensureOutDir(entry: string): string {
31
- let outDir = values.output ?? join(projectDirFor(entry), "dist", "bundle")
29
+ // subdir of the build root, or an explicit --output dir. The build root is
30
+ // the cwd: a project command runs in its root (mode.ts), and a file on its
31
+ // own builds where it is run from. Only reused when it is empty or already a
32
+ // bundle output (a *.srt.* or *.flux.* bundle at top level) - the
33
+ // writePackFolder rule - so it never writes into an unrelated directory.
34
+ function ensureOutDir(defaultDir = join("dist", "bundle")): string {
35
+ let outDir = values.output ?? defaultDir
32
36
  let existing = existsSync(outDir) ? readdirSync(outDir) : null
33
37
  if (existing && existing.length > 0 && !existing.some((name) => /\.(srt|flux)\.(js|bin)$/.test(name))) {
34
38
  console.error(`${resolve(outDir)} exists and is not a bundle output; choose another --output`)
@@ -64,7 +68,7 @@ async function writeBytecode(jsCode: string, outfile: string) {
64
68
  console.log(`>> wrote ${binSize} bytes to ${outfile}`)
65
69
  }
66
70
 
67
- export async function runBundleCommand() {
71
+ export async function main() {
68
72
  if (values.flux) {
69
73
  let entry = resolve(source!)
70
74
  let name = basename(entry).replace(/\.[jt]s$/, "")
@@ -83,7 +87,7 @@ export async function runBundleCommand() {
83
87
  await writeStdout(jsCode)
84
88
  process.exit()
85
89
  }
86
- let outDir = ensureOutDir(entry)
90
+ let outDir = ensureOutDir()
87
91
  if (values.compile) {
88
92
  await writeBytecode(jsCode, join(outDir, name + ".flux.bin"))
89
93
  } else {
@@ -110,27 +114,65 @@ export async function runBundleCommand() {
110
114
  process.exit()
111
115
  }
112
116
 
117
+ // --json: the dev server's rebuild (server/rebuild.ts spawns it in the
118
+ // project root or the entry's directory with the entry and --project or
119
+ // --file, so the mode resolves the same). One BundleOutput object on
120
+ // stdout (types/bundle.d.ts), diagnostics on stderr, exit 1 with an empty
121
+ // stdout on a build failure. A prebuilt .srt.js is read as-is with its
122
+ // sibling isolate bundles.
123
+ if (values.json) {
124
+ let mode = resolveMode()
125
+ let result: BundleOutput | null
126
+ if (mode.entry.endsWith(".srt.js")) {
127
+ let code = await Bun.file(mode.entry).text()
128
+ let isolates = readPrebuiltIsolates(mode.entry).map((i) => ({ ...i, map: null }))
129
+ result = {
130
+ code,
131
+ map: null,
132
+ manifest: buildManifest(code, mode.entry, isolateManifestAssets(isolates), mode.projectDir),
133
+ isolates,
134
+ inputs: [mode.entry],
135
+ }
136
+ } else {
137
+ result = await bundleWith({
138
+ entry: mode.entry,
139
+ project: mode.projectDir,
140
+ dev: values.dev,
141
+ minify: values.minify,
142
+ })
143
+ }
144
+ if (!result) process.exit(1)
145
+ await writeStdout(JSON.stringify(result))
146
+ process.exit()
147
+ }
148
+
149
+ // A prebuilt .srt.js (validateArgs admits no other prebuilt form) is
150
+ // compiled to bytecode: the only step left, so --compile is implied. The
151
+ // output lands in the bundle's own dir unless --output says otherwise; its
152
+ // isolate bundles compile along into the output's isolates/ (the ids match,
153
+ // the extension picks the form).
113
154
  if (isPrebuilt) {
114
- if (!source!.endsWith(".srt.js")) {
115
- console.error("Can only compile .srt.js files. .srt.bin is already compiled.")
155
+ let jsFile = resolve(source!)
156
+ if (!existsSync(jsFile)) {
157
+ console.error(`Entry not found: ${source}`)
116
158
  process.exit(1)
117
159
  }
118
- let jsFile = resolve(source!)
119
- let binOut = jsFile.replace(/\.srt\.js$/, ".srt.bin").replace(/\.js$/, ".bin")
120
- await writeBytecode(await Bun.file(jsFile).text(), binOut)
121
- // The isolate bundles compile along, .bin beside .js in the output's
122
- // isolates/ dir (the ids match, the extension picks the form).
160
+ let outDir = ensureOutDir(dirname(jsFile))
161
+ await writeBytecode(await Bun.file(jsFile).text(), join(outDir, basename(jsFile).replace(/\.js$/, ".bin")))
162
+ let isolatesDir = join(outDir, "isolates")
163
+ clearIsolates(isolatesDir, ".bin")
123
164
  for (let isolate of readPrebuiltIsolates(jsFile)) {
124
- await writeIsolateBytecode(bundleIsolatesDir(jsFile), isolate)
165
+ await writeIsolateBytecode(isolatesDir, isolate)
125
166
  }
126
167
  process.exit()
127
168
  }
128
169
 
129
- let entry = resolve(source!)
170
+ let mode = resolveMode()
171
+ let entry = mode.entry
130
172
  let name = basename(entry).replace(/\.[jt]sx?$/, "")
131
173
 
132
174
  if (values.stdout) {
133
- let result = await bundleSolid()
175
+ let result = await bundleSolid(mode)
134
176
  if (result.isolates.length) {
135
177
  console.error("[cli] Warning: this app has isolate modules; --stdout carries only the main bundle")
136
178
  }
@@ -138,11 +180,11 @@ export async function runBundleCommand() {
138
180
  process.exit()
139
181
  }
140
182
 
141
- let outDir = ensureOutDir(entry)
183
+ let outDir = ensureOutDir()
142
184
  let isolatesDir = join(outDir, "isolates")
143
185
 
144
186
  if (values.compile) {
145
- let result = await bundleSolid()
187
+ let result = await bundleSolid(mode)
146
188
  await writeBytecode(result.code, join(outDir, name + ".srt.bin"))
147
189
  clearIsolates(isolatesDir, ".bin")
148
190
  for (let isolate of result.isolates) {
@@ -151,7 +193,7 @@ export async function runBundleCommand() {
151
193
  process.exit()
152
194
  }
153
195
 
154
- let result = await bundleSolid()
196
+ let result = await bundleSolid(mode)
155
197
  let jsOutfile = join(outDir, name + ".srt.js")
156
198
  await Bun.write(jsOutfile, result.code)
157
199
  clearIsolates(isolatesDir, ".js")
@@ -0,0 +1,10 @@
1
+ # srt check
2
+
3
+ {{ usage check }}
4
+
5
+ The gate worth wiring into CI: it builds and typechecks the app without
6
+ producing build output. With a folder it covers every entry under it
7
+ (`src/index.tsx`, `examples/*`, and in a monorepo every example app,
8
+ package example and package demo), so one call at the repo root answers
9
+ "did I break any example" before pushing. The dev server runs the same check once at
10
+ startup, without gating on it.
@@ -0,0 +1,85 @@
1
+ import { existsSync, statSync } from "node:fs"
2
+ import { Glob } from "bun"
3
+ import { join, resolve } from "node:path"
4
+ import { source } from "../lib/args"
5
+ import { bundleWith } from "../bundle/bundler"
6
+ import { findProject } from "../lib/project"
7
+ import { findProjectRoot, reportTypes, typecheck } from "./typecheck"
8
+
9
+ // srt check: verify the app without side effects. Bundles in memory (nothing
10
+ // written, so no dev-server reload fires and no build outputs land in the
11
+ // project) and typechecks with the project's own tsc (typecheck.ts).
12
+
13
+ // Check one entry: bundle in memory, then typecheck. Returns whether it passed.
14
+ async function checkEntry(entry: string): Promise<boolean> {
15
+ let failed = false
16
+ // check verifies trees of entries from one cwd, so it is the one command
17
+ // that walks up from each entry to its project.
18
+ let result = await bundleWith({ entry, dev: true, minify: false, project: findProject(entry)?.dir ?? null })
19
+ if (!result) {
20
+ // bundleWith already printed the compile errors.
21
+ failed = true
22
+ }
23
+ let root = findProjectRoot(entry)
24
+ if (!root) {
25
+ console.warn("Typecheck skipped: no tsconfig.json or package.json above the entry")
26
+ } else {
27
+ let types = await typecheck(root, entry)
28
+ if (types && reportTypes(types)) failed = true
29
+ }
30
+ return !failed
31
+ }
32
+
33
+ // The entries `srt check <folder>` covers, relative to the folder (a bare
34
+ // `srt check` is `srt check .`): the app itself, its own examples, and in
35
+ // a monorepo every example app, package example and package demo. The same
36
+ // set CI gates, so one call at the repo root answers "did I break any
37
+ // example" before pushing. Entries, not files: a source no entry imports is
38
+ // not checked.
39
+ const CHECK_ALL_GLOBS = [
40
+ "src/index.tsx",
41
+ "examples/*.tsx",
42
+ "examples/*/src/index.tsx",
43
+ "packages/*/examples/*.tsx",
44
+ "packages/*/demos/src/*.tsx",
45
+ ]
46
+ function discoverEntries(root: string): string[] {
47
+ let entries: string[] = []
48
+ for (let pattern of CHECK_ALL_GLOBS) {
49
+ entries.push(...[...new Glob(pattern).scanSync({ cwd: root })].map((e) => join(root, e)))
50
+ }
51
+ return entries.sort()
52
+ }
53
+
54
+ export async function main() {
55
+ let target = source ?? "."
56
+ if (!existsSync(target)) {
57
+ // Without this, the missing file surfaces later as an internal ENOENT
58
+ // stack trace (scandir/Bun.build), which reads as a CLI bug - the common
59
+ // cause is just running from the wrong directory.
60
+ console.error(`No such entry: ${target} (resolved from ${process.cwd()})`)
61
+ process.exit(1)
62
+ }
63
+ if (!statSync(target).isDirectory()) {
64
+ if (!(await checkEntry(target))) process.exit(1)
65
+ console.log("Check passed")
66
+ process.exit(0)
67
+ }
68
+
69
+ let entries = discoverEntries(target)
70
+ if (entries.length === 0) {
71
+ console.error(`No entries found under ${resolve(target)} (looked for ${CHECK_ALL_GLOBS.join(", ")})`)
72
+ process.exit(1)
73
+ }
74
+ let failures: string[] = []
75
+ for (let entry of entries) {
76
+ console.log(`== ${entry}`)
77
+ if (!(await checkEntry(entry))) failures.push(entry)
78
+ }
79
+ if (failures.length > 0) {
80
+ console.error(`${failures.length} of ${entries.length} entries failed:\n ${failures.join("\n ")}`)
81
+ process.exit(1)
82
+ }
83
+ console.log(`Check passed (${entries.length} entries)`)
84
+ process.exit(0)
85
+ }
@@ -1,14 +1,12 @@
1
1
  import { existsSync, mkdirSync, rmSync } from "node:fs"
2
2
  import { dirname, join, resolve } from "node:path"
3
- import { source } from "../args"
4
- import { bundleWith, findIsolateModules } from "../bundler"
3
+ import { findIsolateModules } from "../bundle/bundler"
5
4
 
6
- // srt check: verify the app without side effects. Bundles in memory (nothing
7
- // written, so no dev-server reload fires and no build outputs land in the
8
- // project) and typechecks with the project's own tsc, reporting only
9
- // diagnostics in app code. @solidrt packages ship raw .ts sources, so a strict
10
- // consumer config surfaces their internal errors too; those are counted and
11
- // hidden, not the caller's problem to wade through.
5
+ // Typechecking an app entry with the project's own tsc, reporting only
6
+ // diagnostics in app code. Used by `srt check` (the hard gate) and by the dev
7
+ // server's startup check (it spawns `srt check <entry>`). @solidrt packages ship
8
+ // raw .ts sources, so a strict consumer config surfaces their internal errors
9
+ // too; those are counted and hidden, not the caller's problem to wade through.
12
10
 
13
11
  // Walk up from the entry to the enclosing project (tsconfig.json or, failing
14
12
  // that, package.json).
@@ -27,7 +25,7 @@ export function findProjectRoot(entry: string): string | null {
27
25
 
28
26
  // One tsc --pretty false diagnostic: the "path(line,col): error TS...: ..."
29
27
  // head line plus any indented continuation lines.
30
- type Diagnostic = { head: string; lines: string[]; inDependencies: boolean }
28
+ export type Diagnostic = { head: string; lines: string[]; inDependencies: boolean }
31
29
 
32
30
  function parseDiagnostics(output: string): Diagnostic[] {
33
31
  let diagnostics: Diagnostic[] = []
@@ -58,6 +56,8 @@ function findTsc(fromDir: string): string | null {
58
56
  }
59
57
  }
60
58
 
59
+ export type TypecheckResult = { app: Diagnostic[]; hidden: number }
60
+
61
61
  // Typecheck the entry's program, not the enclosing project: a transient
62
62
  // config extends the project's tsconfig and roots the program at the entry
63
63
  // alone (plus the project's ambient declarations), so tsc checks exactly the
@@ -65,7 +65,7 @@ function findTsc(fromDir: string): string | null {
65
65
  // config lives in the project-local .srt-data (the dev-artifact dir; absolute
66
66
  // paths inside, so its location only matters for type-package resolution,
67
67
  // which walks up to the project's node_modules from there).
68
- export async function typecheck(root: string, entry: string): Promise<{ app: Diagnostic[]; hidden: number } | null> {
68
+ export async function typecheck(root: string, entry: string): Promise<TypecheckResult | null> {
69
69
  let tsconfig = join(root, "tsconfig.json")
70
70
  if (!existsSync(tsconfig)) {
71
71
  console.warn("Typecheck skipped: no tsconfig.json above the entry")
@@ -111,7 +111,7 @@ export async function typecheck(root: string, entry: string): Promise<{ app: Dia
111
111
  // whether app-code errors were found. Callers pass repl-aware printers when
112
112
  // the output lands over a live prompt (the dev-server startup check).
113
113
  export function reportTypes(
114
- types: { app: Diagnostic[]; hidden: number },
114
+ types: TypecheckResult,
115
115
  log: (...args: any[]) => void = console.log,
116
116
  error: (...args: any[]) => void = console.error,
117
117
  ): boolean {
@@ -125,33 +125,3 @@ export function reportTypes(
125
125
  else log("Types OK")
126
126
  return false
127
127
  }
128
-
129
- export async function runCheckCommand() {
130
- let entry = source!
131
- if (!existsSync(entry)) {
132
- // Without this, the missing file surfaces later as an internal ENOENT
133
- // stack trace (scandir/Bun.build), which reads as a CLI bug - the common
134
- // cause is just running from the wrong directory.
135
- console.error(`No such entry: ${entry} (resolved from ${process.cwd()})`)
136
- process.exit(1)
137
- }
138
- let failed = false
139
-
140
- let result = await bundleWith({ entry, dev: true, minify: false })
141
- if (!result) {
142
- // bundleWith already printed the compile errors.
143
- failed = true
144
- }
145
-
146
- let root = findProjectRoot(entry)
147
- if (!root) {
148
- console.warn("Typecheck skipped: no tsconfig.json or package.json above the entry")
149
- } else {
150
- let types = await typecheck(root, entry)
151
- if (types && reportTypes(types)) failed = true
152
- }
153
-
154
- if (failed) process.exit(1)
155
- console.log("Check passed")
156
- process.exit(0)
157
- }
@@ -0,0 +1,9 @@
1
+ # srt client
2
+
3
+ {{ usage client }}
4
+
5
+ The client half of [srt run](../server/docs.md), on its own. Without flags
6
+ it attaches to the dev server of the project (or file) in the current
7
+ directory; `--port` picks a local server by port and `--server` names any
8
+ address, which is how a second machine joins a server started with `--lan`.
9
+ A phone or tablet is [srt android](../android/docs.md).
@@ -0,0 +1,33 @@
1
+ import { values, port, clientStorageArgs } from "../lib/args"
2
+ import { requireBinary, run } from "../lib/util"
3
+ import { resolveFromCwd } from "../lib/registry"
4
+
5
+ // Standalone solidrt-go client (no dev server of its own). Without flags it
6
+ // attaches to the dev server of the project (or file) in the current
7
+ // directory, resolved from the registry; --port picks a local server by port
8
+ // and --server names any address. A device is `srt android`.
9
+ export async function main() {
10
+ let runner = requireBinary("solidrt-go")
11
+ let args: string[] = [...clientStorageArgs()]
12
+ if (values.size) args.push("--size", values.size)
13
+ let address: string
14
+ if (values.server) {
15
+ if (!values.server.includes(":")) {
16
+ console.error(`--server needs host:port (got "${values.server}"); dev servers have no fixed port`)
17
+ process.exit(1)
18
+ }
19
+ address = values.server
20
+ } else if (port !== undefined) {
21
+ address = `127.0.0.1:${port}`
22
+ } else {
23
+ let resolved = await resolveFromCwd(process.cwd())
24
+ if (!resolved.ok) {
25
+ console.error(resolved.message)
26
+ process.exit(1)
27
+ }
28
+ address = `127.0.0.1:${resolved.record.port}`
29
+ }
30
+ args.push("--dev-server", address)
31
+ let exit = await run(runner, args)
32
+ process.exit(exit)
33
+ }
@@ -0,0 +1,14 @@
1
+ # srt console
2
+
3
+ {{ usage console }}
4
+
5
+ The dev console: the dev servers running on this machine, the clients
6
+ attached to each, and a button to start a local client for one. It is not
7
+ served by a dev server: the CLI ships the console pre-compiled
8
+ (`dist/console.srtapp`, see [srt pack --app](../pack/docs.md)) and this
9
+ command starts the `solidrt` runner on that file, so it opens at once and
10
+ runs the same whether a project is open or not.
11
+
12
+ In a checkout the file is built with `make -C packages/cli
13
+ dist/console.srtapp`; rerun it after editing `apps/console`. Everything
14
+ after `--` reaches the console as its arguments.
@@ -0,0 +1,21 @@
1
+ import { existsSync } from "node:fs"
2
+ import { fileURLToPath } from "node:url"
3
+ import { appArgs } from "../lib/args"
4
+ import { requireBinary, run } from "../lib/util"
5
+
6
+ // srt console: the dev console (apps/console) as a packed app on the plain
7
+ // runner. The console is pre-compiled: dist/console.srtapp (manifest +
8
+ // bytecode + assets, `srt pack --app`), built by `make -C packages/cli
9
+ // dist/console.srtapp` - the release workflow before publishing, a checkout
10
+ // after editing the console. This command builds nothing: it points the
11
+ // runner at the file, and the runner is used in place, so a signed runner
12
+ // stays signed.
13
+ export async function main() {
14
+ let app = fileURLToPath(new URL("../../dist/console.srtapp", import.meta.url))
15
+ if (!existsSync(app)) {
16
+ console.error("Console not built: run make -C packages/cli dist/console.srtapp")
17
+ process.exit(1)
18
+ }
19
+ let exit = await run(requireBinary("solidrt"), [app, ...appArgs])
20
+ process.exit(exit)
21
+ }
@@ -0,0 +1,27 @@
1
+ # srt demo
2
+
3
+ {{ usage demo }}
4
+
5
+ The demos that come with the packages this project installs. Without an
6
+ argument it lists them, numbered:
7
+
8
+ ```
9
+ 1 3d/the-third-dimension
10
+ ```
11
+
12
+ `srt demo 1` runs that one, exactly as [srt run](../server/docs.md) runs any
13
+ project: a dev server, a local client window, reload on save, and the same
14
+ server options (`--lan` to reach it from a device, `--port`, `--tunnel`). The qualified
15
+ name works too (`srt demo 3d/the-third-dimension`), which is the stable way to
16
+ name one in a script - the numbers follow the list, and the list follows what
17
+ is installed.
18
+
19
+ A package keeps its demos in `demos/`, and that folder is one project: its own
20
+ package.json, one `assets/` folder, and `src/<name>.tsx` per demo. So the
21
+ demos of one package share a dev server and a port - start a second one while
22
+ the first is up and it says so. Demos from different packages run side by
23
+ side.
24
+
25
+ The demos ship inside the packages, so `node_modules` is where they are found:
26
+ `srt demo` looks in the current directory and nowhere above it. Run it from
27
+ the project root.
@@ -0,0 +1,67 @@
1
+ import { readdirSync } from "node:fs"
2
+ import { join, resolve } from "node:path"
3
+ import { source } from "../lib/args"
4
+
5
+ // srt demo: the demos the installed @solidrt packages ship. A package's
6
+ // demos are ONE project (`<package>/demos/`: package.json, one assets/, and
7
+ // src/<name>.tsx per demo), so a demo runs as the project it lives in - this
8
+ // file only lists and resolves, and main.ts starts the ordinary dev server
9
+ // with its cwd set to that project. Nothing downstream knows about demos.
10
+
11
+ export type Demo = { name: string; cwd: string; entry: string }
12
+
13
+ const SCOPE = join("node_modules", "@solidrt")
14
+
15
+ /** Every demo installed here, sorted so the printed numbers are stable.
16
+ * The cwd and nothing above it - the same rule the server's mode resolution
17
+ * follows (server/mode.ts), so this lists what THIS project installed. */
18
+ function discover(): Demo[] {
19
+ let demos: Demo[] = []
20
+ for (let pkg of names(SCOPE)) {
21
+ let dir = join(SCOPE, pkg, "demos")
22
+ for (let file of names(join(dir, "src"))) {
23
+ if (!file.endsWith(".tsx")) continue
24
+ demos.push({ name: `${pkg}/${file.slice(0, -".tsx".length)}`, cwd: resolve(dir), entry: join("src", file) })
25
+ }
26
+ }
27
+ return demos
28
+ }
29
+
30
+ // A missing folder is the normal case (most packages ship no demos), so it
31
+ // reads as an empty one rather than an error.
32
+ function names(dir: string): string[] {
33
+ try {
34
+ return readdirSync(dir).sort()
35
+ } catch {
36
+ return []
37
+ }
38
+ }
39
+
40
+ function list(demos: Demo[]) {
41
+ for (let [i, demo] of demos.entries()) console.log(` ${String(i + 1).padStart(2)} ${demo.name}`)
42
+ }
43
+
44
+ /** Lists (no argument) or resolves one demo for the dev server to serve. */
45
+ export async function main(): Promise<{ cwd: string; entry: string } | undefined> {
46
+ let demos = discover()
47
+ if (demos.length === 0) {
48
+ console.error(`No demos installed in ${process.cwd()} (looked in ${SCOPE}/*/demos/src/)`)
49
+ process.exit(1)
50
+ }
51
+
52
+ if (source === undefined) {
53
+ list(demos)
54
+ console.log("\nRun one with: srt demo <number>")
55
+ return undefined
56
+ }
57
+
58
+ // A number picks from the printed list; the qualified name it printed
59
+ // works too, so a demo can be named in a script without a fixed index.
60
+ let picked = /^\d+$/.test(source) ? demos[Number(source) - 1] : demos.find((d) => d.name === source)
61
+ if (!picked) {
62
+ console.error(`No such demo: ${source}`)
63
+ list(demos)
64
+ process.exit(1)
65
+ }
66
+ return { cwd: picked.cwd, entry: picked.entry }
67
+ }
@@ -0,0 +1,11 @@
1
+ # srt init
2
+
3
+ {{ usage init }}
4
+
5
+ Scaffolds a new SolidRT project into a new (empty) folder: package.json,
6
+ tsconfig.json, AGENTS.md, a starter src/index.tsx, an empty assets/
7
+ (everything in it ships with the app) and an `.mcp.json`, then installs the
8
+ dependencies. The picker offers extensions on an interactive terminal.
9
+
10
+ The public entry point is `bun create solidrt <dir>`, which forwards here and
11
+ needs nothing installed first.
@@ -1,15 +1,15 @@
1
1
  import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises"
2
2
  import { basename, dirname, join, resolve } from "node:path"
3
- import { source, values } from "../args"
4
- import { multiselect, note, text } from "../prompt"
3
+ import { source, values } from "../lib/args"
4
+ import { multiselect, note, text } from "./prompt"
5
5
 
6
6
  const DEFAULT_NAME = "solidrt-app"
7
7
 
8
- const SCAFFOLD_DIR = join(import.meta.dir, "../../scaffold")
8
+ const SCAFFOLD_DIR = join(import.meta.dir, "scaffold")
9
9
  const TEMPLATES_DIR = join(SCAFFOLD_DIR, "templates")
10
10
 
11
11
  // Shared project files written for every template. Sources live in
12
- // cli/scaffold/. The .gitignore is stored there as `gitignore` because npm
12
+ // cli/src/init/scaffold/. The .gitignore is stored there as `gitignore` because npm
13
13
  // strips files literally named `.gitignore` from published packages, so it is
14
14
  // renamed on the way out. The per-template src/ comes from scaffold/templates/.
15
15
  const TEMPLATE_FILES: Array<{ from: string; to: string }> = [
@@ -31,22 +31,26 @@ function packageName(dir: string): string {
31
31
  const DEFAULT_TEMPLATE = "default"
32
32
 
33
33
  // One AGENTS.md serves every template, so the lines that point an agent at
34
- // @solidrt/components docs are fenced between markers: with the extension
35
- // selected only the markers go, without it the block goes too, so a core-only
36
- // app never ships references to files that are not installed.
37
- const MARKED_BLOCK = /^<!-- components:begin -->\n[\s\S]*?^<!-- components:end -->\n/gm
38
- const MARKER = /^<!-- components:(?:begin|end) -->\n/gm
39
-
34
+ // an extension's docs are fenced between `<!-- <key>:begin/end -->` markers:
35
+ // with the extension selected only the markers go, without it the block goes
36
+ // too, so an app never ships references to files that are not installed.
40
37
  function resolveMarkers(text: string, extensions: Extension[]): string {
41
- let selected = extensions.some((e) => e.pkg === "@solidrt/components")
42
- return selected ? text.replace(MARKER, "") : text.replace(MARKED_BLOCK, "")
38
+ for (let ext of EXTENSIONS) {
39
+ let selected = extensions.includes(ext)
40
+ let block = new RegExp(`^<!-- ${ext.key}:begin -->\\n[\\s\\S]*?^<!-- ${ext.key}:end -->\\n`, "gm")
41
+ let marker = new RegExp(`^<!-- ${ext.key}:(?:begin|end) -->\\n`, "gm")
42
+ text = selected ? text.replace(marker, "") : text.replace(block, "")
43
+ }
44
+ return text
43
45
  }
44
46
 
45
47
  // Optional packages an app can opt into on top of core. Each maps to a
46
48
  // dependency in the scaffold package.json (kept when selected, removed
47
- // otherwise) and optionally to a starter under scaffold/templates/.
49
+ // otherwise), to a marker key fencing its lines in scaffold/AGENTS.md, and
50
+ // optionally to a starter under scaffold/templates/.
48
51
  interface Extension {
49
52
  pkg: string
53
+ key: string
50
54
  template?: string
51
55
  description: string
52
56
  }
@@ -54,10 +58,12 @@ interface Extension {
54
58
  const EXTENSIONS: Extension[] = [
55
59
  {
56
60
  pkg: "@solidrt/components",
61
+ key: "components",
57
62
  template: "components",
58
63
  description: "component framework: widgets, theming, navigation",
59
64
  },
60
- { pkg: "@solidrt/3d", description: "general purpose 3D library" },
65
+ { pkg: "@solidrt/2d", key: "2d", description: "general purpose 2D library" },
66
+ { pkg: "@solidrt/3d", key: "3d", description: "general purpose 3D library" },
61
67
  ]
62
68
 
63
69
  // Resolve which extensions the app takes: an interactive picker on a TTY,
@@ -80,7 +86,7 @@ function resolveTemplate(extensions: Extension[]): string {
80
86
  return extensions.find((e) => e.template)?.template ?? DEFAULT_TEMPLATE
81
87
  }
82
88
 
83
- export async function runInitCommand() {
89
+ export async function main() {
84
90
  // The target folder comes from the positional arg, or an interactive prompt
85
91
  // (defaulting to a suggested name) when omitted.
86
92
  let dir = source