rowork 0.1.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 (187) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/bin/rowork.js +18 -0
  4. package/dashboard/app.css +234 -0
  5. package/dashboard/app.js +117 -0
  6. package/dashboard/index.html +67 -0
  7. package/dist/cli/errors.d.ts +15 -0
  8. package/dist/cli/errors.js +18 -0
  9. package/dist/cli/program.d.ts +19 -0
  10. package/dist/cli/program.js +59 -0
  11. package/dist/cli/registry.d.ts +37 -0
  12. package/dist/cli/registry.js +61 -0
  13. package/dist/commands/add.d.ts +8 -0
  14. package/dist/commands/add.js +77 -0
  15. package/dist/commands/assets-setup.d.ts +18 -0
  16. package/dist/commands/assets-setup.js +136 -0
  17. package/dist/commands/assets.d.ts +2 -0
  18. package/dist/commands/assets.js +153 -0
  19. package/dist/commands/console.d.ts +4 -0
  20. package/dist/commands/console.js +141 -0
  21. package/dist/commands/dashboard.d.ts +2 -0
  22. package/dist/commands/dashboard.js +50 -0
  23. package/dist/commands/dev-background.d.ts +3 -0
  24. package/dist/commands/dev-background.js +87 -0
  25. package/dist/commands/dev.d.ts +2 -0
  26. package/dist/commands/dev.js +270 -0
  27. package/dist/commands/eject.d.ts +2 -0
  28. package/dist/commands/eject.js +136 -0
  29. package/dist/commands/index.d.ts +8 -0
  30. package/dist/commands/index.js +54 -0
  31. package/dist/commands/info.d.ts +3 -0
  32. package/dist/commands/info.js +50 -0
  33. package/dist/commands/init.d.ts +2 -0
  34. package/dist/commands/init.js +55 -0
  35. package/dist/commands/links.d.ts +32 -0
  36. package/dist/commands/links.js +152 -0
  37. package/dist/commands/make-event.d.ts +2 -0
  38. package/dist/commands/make-event.js +160 -0
  39. package/dist/commands/make-menu.d.ts +3 -0
  40. package/dist/commands/make-menu.js +36 -0
  41. package/dist/commands/make-stat.d.ts +2 -0
  42. package/dist/commands/make-stat.js +199 -0
  43. package/dist/commands/make-tool.d.ts +2 -0
  44. package/dist/commands/make-tool.js +124 -0
  45. package/dist/commands/make-ui.d.ts +3 -0
  46. package/dist/commands/make-ui.js +199 -0
  47. package/dist/commands/make.d.ts +21 -0
  48. package/dist/commands/make.js +181 -0
  49. package/dist/commands/next-steps.d.ts +5 -0
  50. package/dist/commands/next-steps.js +18 -0
  51. package/dist/commands/start.d.ts +2 -0
  52. package/dist/commands/start.js +144 -0
  53. package/dist/commands/studio.d.ts +3 -0
  54. package/dist/commands/studio.js +45 -0
  55. package/dist/commands/update.d.ts +2 -0
  56. package/dist/commands/update.js +173 -0
  57. package/dist/commands/wire.d.ts +2 -0
  58. package/dist/commands/wire.js +34 -0
  59. package/dist/core/agent-docs.d.ts +23 -0
  60. package/dist/core/agent-docs.js +151 -0
  61. package/dist/core/assets.d.ts +96 -0
  62. package/dist/core/assets.js +268 -0
  63. package/dist/core/background.d.ts +47 -0
  64. package/dist/core/background.js +149 -0
  65. package/dist/core/config.d.ts +9 -0
  66. package/dist/core/config.js +62 -0
  67. package/dist/core/exec.d.ts +22 -0
  68. package/dist/core/exec.js +38 -0
  69. package/dist/core/format-generated.d.ts +12 -0
  70. package/dist/core/format-generated.js +28 -0
  71. package/dist/core/generate.d.ts +37 -0
  72. package/dist/core/generate.js +94 -0
  73. package/dist/core/github-release.d.ts +28 -0
  74. package/dist/core/github-release.js +38 -0
  75. package/dist/core/info.d.ts +77 -0
  76. package/dist/core/info.js +65 -0
  77. package/dist/core/modules.d.ts +32 -0
  78. package/dist/core/modules.js +265 -0
  79. package/dist/core/naming.d.ts +6 -0
  80. package/dist/core/naming.js +26 -0
  81. package/dist/core/open-cloud.d.ts +58 -0
  82. package/dist/core/open-cloud.js +170 -0
  83. package/dist/core/project-index.d.ts +26 -0
  84. package/dist/core/project-index.js +59 -0
  85. package/dist/core/rojo-edit.d.ts +18 -0
  86. package/dist/core/rojo-edit.js +53 -0
  87. package/dist/core/rokit-installer.d.ts +15 -0
  88. package/dist/core/rokit-installer.js +77 -0
  89. package/dist/core/scaffold.d.ts +44 -0
  90. package/dist/core/scaffold.js +163 -0
  91. package/dist/core/schema-edit.d.ts +46 -0
  92. package/dist/core/schema-edit.js +125 -0
  93. package/dist/core/source-scan.d.ts +24 -0
  94. package/dist/core/source-scan.js +84 -0
  95. package/dist/core/studio.d.ts +83 -0
  96. package/dist/core/studio.js +225 -0
  97. package/dist/core/toolchain.d.ts +31 -0
  98. package/dist/core/toolchain.js +98 -0
  99. package/dist/core/tsconfig-edit.d.ts +11 -0
  100. package/dist/core/tsconfig-edit.js +47 -0
  101. package/dist/core/ui-edit.d.ts +31 -0
  102. package/dist/core/ui-edit.js +89 -0
  103. package/dist/core/versions.d.ts +24 -0
  104. package/dist/core/versions.js +67 -0
  105. package/dist/dashboard/open.d.ts +3 -0
  106. package/dist/dashboard/open.js +18 -0
  107. package/dist/dashboard/server.d.ts +28 -0
  108. package/dist/dashboard/server.js +191 -0
  109. package/dist/index.d.ts +3 -0
  110. package/dist/index.js +3 -0
  111. package/dist/main.d.ts +9 -0
  112. package/dist/main.js +135 -0
  113. package/dist/modules/format.d.ts +3 -0
  114. package/dist/modules/format.js +28 -0
  115. package/dist/modules/index.d.ts +7 -0
  116. package/dist/modules/index.js +12 -0
  117. package/dist/modules/integrations.d.ts +8 -0
  118. package/dist/modules/integrations.js +54 -0
  119. package/dist/modules/leaderstats.d.ts +3 -0
  120. package/dist/modules/leaderstats.js +104 -0
  121. package/dist/modules/lint.d.ts +5 -0
  122. package/dist/modules/lint.js +43 -0
  123. package/dist/modules/networking.d.ts +23 -0
  124. package/dist/modules/networking.js +246 -0
  125. package/dist/modules/player-data.d.ts +19 -0
  126. package/dist/modules/player-data.js +176 -0
  127. package/dist/modules/types.d.ts +96 -0
  128. package/dist/modules/types.js +2 -0
  129. package/dist/modules/ui.d.ts +3 -0
  130. package/dist/modules/ui.js +67 -0
  131. package/dist/plugins/api.d.ts +127 -0
  132. package/dist/plugins/api.js +21 -0
  133. package/dist/plugins/loader.d.ts +20 -0
  134. package/dist/plugins/loader.js +128 -0
  135. package/dist/process/log-mux.d.ts +13 -0
  136. package/dist/process/log-mux.js +31 -0
  137. package/dist/process/supervisor.d.ts +48 -0
  138. package/dist/process/supervisor.js +141 -0
  139. package/dist/process/tree-kill.d.ts +25 -0
  140. package/dist/process/tree-kill.js +70 -0
  141. package/dist/templates/engine.d.ts +18 -0
  142. package/dist/templates/engine.js +54 -0
  143. package/dist/ui/logger.d.ts +5 -0
  144. package/dist/ui/logger.js +53 -0
  145. package/dist/ui/prompt.d.ts +9 -0
  146. package/dist/ui/prompt.js +24 -0
  147. package/package.json +69 -0
  148. package/templates/init/README.md.tmpl +37 -0
  149. package/templates/init/_gitignore.tmpl +23 -0
  150. package/templates/init/default.project.json.tmpl +57 -0
  151. package/templates/init/package.json.tmpl +12 -0
  152. package/templates/init/rokit.toml.tmpl +5 -0
  153. package/templates/init/src/client/controllers/ExampleController.ts.tmpl +12 -0
  154. package/templates/init/src/client/runtime.client.ts.tmpl +6 -0
  155. package/templates/init/src/server/runtime.server.ts.tmpl +12 -0
  156. package/templates/init/src/server/services/ExampleService.ts.tmpl +12 -0
  157. package/templates/init/src/shared/_gitkeep.tmpl +0 -0
  158. package/templates/init/tsconfig.json.tmpl +28 -0
  159. package/templates/integrations/data-replication/DataReplicationService.ts.tmpl +33 -0
  160. package/templates/integrations/data-replication/PlayerDataController.ts.tmpl +40 -0
  161. package/templates/integrations/data-replication/dataEvents.ts.tmpl +21 -0
  162. package/templates/make/component.ts.tmpl +8 -0
  163. package/templates/make/controller.ts.tmpl +6 -0
  164. package/templates/make/event-handler.ts.tmpl +22 -0
  165. package/templates/make/screen.tsx.tmpl +35 -0
  166. package/templates/make/service.ts.tmpl +6 -0
  167. package/templates/make/stat-service.ts.tmpl +26 -0
  168. package/templates/make/ui-button.tsx.tmpl +22 -0
  169. package/templates/make/ui-image.tsx.tmpl +11 -0
  170. package/templates/make/ui-label.tsx.tmpl +18 -0
  171. package/templates/make/ui-panel.tsx.tmpl +33 -0
  172. package/templates/make/ui-story.tsx.tmpl +13 -0
  173. package/templates/modules/format/prettierignore.tmpl +9 -0
  174. package/templates/modules/format/prettierrc.json.tmpl +4 -0
  175. package/templates/modules/leaderstats/LeaderstatsService.ts.tmpl +69 -0
  176. package/templates/modules/lint/eslint.config.mjs.tmpl +17 -0
  177. package/templates/modules/networking/client-network.ts.tmpl +9 -0
  178. package/templates/modules/networking/networking.ts.tmpl +22 -0
  179. package/templates/modules/networking/rate-limit.ts.tmpl +52 -0
  180. package/templates/modules/networking/server-network.ts.tmpl +20 -0
  181. package/templates/modules/player-data/PlayerData.ts.tmpl +15 -0
  182. package/templates/modules/player-data/PlayerDataService.ts.tmpl +127 -0
  183. package/templates/modules/ui/App.tsx.tmpl +19 -0
  184. package/templates/modules/ui/Button.story.tsx.tmpl +13 -0
  185. package/templates/modules/ui/Button.tsx.tmpl +21 -0
  186. package/templates/modules/ui/UiController.tsx.tmpl +21 -0
  187. package/templates/modules/ui/screens/HomeScreen.tsx.tmpl +20 -0
