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,149 @@
1
+ import { spawn as spawnDetached, spawnSync } from "node:child_process";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ /** Set in the background process, so it knows to clean up after itself. */
5
+ export const DAEMON_ENV = "ROWORK_DEV_DAEMON";
6
+ export function runDirectory(projectRoot) {
7
+ return join(projectRoot, ".rowork", "run");
8
+ }
9
+ const pidFile = (root) => join(runDirectory(root), "dev.pid");
10
+ export const logFile = (root) => join(runDirectory(root), "dev.log");
11
+ export function isAlive(pid) {
12
+ try {
13
+ process.kill(pid, 0);
14
+ return true;
15
+ }
16
+ catch (error) {
17
+ // EPERM means the process exists but belongs to someone else.
18
+ return error.code === "EPERM";
19
+ }
20
+ }
21
+ /**
22
+ * Guards against a recycled PID: after a reboot, an old pid file could name an
23
+ * unrelated process, and `dev:stop` must never signal that. Where `ps` exists
24
+ * the command line must mention rowork; Windows has no cheap equivalent, so
25
+ * there the liveness check alone applies.
26
+ */
27
+ function looksLikeRowork(pid) {
28
+ if (process.platform === "win32")
29
+ return true;
30
+ const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
31
+ if (result.status !== 0)
32
+ return true;
33
+ return /rowork/i.test(result.stdout);
34
+ }
35
+ export function readRecord(projectRoot) {
36
+ try {
37
+ return JSON.parse(readFileSync(pidFile(projectRoot), "utf8"));
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ export function writeRecord(projectRoot, record) {
44
+ mkdirSync(runDirectory(projectRoot), { recursive: true });
45
+ writeFileSync(pidFile(projectRoot), `${JSON.stringify(record)}\n`, "utf8");
46
+ }
47
+ export function clearRecord(projectRoot) {
48
+ rmSync(pidFile(projectRoot), { force: true });
49
+ }
50
+ /** The background `rowork dev` of this project, if one is really running. */
51
+ export function runningRecord(projectRoot) {
52
+ const record = readRecord(projectRoot);
53
+ if (record === undefined)
54
+ return undefined;
55
+ if (!isAlive(record.pid) || !looksLikeRowork(record.pid)) {
56
+ clearRecord(projectRoot); // stale: the process is gone
57
+ return undefined;
58
+ }
59
+ return record;
60
+ }
61
+ /** Last `lines` lines of the background log. */
62
+ export function tailLog(projectRoot, lines) {
63
+ if (!existsSync(logFile(projectRoot)))
64
+ return [];
65
+ return readFileSync(logFile(projectRoot), "utf8").split("\n").filter(Boolean).slice(-lines);
66
+ }
67
+ /**
68
+ * Starts `rowork dev` detached from this terminal and returns its pid.
69
+ *
70
+ * A session and process group of its own on POSIX, so closing the terminal
71
+ * neither reaches it nor stops it; no console window on Windows.
72
+ */
73
+ export function spawnBackground(projectRoot, script, args) {
74
+ mkdirSync(runDirectory(projectRoot), { recursive: true });
75
+ const log = openSync(logFile(projectRoot), "w");
76
+ const child = spawnDetached(process.execPath, [script, "--cwd", projectRoot, "dev", ...args], {
77
+ cwd: projectRoot,
78
+ detached: true,
79
+ stdio: ["ignore", log, log],
80
+ env: { ...process.env, [DAEMON_ENV]: "1" },
81
+ windowsHide: true,
82
+ });
83
+ child.unref();
84
+ closeSync(log);
85
+ if (child.pid === undefined)
86
+ throw new Error("could not start the background process");
87
+ return child.pid;
88
+ }
89
+ /** Stops the process and everything it started. Returns false if it had to be forced. */
90
+ export async function stopProcess(pid) {
91
+ if (process.platform === "win32") {
92
+ // The tasks are children of the supervisor: taskkill /T takes the whole tree.
93
+ await new Promise((resolve) => {
94
+ const killer = spawnDetached("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
95
+ killer.on("error", () => resolve());
96
+ killer.on("close", () => resolve());
97
+ });
98
+ return true;
99
+ }
100
+ // SIGTERM lets the supervisor stop each task's process tree itself.
101
+ try {
102
+ process.kill(pid, "SIGTERM");
103
+ }
104
+ catch {
105
+ return true;
106
+ }
107
+ for (let waited = 0; waited < 10000; waited += 100) {
108
+ if (!isAlive(pid))
109
+ return true;
110
+ await new Promise((resolve) => setTimeout(resolve, 100));
111
+ }
112
+ try {
113
+ process.kill(pid, "SIGKILL");
114
+ }
115
+ catch {
116
+ // Gone in the meantime.
117
+ }
118
+ return false;
119
+ }
120
+ const dashboardFile = (root) => join(runDirectory(root), "dashboard.json");
121
+ /**
122
+ * Remembers the dashboard that is running, so `rowork dev -d` can print its address
123
+ * and `rowork dashboard` can open it instead of starting a second one.
124
+ *
125
+ * The address holds the secret token, so the file is readable by its owner only
126
+ * (mode 0600; on Windows the user profile already is private) and lives in
127
+ * `.rowork/run/`, which the generated .gitignore excludes.
128
+ */
129
+ export function writeDashboardRecord(root, record) {
130
+ mkdirSync(runDirectory(root), { recursive: true });
131
+ writeFileSync(dashboardFile(root), `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
132
+ }
133
+ export function clearDashboardRecord(root) {
134
+ rmSync(dashboardFile(root), { force: true });
135
+ }
136
+ /** The dashboard of this project, if its process is really alive. A stale record is removed. */
137
+ export function runningDashboard(root) {
138
+ try {
139
+ const record = JSON.parse(readFileSync(dashboardFile(root), "utf8"));
140
+ if (isAlive(record.pid) && looksLikeRowork(record.pid))
141
+ return record;
142
+ }
143
+ catch {
144
+ // No record, or unreadable: nothing is running as far as Rowork knows.
145
+ }
146
+ clearDashboardRecord(root);
147
+ return undefined;
148
+ }
149
+ //# sourceMappingURL=background.js.map
@@ -0,0 +1,9 @@
1
+ import { type RoworkConfig } from "../plugins/api.js";
2
+ export declare const CONFIG_FILENAME = "rowork.json";
3
+ /** Walks up the directory tree until a rowork.json is found. */
4
+ export declare function findProjectRoot(from: string): string | undefined;
5
+ export declare function loadConfig(projectRoot: string): RoworkConfig;
6
+ export declare function defaultConfig(name: string): RoworkConfig;
7
+ /** Resolves a config path relative to the project root. */
8
+ export declare function resolveProjectPath(projectRoot: string, path: string): string;
9
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, parse, resolve } from "node:path";
3
+ import { RoworkError } from "../cli/errors.js";
4
+ import { ROWORK_PLUGIN_API_VERSION } from "../plugins/api.js";
5
+ export const CONFIG_FILENAME = "rowork.json";
6
+ /** Walks up the directory tree until a rowork.json is found. */
7
+ export function findProjectRoot(from) {
8
+ let current = resolve(from);
9
+ const { root } = parse(current);
10
+ for (;;) {
11
+ if (existsSync(join(current, CONFIG_FILENAME)))
12
+ return current;
13
+ if (current === root)
14
+ return undefined;
15
+ const parent = dirname(current);
16
+ if (parent === current)
17
+ return undefined;
18
+ current = parent;
19
+ }
20
+ }
21
+ export function loadConfig(projectRoot) {
22
+ const file = join(projectRoot, CONFIG_FILENAME);
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(readFileSync(file, "utf8"));
26
+ }
27
+ catch (cause) {
28
+ throw new RoworkError(`${CONFIG_FILENAME} is unreadable or malformed.`, {
29
+ hint: `Check the JSON syntax of ${file}.`,
30
+ cause,
31
+ });
32
+ }
33
+ if (typeof parsed !== "object" || parsed === null) {
34
+ throw new RoworkError(`${CONFIG_FILENAME} must contain a JSON object.`);
35
+ }
36
+ const config = parsed;
37
+ if (config.roworkApiVersion !== ROWORK_PLUGIN_API_VERSION) {
38
+ throw new RoworkError(`This project targets Rowork API v${String(config.roworkApiVersion)}, this CLI provides v${ROWORK_PLUGIN_API_VERSION}.`, { hint: "Update Rowork, or adjust `roworkApiVersion` in rowork.json." });
39
+ }
40
+ return config;
41
+ }
42
+ export function defaultConfig(name) {
43
+ return {
44
+ name,
45
+ roworkApiVersion: ROWORK_PLUGIN_API_VERSION,
46
+ language: "roblox-ts",
47
+ paths: {
48
+ source: "src",
49
+ out: "out",
50
+ rojoProject: "default.project.json",
51
+ services: "src/server/services",
52
+ controllers: "src/client/controllers",
53
+ shared: "src/shared",
54
+ },
55
+ plugins: [],
56
+ };
57
+ }
58
+ /** Resolves a config path relative to the project root. */
59
+ export function resolveProjectPath(projectRoot, path) {
60
+ return isAbsolute(path) ? path : resolve(projectRoot, path);
61
+ }
62
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,22 @@
1
+ export interface RunOptions {
2
+ cwd: string;
3
+ /** Whether the tool's own output reaches the terminal. */
4
+ stdio?: "inherit" | "ignore";
5
+ /** Extra environment variables, merged over the current ones. */
6
+ env?: Record<string, string>;
7
+ }
8
+ /**
9
+ * Runs an external executable.
10
+ *
11
+ * Every external tool Rowork drives on Windows (`npm`, `rojo`, `rbxtsc`) is a
12
+ * `.cmd` shim, and since the fix for CVE-2024-27980 Node refuses to spawn one
13
+ * without `shell: true`, failing with EINVAL. Turning the shell on instead
14
+ * would make every argument a shell injection vector, and a hostile
15
+ * rowork.json could then run arbitrary commands on a contributor's machine.
16
+ *
17
+ * cross-spawn is the standard way out: it invokes `cmd.exe` itself and escapes
18
+ * the arguments, so no argument is ever interpreted as a command. It is also
19
+ * why `.cmd` is never appended by hand anywhere in this codebase.
20
+ */
21
+ export declare function run(command: string, args: string[], options: RunOptions): Promise<void>;
22
+ //# sourceMappingURL=exec.d.ts.map
@@ -0,0 +1,38 @@
1
+ import spawn from "cross-spawn";
2
+ import { RoworkError } from "../cli/errors.js";
3
+ /**
4
+ * Runs an external executable.
5
+ *
6
+ * Every external tool Rowork drives on Windows (`npm`, `rojo`, `rbxtsc`) is a
7
+ * `.cmd` shim, and since the fix for CVE-2024-27980 Node refuses to spawn one
8
+ * without `shell: true`, failing with EINVAL. Turning the shell on instead
9
+ * would make every argument a shell injection vector, and a hostile
10
+ * rowork.json could then run arbitrary commands on a contributor's machine.
11
+ *
12
+ * cross-spawn is the standard way out: it invokes `cmd.exe` itself and escapes
13
+ * the arguments, so no argument is ever interpreted as a command. It is also
14
+ * why `.cmd` is never appended by hand anywhere in this codebase.
15
+ */
16
+ export function run(command, args, options) {
17
+ return new Promise((resolvePromise, rejectPromise) => {
18
+ const child = spawn(command, args, {
19
+ cwd: options.cwd,
20
+ stdio: options.stdio ?? "inherit",
21
+ env: { ...process.env, ...options.env },
22
+ });
23
+ child.on("error", (cause) => {
24
+ rejectPromise(new RoworkError(`Could not run \`${command}\`.`, {
25
+ hint: `Make sure \`${command}\` is installed and available on your PATH.`,
26
+ cause,
27
+ }));
28
+ });
29
+ child.on("close", (code) => {
30
+ if (code === 0) {
31
+ resolvePromise();
32
+ return;
33
+ }
34
+ rejectPromise(new RoworkError(`\`${command} ${args.join(" ")}\` exited with code ${String(code)}.`));
35
+ });
36
+ });
37
+ }
38
+ //# sourceMappingURL=exec.js.map
@@ -0,0 +1,12 @@
1
+ import type { Logger } from "../plugins/api.js";
2
+ /**
3
+ * Runs the project's own Prettier over files Rowork just wrote or edited.
4
+ *
5
+ * A generator cannot know where a formatter will wrap a line: a long name pushes an import
6
+ * or a tag past the line width and the project's `format:check` then fails on code the
7
+ * user never touched. Asking the formatter itself is the only answer that stays right when
8
+ * its settings change. Nothing to do (and nothing said) when the project has no Prettier:
9
+ * the code is still correct, only laid out a little differently.
10
+ */
11
+ export declare function formatGenerated(projectRoot: string, files: string[], logger: Logger): Promise<void>;
12
+ //# sourceMappingURL=format-generated.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { RoworkError } from "../cli/errors.js";
2
+ import { run } from "./exec.js";
3
+ import { findExecutable, pathWithLocalBinaries } from "./toolchain.js";
4
+ /**
5
+ * Runs the project's own Prettier over files Rowork just wrote or edited.
6
+ *
7
+ * A generator cannot know where a formatter will wrap a line: a long name pushes an import
8
+ * or a tag past the line width and the project's `format:check` then fails on code the
9
+ * user never touched. Asking the formatter itself is the only answer that stays right when
10
+ * its settings change. Nothing to do (and nothing said) when the project has no Prettier:
11
+ * the code is still correct, only laid out a little differently.
12
+ */
13
+ export async function formatGenerated(projectRoot, files, logger) {
14
+ if (files.length === 0 || findExecutable("prettier", projectRoot) === undefined)
15
+ return;
16
+ try {
17
+ await run("prettier", ["--write", ...files], {
18
+ cwd: projectRoot,
19
+ stdio: "ignore",
20
+ env: { PATH: pathWithLocalBinaries(projectRoot) },
21
+ });
22
+ }
23
+ catch (error) {
24
+ const detail = error instanceof RoworkError ? error.message : String(error);
25
+ logger.warn(`Could not format the new files with Prettier (${detail}). Run \`npm run format\` if your formatter complains.`);
26
+ }
27
+ }
28
+ //# sourceMappingURL=format-generated.js.map
@@ -0,0 +1,37 @@
1
+ import type { Logger } from "../plugins/api.js";
2
+ /** Flamework's own naming convention: `Inventory` -> `InventoryService`. */
3
+ export declare function withSuffix(pascalName: string, suffix: string): string;
4
+ /** Turns anything typed by the user into a class-safe PascalCase name. */
5
+ export declare function toClassBase(raw: string): string;
6
+ export interface GenerateOptions {
7
+ projectRoot: string;
8
+ /** Directory to write into, relative to the project root. */
9
+ directory: string;
10
+ fileName: string;
11
+ /** Template name under `templates/<templateRoot>/`, without the `.ts.tmpl` suffix. */
12
+ template: string;
13
+ /** Directory under `templates/` holding the template. Defaults to `make`. */
14
+ templateRoot?: string;
15
+ variables: Record<string, string>;
16
+ force: boolean;
17
+ /** What to do when the file exists and `force` is off. Defaults to failing. */
18
+ ifExists?: "fail" | "skip";
19
+ }
20
+ /**
21
+ * Writes one generated file and returns its path relative to the project, or
22
+ * undefined when it already existed and `ifExists` is "skip".
23
+ */
24
+ export declare function generateFile(options: GenerateOptions): string | undefined;
25
+ /** Relative module specifier from one project directory to a file (no extension). */
26
+ export declare function importPath(projectRoot: string, fromDirectory: string, toFile: string): string;
27
+ /**
28
+ * Makes sure a runtime entry file scans a directory.
29
+ *
30
+ * Flamework only discovers what `addPaths` lists, so a class written to a
31
+ * directory that is not listed compiles fine and then silently never runs.
32
+ * Returns true when the file was changed. The edit is a plain text insertion
33
+ * next to the existing `addPaths` calls; if the file does not look the way Rowork
34
+ * generated it, nothing is touched and the caller is told to do it by hand.
35
+ */
36
+ export declare function ensureFlameworkPath(runtimeFile: string, directory: string, logger: Logger): boolean;
37
+ //# sourceMappingURL=generate.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { RoworkError } from "../cli/errors.js";
4
+ import { renderString, templatesRoot } from "../templates/engine.js";
5
+ import { resolveProjectPath } from "./config.js";
6
+ /** Flamework's own naming convention: `Inventory` -> `InventoryService`. */
7
+ export function withSuffix(pascalName, suffix) {
8
+ return pascalName.endsWith(suffix) && pascalName.length > suffix.length
9
+ ? pascalName
10
+ : `${pascalName}${suffix}`;
11
+ }
12
+ /** Turns anything typed by the user into a class-safe PascalCase name. */
13
+ export function toClassBase(raw) {
14
+ const pascal = raw
15
+ .split(/[^A-Za-z0-9]+/)
16
+ .filter((part) => part.length > 0)
17
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
18
+ .join("");
19
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(pascal)) {
20
+ throw new RoworkError(`\`${raw}\` cannot be turned into a class name.`, {
21
+ hint: "Use letters and digits, starting with a letter: e.g. Inventory, or player-stats.",
22
+ });
23
+ }
24
+ return pascal;
25
+ }
26
+ /**
27
+ * Writes one generated file and returns its path relative to the project, or
28
+ * undefined when it already existed and `ifExists` is "skip".
29
+ */
30
+ export function generateFile(options) {
31
+ const directory = resolveProjectPath(options.projectRoot, options.directory);
32
+ const target = join(directory, options.fileName);
33
+ const shown = relative(options.projectRoot, target);
34
+ if (existsSync(target) && !options.force) {
35
+ if (options.ifExists === "skip")
36
+ return undefined;
37
+ throw new RoworkError(`${shown} already exists.`, {
38
+ hint: "Pick another name, or pass --force to overwrite it.",
39
+ });
40
+ }
41
+ // `name.ts.tmpl` by default; a template that is not TypeScript carries its own
42
+ // extension in its name (`eslint.config.mjs` is stored as `eslint.config.mjs.tmpl`).
43
+ const base = join(templatesRoot(), options.templateRoot ?? "make", options.template);
44
+ const source = readFileSync(existsSync(`${base}.ts.tmpl`) ? `${base}.ts.tmpl` : `${base}.tmpl`, "utf8");
45
+ mkdirSync(directory, { recursive: true });
46
+ writeFileSync(target, renderString(source, options.variables), "utf8");
47
+ return shown;
48
+ }
49
+ /** Relative module specifier from one project directory to a file (no extension). */
50
+ export function importPath(projectRoot, fromDirectory, toFile) {
51
+ const path = relative(resolveProjectPath(projectRoot, fromDirectory), resolveProjectPath(projectRoot, toFile))
52
+ .split("\\")
53
+ .join("/");
54
+ return path.startsWith(".") ? path : `./${path}`;
55
+ }
56
+ /**
57
+ * Makes sure a runtime entry file scans a directory.
58
+ *
59
+ * Flamework only discovers what `addPaths` lists, so a class written to a
60
+ * directory that is not listed compiles fine and then silently never runs.
61
+ * Returns true when the file was changed. The edit is a plain text insertion
62
+ * next to the existing `addPaths` calls; if the file does not look the way Rowork
63
+ * generated it, nothing is touched and the caller is told to do it by hand.
64
+ */
65
+ export function ensureFlameworkPath(runtimeFile, directory, logger) {
66
+ const posixDirectory = directory.split("\\").join("/");
67
+ if (!existsSync(runtimeFile)) {
68
+ logger.warn(`${runtimeFile} not found: add Flamework.addPaths("${posixDirectory}") yourself.`);
69
+ return false;
70
+ }
71
+ const source = readFileSync(runtimeFile, "utf8");
72
+ const alreadyListed = new RegExp(`Flamework\\.addPaths\\(\\s*["']${posixDirectory.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']\\s*\\)`);
73
+ if (alreadyListed.test(source))
74
+ return false;
75
+ // Keep the addPaths calls together: insert after the last one when there is
76
+ // one, and otherwise right before ignite().
77
+ const calls = [...source.matchAll(/^[ \t]*Flamework\.addPaths\([^\n]*\);?[ \t]*$/gm)];
78
+ const last = calls.at(-1);
79
+ const line = `Flamework.addPaths("${posixDirectory}");`;
80
+ if (last !== undefined) {
81
+ const indent = /^[ \t]*/.exec(last[0])?.[0] ?? "";
82
+ const end = last.index + last[0].length;
83
+ writeFileSync(runtimeFile, `${source.slice(0, end)}\n${indent}${line}${source.slice(end)}`, "utf8");
84
+ return true;
85
+ }
86
+ const ignite = /^([ \t]*)Flamework\.ignite\(\);?/m.exec(source);
87
+ if (ignite === null) {
88
+ logger.warn(`Could not find Flamework.ignite() in ${runtimeFile}: add ${line} yourself.`);
89
+ return false;
90
+ }
91
+ writeFileSync(runtimeFile, `${source.slice(0, ignite.index)}${ignite[1] ?? ""}${line}\n\n${source.slice(ignite.index)}`, "utf8");
92
+ return true;
93
+ }
94
+ //# sourceMappingURL=generate.js.map
@@ -0,0 +1,28 @@
1
+ export interface ReleaseAsset {
2
+ name: string;
3
+ browser_download_url: string;
4
+ /** `sha256:<hex>`, published by GitHub for recent release assets only. */
5
+ digest?: string | null;
6
+ }
7
+ export interface Release {
8
+ tag_name: string;
9
+ assets?: ReleaseAsset[];
10
+ }
11
+ /** Fetches a release of `owner/repo`: the latest one, or the one tagged `tag`. */
12
+ export declare function getRelease(repository: string, tag?: string): Promise<Release>;
13
+ export interface DownloadedAsset {
14
+ data: Buffer;
15
+ /** False when GitHub published no checksum, so nothing could be verified. */
16
+ verified: boolean;
17
+ }
18
+ /**
19
+ * Downloads a release asset and checks it against the SHA-256 GitHub publishes.
20
+ *
21
+ * A published checksum that does not match always fails. A missing checksum
22
+ * fails too unless `allowUnverified` is set: older releases predate checksums,
23
+ * and the caller decides whether the artifact is worth the risk.
24
+ */
25
+ export declare function downloadAsset(asset: ReleaseAsset, options?: {
26
+ allowUnverified?: boolean;
27
+ }): Promise<DownloadedAsset>;
28
+ //# sourceMappingURL=github-release.d.ts.map
@@ -0,0 +1,38 @@
1
+ import { createHash } from "node:crypto";
2
+ const HEADERS = { "User-Agent": "rowork" };
3
+ /** Fetches a release of `owner/repo`: the latest one, or the one tagged `tag`. */
4
+ export async function getRelease(repository, tag) {
5
+ const path = tag === undefined ? "latest" : `tags/${tag}`;
6
+ const url = `https://api.github.com/repos/${repository}/releases/${path}`;
7
+ // A stalled connection must not hang the CLI: the API answers in well under a second.
8
+ const response = await fetch(url, { headers: HEADERS, signal: AbortSignal.timeout(20_000) });
9
+ if (!response.ok)
10
+ throw new Error(`${url} answered ${response.status}`);
11
+ return (await response.json());
12
+ }
13
+ /**
14
+ * Downloads a release asset and checks it against the SHA-256 GitHub publishes.
15
+ *
16
+ * A published checksum that does not match always fails. A missing checksum
17
+ * fails too unless `allowUnverified` is set: older releases predate checksums,
18
+ * and the caller decides whether the artifact is worth the risk.
19
+ */
20
+ export async function downloadAsset(asset, options = {}) {
21
+ const response = await fetch(asset.browser_download_url, { headers: HEADERS });
22
+ if (!response.ok)
23
+ throw new Error(`${asset.browser_download_url} answered ${response.status}`);
24
+ const data = Buffer.from(await response.arrayBuffer());
25
+ const expected = asset.digest?.replace(/^sha256:/, "");
26
+ if (expected === undefined) {
27
+ if (options.allowUnverified !== true) {
28
+ throw new Error(`${asset.name} has no published checksum`);
29
+ }
30
+ return { data, verified: false };
31
+ }
32
+ const actual = createHash("sha256").update(data).digest("hex");
33
+ if (actual !== expected) {
34
+ throw new Error(`checksum mismatch for ${asset.name} (expected ${expected}, got ${actual})`);
35
+ }
36
+ return { data, verified: true };
37
+ }
38
+ //# sourceMappingURL=github-release.js.map
@@ -0,0 +1,77 @@
1
+ import { type RoworkConfig } from "../plugins/api.js";
2
+ /**
3
+ * Everything Rowork knows about a project and about itself, as plain data.
4
+ *
5
+ * One source for `rowork info --json` and for the dashboard, so the two can never
6
+ * disagree about what is installed or which commands exist.
7
+ */
8
+ export declare function collectInfo(input: {
9
+ config: RoworkConfig | undefined;
10
+ projectRoot: string | undefined;
11
+ roworkVersion: string;
12
+ }): {
13
+ rowork: {
14
+ version: string;
15
+ pluginApiVersion: number;
16
+ };
17
+ project: {
18
+ name: string;
19
+ root: string;
20
+ language: "roblox-ts";
21
+ paths: {
22
+ source: string;
23
+ out: string;
24
+ rojoProject: string;
25
+ services: string;
26
+ controllers: string;
27
+ shared: string;
28
+ };
29
+ modules: string[];
30
+ integrations: string[];
31
+ dev: {
32
+ pid: number;
33
+ port: number;
34
+ mode: "foreground" | "background";
35
+ } | null;
36
+ devRunningInBackground: {
37
+ pid: number;
38
+ port: number;
39
+ } | null;
40
+ } | null;
41
+ modules: {
42
+ name: string;
43
+ title: string;
44
+ description: string;
45
+ installed: boolean;
46
+ requires: string[];
47
+ dependencies: string[];
48
+ command: string;
49
+ options: {
50
+ flags: string;
51
+ description: string;
52
+ }[];
53
+ }[];
54
+ integrations: {
55
+ name: string;
56
+ title: string;
57
+ description: string;
58
+ modules: string[];
59
+ applied: boolean;
60
+ }[];
61
+ commands: {
62
+ name: string;
63
+ description: string;
64
+ guided: boolean;
65
+ arguments: {
66
+ name: string;
67
+ description: string;
68
+ required: boolean;
69
+ variadic: boolean;
70
+ }[];
71
+ options: {
72
+ flags: string;
73
+ description: string;
74
+ }[];
75
+ }[];
76
+ };
77
+ //# sourceMappingURL=info.d.ts.map
@@ -0,0 +1,65 @@
1
+ import { activeRegistry } from "../cli/registry.js";
2
+ import { coreModules } from "../modules/index.js";
3
+ import { coreIntegrations } from "../modules/integrations.js";
4
+ import { ROWORK_PLUGIN_API_VERSION } from "../plugins/api.js";
5
+ import { runningRecord } from "./background.js";
6
+ /**
7
+ * Everything Rowork knows about a project and about itself, as plain data.
8
+ *
9
+ * One source for `rowork info --json` and for the dashboard, so the two can never
10
+ * disagree about what is installed or which commands exist.
11
+ */
12
+ export function collectInfo(input) {
13
+ const { config, projectRoot } = input;
14
+ const installed = config?.modules ?? [];
15
+ const appliedIntegrations = config?.integrations ?? [];
16
+ const dev = projectRoot === undefined ? undefined : runningRecord(projectRoot);
17
+ const modules = coreModules.map((module) => ({
18
+ name: module.name,
19
+ title: module.title,
20
+ description: module.description,
21
+ installed: installed.includes(module.name),
22
+ requires: module.requires ?? [],
23
+ dependencies: module.dependencies ?? [],
24
+ command: `add:${module.name}`,
25
+ options: (module.options ?? []).map((option) => ({ flags: option.flags, description: option.description })),
26
+ }));
27
+ const integrations = coreIntegrations.map((integration) => ({
28
+ name: integration.name,
29
+ title: integration.title,
30
+ description: integration.description,
31
+ modules: integration.modules,
32
+ applied: appliedIntegrations.includes(integration.name),
33
+ }));
34
+ const commands = (activeRegistry()?.all() ?? []).map(({ definition }) => ({
35
+ name: definition.name,
36
+ description: definition.description,
37
+ guided: definition.guided === true,
38
+ arguments: (definition.arguments ?? []).map((argument) => ({
39
+ name: argument.name,
40
+ description: argument.description,
41
+ required: argument.required ?? true,
42
+ variadic: argument.variadic === true,
43
+ })),
44
+ options: (definition.options ?? []).map((option) => ({ flags: option.flags, description: option.description })),
45
+ }));
46
+ return {
47
+ rowork: { version: input.roworkVersion, pluginApiVersion: ROWORK_PLUGIN_API_VERSION },
48
+ project: projectRoot === undefined || config === undefined
49
+ ? null
50
+ : {
51
+ name: config.name,
52
+ root: projectRoot,
53
+ language: config.language,
54
+ paths: config.paths,
55
+ modules: installed,
56
+ integrations: appliedIntegrations,
57
+ dev: dev === undefined ? null : { pid: dev.pid, port: dev.port, mode: dev.mode ?? "background" },
58
+ devRunningInBackground: dev === undefined || dev.mode === "foreground" ? null : { pid: dev.pid, port: dev.port },
59
+ },
60
+ modules,
61
+ integrations,
62
+ commands,
63
+ };
64
+ }
65
+ //# sourceMappingURL=info.js.map