@@ -0,0 +1,50 @@
1
+ import pc from "picocolors";
2
+ import { RoworkError } from "../cli/errors.js";
3
+ import { runningDashboard } from "../core/background.js";
4
+ import { openBrowser } from "../dashboard/open.js";
5
+ import { startDashboard } from "../dashboard/server.js";
6
+ import { defineCommand } from "../plugins/api.js";
7
+ import { requireProject } from "./make.js";
8
+ export const dashboardCommand = defineCommand({
9
+ name: "dashboard",
10
+ description: "Open a local web dashboard for the project: state, modules and the live output of `dev`.",
11
+ options: [
12
+ { flags: "--port <port>", description: "port to listen on (default: any free one)" },
13
+ { flags: "--no-open", description: "print the address without opening a browser" },
14
+ ],
15
+ async run(context) {
16
+ const { root } = requireProject(context, "dashboard");
17
+ // `rowork dev` already runs one: open that instead of starting a second server.
18
+ const existing = runningDashboard(root);
19
+ if (existing !== undefined) {
20
+ context.logger.success("The dashboard is already running (started by `rowork dev`).");
21
+ context.logger.info(` ${pc.bold(existing.url)}`);
22
+ if (context.options["open"] !== false)
23
+ openBrowser(existing.url);
24
+ return;
25
+ }
26
+ const rawPort = context.options["port"];
27
+ const port = typeof rawPort === "string" ? Number(rawPort) : 0;
28
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
29
+ throw new RoworkError(`\`${String(rawPort)}\` is not a valid port.`, { hint: "Use a number between 0 and 65535." });
30
+ }
31
+ const dashboard = await startDashboard({ projectRoot: root, roworkVersion: context.roworkVersion, port }).catch((error) => {
32
+ throw new RoworkError("Could not start the dashboard.", {
33
+ hint: error instanceof Error ? error.message : String(error),
34
+ });
35
+ });
36
+ context.logger.success("Dashboard running. Only this computer can reach it.");
37
+ context.logger.info(` ${pc.bold(dashboard.url)}`);
38
+ context.logger.info(pc.dim(" The address carries a secret token: do not share it. Press Ctrl+C to stop."));
39
+ if (context.options["open"] !== false)
40
+ openBrowser(dashboard.url);
41
+ await new Promise((resolve) => {
42
+ const stop = () => resolve();
43
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK"])
44
+ process.once(signal, stop);
45
+ });
46
+ await dashboard.close();
47
+ context.logger.info("Dashboard stopped.");
48
+ },
49
+ });
50
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1,3 @@
1
+ export declare const devStopCommand: import("../plugins/api.js").CommandDefinition;
2
+ export declare const devLogsCommand: import("../plugins/api.js").CommandDefinition;
3
+ //# sourceMappingURL=dev-background.d.ts.map
@@ -0,0 +1,87 @@
1
+ import { existsSync, openSync, readSync, closeSync, statSync } from "node:fs";
2
+ import pc from "picocolors";
3
+ import { RoworkError } from "../cli/errors.js";
4
+ import { clearDashboardRecord, clearRecord, logFile, runningRecord, stopProcess, tailLog } from "../core/background.js";
5
+ import { defineCommand } from "../plugins/api.js";
6
+ function requireRoot(root, command) {
7
+ if (root === undefined) {
8
+ throw new RoworkError(`\`rowork ${command}\` must run inside a Rowork project.`, {
9
+ hint: "No rowork.json found here or in any parent directory.",
10
+ });
11
+ }
12
+ return root;
13
+ }
14
+ export const devStopCommand = defineCommand({
15
+ name: "dev:stop",
16
+ description: "Stop the `rowork dev` running in the background.",
17
+ async run(context) {
18
+ const root = requireRoot(context.projectRoot, "dev:stop");
19
+ const record = runningRecord(root);
20
+ if (record === undefined) {
21
+ context.logger.info("No `rowork dev` is running in the background.");
22
+ return;
23
+ }
24
+ context.logger.step(`stopping rowork dev (pid ${record.pid})`);
25
+ const clean = await stopProcess(record.pid);
26
+ clearRecord(root);
27
+ clearDashboardRecord(root);
28
+ if (clean) {
29
+ context.logger.success("Stopped.");
30
+ }
31
+ else {
32
+ context.logger.warn(`It did not stop in time and was killed. If port ${record.port} is still busy, look for a leftover rojo process.`);
33
+ }
34
+ },
35
+ });
36
+ export const devLogsCommand = defineCommand({
37
+ name: "dev:logs",
38
+ description: "Show the output of the background `rowork dev` (compiler errors included).",
39
+ options: [
40
+ { flags: "-n, --lines <count>", description: "how many recent lines to show (default 40)" },
41
+ { flags: "-f, --follow", description: "keep printing new output until Ctrl+C" },
42
+ ],
43
+ async run(context) {
44
+ const root = requireRoot(context.projectRoot, "dev:logs");
45
+ const file = logFile(root);
46
+ if (!existsSync(file)) {
47
+ throw new RoworkError("There is no background log yet.", {
48
+ hint: "Start one with `rowork dev -d`.",
49
+ });
50
+ }
51
+ const rawLines = context.options["lines"];
52
+ const count = typeof rawLines === "string" ? Number(rawLines) : 40;
53
+ if (!Number.isInteger(count) || count < 1) {
54
+ throw new RoworkError(`\`${String(rawLines)}\` is not a valid line count.`);
55
+ }
56
+ for (const line of tailLog(root, count))
57
+ process.stdout.write(`${line}\n`);
58
+ if (context.options["follow"] !== true) {
59
+ if (runningRecord(root) === undefined) {
60
+ context.logger.info(pc.dim("(rowork dev is not running in the background)"));
61
+ }
62
+ return;
63
+ }
64
+ // Follow: print what is appended, until Ctrl+C.
65
+ let offset = statSync(file).size;
66
+ await new Promise((resolve) => {
67
+ const timer = setInterval(() => {
68
+ const size = statSync(file).size;
69
+ if (size < offset)
70
+ offset = 0; // the log was restarted
71
+ if (size === offset)
72
+ return;
73
+ const buffer = Buffer.alloc(size - offset);
74
+ const fd = openSync(file, "r");
75
+ readSync(fd, buffer, 0, buffer.length, offset);
76
+ closeSync(fd);
77
+ offset = size;
78
+ process.stdout.write(buffer);
79
+ }, 300);
80
+ process.once("SIGINT", () => {
81
+ clearInterval(timer);
82
+ resolve();
83
+ });
84
+ });
85
+ },
86
+ });
87
+ //# sourceMappingURL=dev-background.js.map
@@ -0,0 +1,2 @@
1
+ export declare const devCommand: import("../plugins/api.js").CommandDefinition;
2
+ //# sourceMappingURL=dev.d.ts.map
@@ -0,0 +1,270 @@
1
+ import { createWriteStream, existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import pc from "picocolors";
4
+ import { RoworkError } from "../cli/errors.js";
5
+ import { resolveProjectPath } from "../core/config.js";
6
+ import { run as runBinary } from "../core/exec.js";
7
+ import { clearDashboardRecord, clearRecord, DAEMON_ENV, logFile, isAlive, runDirectory, runningDashboard, runningRecord, spawnBackground, tailLog, writeDashboardRecord, writeRecord, } from "../core/background.js";
8
+ import { openBrowser } from "../dashboard/open.js";
9
+ import { startDashboard } from "../dashboard/server.js";
10
+ import { unmappedScopes } from "../core/rojo-edit.js";
11
+ import { findStoreRojoPlugin, needsStudioSetup, warnAboutStorePlugin } from "../core/studio.js";
12
+ import { readRojoPin, rojoIsAtLeast } from "../core/versions.js";
13
+ import { findMissing, isPortFree, pathWithLocalBinaries, installAdvice } from "../core/toolchain.js";
14
+ import { defineCommand } from "../plugins/api.js";
15
+ import { Supervisor } from "../process/supervisor.js";
16
+ export const devCommand = defineCommand({
17
+ name: "dev",
18
+ description: "Run the compiler, the Rojo server and the sourcemap watcher together.",
19
+ options: [
20
+ { flags: "--no-compile", description: "skip the roblox-ts compiler" },
21
+ { flags: "--no-rojo", description: "skip the Rojo server" },
22
+ { flags: "--no-sourcemap", description: "skip the sourcemap watcher" },
23
+ { flags: "--port <port>", description: "port for the Rojo server" },
24
+ { flags: "-d, --detach", description: "run in the background; stop with `rowork dev:stop`" },
25
+ { flags: "--no-dashboard", description: "do not start the local web dashboard" },
26
+ { flags: "--open", description: "open the dashboard in your browser" },
27
+ ],
28
+ async run(context) {
29
+ const { projectRoot, config } = context;
30
+ if (projectRoot === undefined || config === undefined) {
31
+ throw new RoworkError("`rowork dev` must run inside a Rowork project.", {
32
+ hint: "No rowork.json found here or in any parent directory. Create one with `rowork init <name>`.",
33
+ });
34
+ }
35
+ const isDaemon = process.env[DAEMON_ENV] !== undefined;
36
+ const already = isDaemon ? undefined : runningRecord(projectRoot);
37
+ if (already !== undefined) {
38
+ throw new RoworkError(`\`rowork dev\` is already running for this project (pid ${already.pid}).`, {
39
+ hint: "Stop it with `rowork dev:stop`, or read what it prints with `rowork dev:logs`.",
40
+ });
41
+ }
42
+ const rojoProject = resolveProjectPath(projectRoot, config.paths.rojoProject);
43
+ if (!existsSync(rojoProject)) {
44
+ throw new RoworkError(`Rojo project file not found: ${config.paths.rojoProject}`, {
45
+ hint: "Check `paths.rojoProject` in rowork.json.",
46
+ });
47
+ }
48
+ if (!existsSync(join(projectRoot, "node_modules"))) {
49
+ throw new RoworkError("Dependencies are not installed.", {
50
+ hint: "Run `npm install` first.",
51
+ });
52
+ }
53
+ const tasks = [];
54
+ if (context.options["compile"] !== false) {
55
+ tasks.push({ name: "compile", command: "rbxtsc", args: ["-w"], paint: pc.cyan });
56
+ }
57
+ if (context.options["rojo"] !== false) {
58
+ const args = ["serve", config.paths.rojoProject];
59
+ const port = context.options["port"];
60
+ if (typeof port === "string")
61
+ args.push("--port", port);
62
+ tasks.push({ name: "rojo", command: "rojo", args, paint: pc.magenta });
63
+ }
64
+ if (context.options["sourcemap"] !== false) {
65
+ // The Flamework transformer resolves instance paths through the Rojo
66
+ // project, and roblox-ts tooling reads sourcemap.json. Keeping it
67
+ // regenerated automatically removes a whole class of "works on my
68
+ // machine" path errors.
69
+ tasks.push({
70
+ name: "sourcemap",
71
+ command: "rojo",
72
+ args: ["sourcemap", config.paths.rojoProject, "--output", "sourcemap.json", "--watch"],
73
+ paint: pc.blue,
74
+ });
75
+ }
76
+ // Fail before anything starts, with the fix, instead of letting each task
77
+ // die separately with a bare `spawn ENOENT` buried in the unified log.
78
+ const required = [];
79
+ if (context.options["compile"] !== false)
80
+ required.push({ command: "rbxtsc", source: "npm" });
81
+ if (context.options["rojo"] !== false || context.options["sourcemap"] !== false) {
82
+ required.push({ command: "rojo", source: "rokit" });
83
+ }
84
+ const missing = findMissing(required, projectRoot);
85
+ if (missing.length > 0) {
86
+ throw new RoworkError(`Missing tool${missing.length > 1 ? "s" : ""}: ${missing.map((tool) => tool.command).join(", ")}.`, { hint: installAdvice(missing) });
87
+ }
88
+ // A package folder that is installed but not mapped in the Rojo project never reaches the game.
89
+ // Studio only says "Infinite yield possible", and a React interface silently never starts.
90
+ const rojoFile = context.config?.paths.rojoProject ?? "default.project.json";
91
+ try {
92
+ const unmapped = unmappedScopes(readFileSync(join(projectRoot, rojoFile), "utf8"), projectRoot);
93
+ for (const scope of unmapped) {
94
+ context.logger.warn(`${rojoFile} does not map ${scope}, so the game will not contain it (Studio waits for it forever, and a React interface never appears).`);
95
+ context.logger.info(` Add next to "@rbxts", under "rbxts_include" > "node_modules": "${scope}": { "$path": "node_modules/${scope}" }`);
96
+ }
97
+ }
98
+ catch {
99
+ // A project file we cannot read is Rojo's error to report, not this check's.
100
+ }
101
+ // Rojo's own report of a busy port is a 15-line "Rojo crashed" that also
102
+ // takes the compiler down. The usual cause is a previous `rowork dev` that
103
+ // is still running, so say so, before anything starts.
104
+ if (context.options["rojo"] !== false) {
105
+ const rawPort = context.options["port"];
106
+ const port = typeof rawPort === "string" ? Number(rawPort) : 34872;
107
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
108
+ throw new RoworkError(`\`${String(rawPort)}\` is not a valid port.`, {
109
+ hint: "Use a number between 1 and 65535, e.g. --port 34873",
110
+ });
111
+ }
112
+ if (!(await isPortFree(port))) {
113
+ throw new RoworkError(`Port ${port} is already in use.`, {
114
+ hint: [
115
+ "Another `rowork dev` (or Rojo) is probably still running, for example in a terminal you closed.",
116
+ `Find it: ${process.platform === "win32" ? `netstat -ano | findstr :${port}` : `ss -ltnp | grep ${port} (or: lsof -i :${port})`}`,
117
+ `Or use another port: rowork dev --port ${port + 1}`,
118
+ ].join("\n "),
119
+ });
120
+ }
121
+ }
122
+ // The Creator Store plugin lags behind Rojo's releases: with Rojo 7.7 or newer it fails
123
+ // with a misleading "Can't parse JSON". Say so before the user loses time on it.
124
+ if (needsStudioSetup() && rojoIsAtLeast(readRojoPin(projectRoot), 7, 7) && findStoreRojoPlugin() !== undefined) {
125
+ warnAboutStorePlugin(context.logger);
126
+ }
127
+ await ensureInitialBuild(context, projectRoot, config.paths.out);
128
+ if (tasks.length === 0) {
129
+ throw new RoworkError("Every task was disabled, nothing left to run.");
130
+ }
131
+ if (context.options["detach"] === true && !isDaemon) {
132
+ await startInBackground(context, projectRoot);
133
+ return;
134
+ }
135
+ context.logger.info(`${pc.bold(config.name)} ${pc.dim(projectRoot)}`);
136
+ for (const task of tasks) {
137
+ context.logger.step(`${task.paint(task.name)} ${pc.dim(`${task.command} ${task.args.join(" ")}`)}`);
138
+ }
139
+ context.logger.info(pc.dim("Press Ctrl+C to stop everything."));
140
+ context.logger.blank();
141
+ // The output goes to a file too, so the dashboard and `dev:logs` see what a terminal sees.
142
+ // (Detached, the process's own output already IS that file.)
143
+ let logStream;
144
+ if (!isDaemon) {
145
+ mkdirSync(runDirectory(projectRoot), { recursive: true });
146
+ logStream = createWriteStream(logFile(projectRoot), { flags: "w" });
147
+ writeRecord(projectRoot, {
148
+ pid: process.pid,
149
+ startedAt: new Date().toISOString(),
150
+ port: typeof context.options["port"] === "string" ? Number(context.options["port"]) : 34872,
151
+ mode: "foreground",
152
+ });
153
+ }
154
+ let dashboard;
155
+ if (context.options["dashboard"] !== false) {
156
+ try {
157
+ dashboard = await startDashboard({ projectRoot, roworkVersion: context.roworkVersion, port: 0 });
158
+ writeDashboardRecord(projectRoot, { pid: process.pid, port: dashboard.port, url: dashboard.url });
159
+ context.logger.info(`Dashboard: ${pc.bold(dashboard.url)}`);
160
+ context.logger.info(pc.dim(" Only this computer can reach it. Do not share the address: it carries a secret token."));
161
+ if (context.options["open"] === true && !isDaemon)
162
+ openBrowser(dashboard.url);
163
+ }
164
+ catch (error) {
165
+ // The dashboard is a convenience: never let it stop the game from being built.
166
+ context.logger.warn(`The dashboard did not start: ${error instanceof Error ? error.message : String(error)}`);
167
+ }
168
+ }
169
+ // However this ends, leave no record of a process that is gone.
170
+ process.once("exit", () => {
171
+ clearRecord(projectRoot);
172
+ clearDashboardRecord(projectRoot);
173
+ });
174
+ const supervisor = new Supervisor({
175
+ tasks,
176
+ cwd: projectRoot,
177
+ logger: context.logger,
178
+ ...(logStream === undefined ? {} : { onOutput: (text) => logStream?.write(text) }),
179
+ });
180
+ const code = await supervisor.run();
181
+ await dashboard?.close();
182
+ logStream?.end();
183
+ clearRecord(projectRoot);
184
+ clearDashboardRecord(projectRoot);
185
+ if (code !== 0)
186
+ process.exitCode = code;
187
+ },
188
+ });
189
+ /**
190
+ * Rojo refuses to start when a `$path` in the project file does not exist yet,
191
+ * and on a fresh clone `out/` and `include/` only appear once roblox-ts has
192
+ * compiled. Starting all tasks at once therefore kills Rojo instantly and takes
193
+ * everything else down with it. One blocking build first removes that race.
194
+ */
195
+ async function ensureInitialBuild(context, projectRoot, outDirectory) {
196
+ if (context.options["compile"] === false)
197
+ return;
198
+ if (existsSync(join(projectRoot, outDirectory)) && existsSync(join(projectRoot, "include")))
199
+ return;
200
+ context.logger.info("First run: compiling once so Rojo has something to serve...");
201
+ try {
202
+ await runBinary("rbxtsc", [], {
203
+ cwd: projectRoot,
204
+ env: { PATH: pathWithLocalBinaries(projectRoot) },
205
+ });
206
+ }
207
+ catch (cause) {
208
+ throw new RoworkError("The initial build failed.", {
209
+ hint: "Fix the compiler errors above, then run `rowork dev` again.",
210
+ cause,
211
+ });
212
+ }
213
+ context.logger.blank();
214
+ }
215
+ /**
216
+ * Relaunches `rowork dev` detached from this terminal.
217
+ *
218
+ * Every check and the first build already ran in the foreground, so their
219
+ * errors are visible here instead of ending up in a log nobody is reading.
220
+ */
221
+ async function startInBackground(context, projectRoot) {
222
+ const script = process.argv[1];
223
+ if (script === undefined)
224
+ throw new RoworkError("Cannot find the Rowork entry point to relaunch.");
225
+ const args = [];
226
+ for (const flag of ["compile", "rojo", "sourcemap", "dashboard"]) {
227
+ if (context.options[flag] === false)
228
+ args.push(`--no-${flag}`);
229
+ }
230
+ const port = context.options["port"];
231
+ if (typeof port === "string")
232
+ args.push("--port", port);
233
+ const pid = spawnBackground(projectRoot, script, args);
234
+ writeRecord(projectRoot, {
235
+ pid,
236
+ startedAt: new Date().toISOString(),
237
+ port: typeof port === "string" ? Number(port) : 34872,
238
+ mode: "background",
239
+ });
240
+ // A task that cannot start dies within a second or two: wait for that, so a
241
+ // failure is reported now with its cause rather than discovered later.
242
+ for (let waited = 0; waited < 4000; waited += 200) {
243
+ await new Promise((resolve) => setTimeout(resolve, 200));
244
+ if (!isAlive(pid)) {
245
+ clearRecord(projectRoot);
246
+ for (const line of tailLog(projectRoot, 15))
247
+ context.logger.info(pc.dim(` ${line}`));
248
+ throw new RoworkError("`rowork dev` stopped right after starting in the background.", {
249
+ hint: `Full output: ${logFile(projectRoot)}`,
250
+ });
251
+ }
252
+ }
253
+ context.logger.success(`rowork dev is running in the background (pid ${pid}).`);
254
+ context.logger.info(` Output: rowork dev:logs (add -f to follow)`);
255
+ context.logger.info(` Stop it: rowork dev:stop`);
256
+ // The background process starts its dashboard a moment after itself.
257
+ if (context.options["dashboard"] !== false) {
258
+ for (let waited = 0; waited < 3000; waited += 200) {
259
+ const running = runningDashboard(projectRoot);
260
+ if (running !== undefined) {
261
+ context.logger.info(` Dashboard: ${pc.bold(running.url)}`);
262
+ if (context.options["open"] === true)
263
+ openBrowser(running.url);
264
+ break;
265
+ }
266
+ await new Promise((resolve) => setTimeout(resolve, 200));
267
+ }
268
+ }
269
+ }
270
+ //# sourceMappingURL=dev.js.map
@@ -0,0 +1,2 @@
1
+ export declare const ejectCommand: import("../plugins/api.js").CommandDefinition;
2
+ //# sourceMappingURL=eject.d.ts.map
@@ -0,0 +1,136 @@
1
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import pc from "picocolors";
4
+ import { RoworkError } from "../cli/errors.js";
5
+ import { runningRecord, stopProcess, clearRecord } from "../core/background.js";
6
+ import { removeAgentDocs, AGENTS_FILE } from "../core/agent-docs.js";
7
+ import { CONFIG_FILENAME } from "../core/config.js";
8
+ import { run as runBinary } from "../core/exec.js";
9
+ import { isOnPlainPath } from "../core/toolchain.js";
10
+ import { defineCommand } from "../plugins/api.js";
11
+ import { answered, isInteractive, prompts } from "../ui/prompt.js";
12
+ import { requireProject } from "./make.js";
13
+ /** What `rowork dev` runs, written as plain npm scripts. */
14
+ function replacementScripts(rojoProject) {
15
+ return {
16
+ // npm runs `predev` first: the same "compile once so Rojo has something to
17
+ // serve" that `rowork dev` does before starting the watchers.
18
+ predev: "rbxtsc",
19
+ dev: [
20
+ "concurrently -k -n compile,rojo,sourcemap",
21
+ '"rbxtsc -w"',
22
+ `"rojo serve ${rojoProject}"`,
23
+ `"rojo sourcemap ${rojoProject} --output sourcemap.json --watch"`,
24
+ ].join(" "),
25
+ };
26
+ }
27
+ function indentOf(source) {
28
+ return /^\t/m.test(source) ? "\t" : 2;
29
+ }
30
+ export const ejectCommand = defineCommand({
31
+ name: "eject",
32
+ guided: true,
33
+ description: "Leave Rowork: keep a project that runs with plain tools, and remove Rowork's files.",
34
+ options: [
35
+ { flags: "--yes", description: "do not ask for confirmation" },
36
+ { flags: "--dry-run", description: "show what would change, and change nothing" },
37
+ { flags: "--no-install", description: "do not install `concurrently` (the dev script needs it)" },
38
+ ],
39
+ async run(context) {
40
+ const { root, config } = requireProject(context, "eject");
41
+ const manifestPath = join(root, "package.json");
42
+ if (!existsSync(manifestPath)) {
43
+ throw new RoworkError("No package.json to update.", {
44
+ hint: "Eject rewrites the `dev` script of package.json.",
45
+ });
46
+ }
47
+ const manifestSource = readFileSync(manifestPath, "utf8");
48
+ const manifest = JSON.parse(manifestSource);
49
+ const scripts = manifest.scripts ?? {};
50
+ const wanted = replacementScripts(config.paths.rojoProject);
51
+ // Only replace a `dev` script that is Rowork's own. A script the user wrote
52
+ // or edited is theirs, and eject must not overwrite it.
53
+ const devIsRowork = scripts["dev"] === undefined || /^rowork(\s|$)/.test(scripts["dev"]);
54
+ const changes = [];
55
+ if (devIsRowork) {
56
+ changes.push('package.json: `dev` becomes "concurrently" running rbxtsc -w, rojo serve and rojo sourcemap');
57
+ changes.push("package.json: adds `predev` (one build first, so Rojo can start)");
58
+ changes.push("package.json: adds `concurrently` to devDependencies");
59
+ }
60
+ else {
61
+ changes.push(`package.json: keeps your own \`dev\` script ("${scripts["dev"]}")`);
62
+ }
63
+ const readmePath = join(root, "README.md");
64
+ const readme = existsSync(readmePath) ? readFileSync(readmePath, "utf8") : "";
65
+ const readmeChanges = /rowork dev/.test(readme);
66
+ if (readmeChanges)
67
+ changes.push("README.md: `rowork dev` becomes `npm run dev`");
68
+ if (existsSync(join(root, AGENTS_FILE)))
69
+ changes.push(`${AGENTS_FILE}: Rowork's generated block removed (your own notes stay)`);
70
+ changes.push(`${CONFIG_FILENAME}: removed`);
71
+ if (existsSync(join(root, ".rowork")))
72
+ changes.push(".rowork/: removed (background dev pid and log)");
73
+ const record = runningRecord(root);
74
+ if (record !== undefined)
75
+ changes.push(`stops the background rowork dev (pid ${record.pid})`);
76
+ context.logger.info(pc.bold("Ejecting will:"));
77
+ for (const change of changes)
78
+ context.logger.step(change);
79
+ context.logger.blank();
80
+ context.logger.info(pc.dim("Your source code, default.project.json, tsconfig.json and rokit.toml are not touched."));
81
+ context.logger.info(pc.dim("Generated code never imports Rowork, so it keeps working as is."));
82
+ context.logger.blank();
83
+ if (context.options["dryRun"] === true) {
84
+ context.logger.info("Dry run: nothing was changed.");
85
+ return;
86
+ }
87
+ if (context.options["yes"] !== true) {
88
+ if (!isInteractive()) {
89
+ throw new RoworkError("`rowork eject` asks for confirmation.", {
90
+ hint: "Outside a terminal, add --yes (and --dry-run first to preview).",
91
+ });
92
+ }
93
+ const confirmed = answered(await prompts.confirm({
94
+ message: "Leave Rowork? `rowork` commands stop working in this project.",
95
+ initialValue: false,
96
+ }));
97
+ if (!confirmed) {
98
+ prompts.cancel("Nothing changed.");
99
+ return;
100
+ }
101
+ }
102
+ if (record !== undefined) {
103
+ await stopProcess(record.pid);
104
+ clearRecord(root);
105
+ }
106
+ if (devIsRowork) {
107
+ manifest.scripts = { ...scripts, ...wanted };
108
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, undefined, indentOf(manifestSource))}\n`, "utf8");
109
+ if (context.options["install"] !== false) {
110
+ context.logger.step("installing concurrently (npm)");
111
+ context.logger.blank();
112
+ await runBinary("npm", ["install", "--save-dev", "concurrently"], { cwd: root });
113
+ }
114
+ else {
115
+ context.logger.warn("Skipped installing `concurrently`: run `npm install --save-dev concurrently` before `npm run dev`.");
116
+ }
117
+ }
118
+ if (readmeChanges)
119
+ writeFileSync(readmePath, readme.replace(/rowork dev/g, "npm run dev"), "utf8");
120
+ removeAgentDocs(root);
121
+ rmSync(join(root, ".rowork"), { recursive: true, force: true });
122
+ rmSync(join(root, CONFIG_FILENAME), { force: true });
123
+ context.logger.blank();
124
+ context.logger.success("Ejected. This project no longer needs Rowork.");
125
+ // Rowork also looked in ~/.rokit/bin on its own. Plain npm scripts will not.
126
+ if (!isOnPlainPath("rojo")) {
127
+ context.logger.blank();
128
+ context.logger.warn("`rojo` is not on your PATH, so `npm run dev` will not find it.");
129
+ context.logger.info("Add Rokit's directory to your PATH (~/.rokit/bin, or %USERPROFILE%\\.rokit\\bin on Windows) and reopen the terminal.");
130
+ }
131
+ context.logger.blank();
132
+ context.logger.info("Run it with: npm run dev");
133
+ context.logger.info("Build once: npm run build");
134
+ },
135
+ });
136
+ //# sourceMappingURL=eject.js.map
@@ -0,0 +1,8 @@
1
+ import type { CommandDefinition } from "../plugins/api.js";
2
+ /**
3
+ * Core commands, listed explicitly rather than discovered by glob: the compiler
4
+ * checks them, startup stays fast, and no phantom command can slip into the
5
+ * published package.
6
+ */
7
+ export declare const coreCommands: CommandDefinition[];
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,54 @@
1
+ import { coreModules } from "../modules/index.js";
2
+ import { addCommand, moduleCommand } from "./add.js";
3
+ import { assetsCommand } from "./assets.js";
4
+ import { assetsSetupCommand } from "./assets-setup.js";
5
+ import { consoleCommand } from "./console.js";
6
+ import { agentsSyncCommand, infoCommand } from "./info.js";
7
+ import { updateCommand } from "./update.js";
8
+ import { wireCommand } from "./wire.js";
9
+ import { dashboardCommand } from "./dashboard.js";
10
+ import { devCommand } from "./dev.js";
11
+ import { ejectCommand } from "./eject.js";
12
+ import { devLogsCommand, devStopCommand } from "./dev-background.js";
13
+ import { initCommand } from "./init.js";
14
+ import { makeCommand } from "./make-menu.js";
15
+ import { makeEventCommand } from "./make-event.js";
16
+ import { makeStatCommand } from "./make-stat.js";
17
+ import { makeScreenCommand, makeUiCommand } from "./make-ui.js";
18
+ import { makeComponentCommand, makeControllerCommand, makeServiceCommand } from "./make.js";
19
+ import { startCommand } from "./start.js";
20
+ import { studioCommand, studioSetupCommand } from "./studio.js";
21
+ /**
22
+ * Core commands, listed explicitly rather than discovered by glob: the compiler
23
+ * checks them, startup stays fast, and no phantom command can slip into the
24
+ * published package.
25
+ */
26
+ export const coreCommands = [
27
+ startCommand,
28
+ initCommand,
29
+ devCommand,
30
+ devStopCommand,
31
+ dashboardCommand,
32
+ assetsCommand,
33
+ assetsSetupCommand,
34
+ devLogsCommand,
35
+ makeCommand,
36
+ makeServiceCommand,
37
+ makeControllerCommand,
38
+ makeComponentCommand,
39
+ makeStatCommand,
40
+ makeEventCommand,
41
+ makeScreenCommand,
42
+ makeUiCommand,
43
+ studioCommand,
44
+ studioSetupCommand,
45
+ consoleCommand,
46
+ addCommand,
47
+ ejectCommand,
48
+ infoCommand,
49
+ updateCommand,
50
+ wireCommand,
51
+ agentsSyncCommand,
52
+ ...coreModules.map(moduleCommand),
53
+ ];
54
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ export declare const infoCommand: import("../plugins/api.js").CommandDefinition;
2
+ export declare const agentsSyncCommand: import("../plugins/api.js").CommandDefinition;
3
+ //# sourceMappingURL=info.d.ts.map