myjsbook 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +263 -0
  3. package/package.json +38 -0
  4. package/public/app.js +6686 -0
  5. package/public/components/constants.js +421 -0
  6. package/public/components/elements.js +118 -0
  7. package/public/components/state.js +53 -0
  8. package/public/icons/audio.svg +1 -0
  9. package/public/icons/azure.svg +1 -0
  10. package/public/icons/babel.svg +1 -0
  11. package/public/icons/bun.svg +1 -0
  12. package/public/icons/bun_light.svg +1 -0
  13. package/public/icons/c.svg +1 -0
  14. package/public/icons/chrome.svg +1 -0
  15. package/public/icons/citation.svg +1 -0
  16. package/public/icons/claude.svg +1 -0
  17. package/public/icons/console.svg +1 -0
  18. package/public/icons/cpp.svg +1 -0
  19. package/public/icons/css-map.svg +1 -0
  20. package/public/icons/css.svg +1 -0
  21. package/public/icons/database.svg +1 -0
  22. package/public/icons/docker.svg +1 -0
  23. package/public/icons/document.svg +1 -0
  24. package/public/icons/ejs.svg +1 -0
  25. package/public/icons/exe.svg +1 -0
  26. package/public/icons/favicon.svg +1 -0
  27. package/public/icons/figma.svg +1 -0
  28. package/public/icons/firebase.svg +1 -0
  29. package/public/icons/folder-admin-open.svg +1 -0
  30. package/public/icons/folder-admin.svg +1 -0
  31. package/public/icons/folder-api-open.svg +1 -0
  32. package/public/icons/folder-api.svg +1 -0
  33. package/public/icons/folder-app-open.svg +1 -0
  34. package/public/icons/folder-app.svg +1 -0
  35. package/public/icons/folder-archive-open.svg +1 -0
  36. package/public/icons/folder-archive.svg +1 -0
  37. package/public/icons/folder-attachment-open.svg +1 -0
  38. package/public/icons/folder-attachment.svg +1 -0
  39. package/public/icons/folder-aws-open.svg +1 -0
  40. package/public/icons/folder-aws.svg +1 -0
  41. package/public/icons/folder-backup-open.svg +1 -0
  42. package/public/icons/folder-backup.svg +1 -0
  43. package/public/icons/folder-class-open.svg +1 -0
  44. package/public/icons/folder-class.svg +1 -0
  45. package/public/icons/folder-claude-open.svg +1 -0
  46. package/public/icons/folder-claude.svg +1 -0
  47. package/public/icons/folder-client-open.svg +1 -0
  48. package/public/icons/folder-client.svg +1 -0
  49. package/public/icons/folder-command-open.svg +1 -0
  50. package/public/icons/folder-command.svg +1 -0
  51. package/public/icons/folder-components-open.svg +1 -0
  52. package/public/icons/folder-components.svg +1 -0
  53. package/public/icons/folder-config-open.svg +1 -0
  54. package/public/icons/folder-config.svg +1 -0
  55. package/public/icons/folder-connection-open.svg +1 -0
  56. package/public/icons/folder-connection.svg +1 -0
  57. package/public/icons/folder-console-open.svg +1 -0
  58. package/public/icons/folder-console.svg +1 -0
  59. package/public/icons/folder-container-open.svg +1 -0
  60. package/public/icons/folder-container.svg +1 -0
  61. package/public/icons/folder-content-open.svg +1 -0
  62. package/public/icons/folder-content.svg +1 -0
  63. package/public/icons/folder-context-open.svg +1 -0
  64. package/public/icons/folder-context.svg +1 -0
  65. package/public/icons/folder-controller-open.svg +1 -0
  66. package/public/icons/folder-controller.svg +1 -0
  67. package/public/icons/folder-core-open.svg +1 -0
  68. package/public/icons/folder-core.svg +1 -0
  69. package/public/icons/folder-css-open.svg +1 -0
  70. package/public/icons/folder-css.svg +1 -0
  71. package/public/icons/folder-custom-open.svg +1 -0
  72. package/public/icons/folder-custom.svg +1 -0
  73. package/public/icons/folder-database-open.svg +1 -0
  74. package/public/icons/folder-database.svg +1 -0
  75. package/public/icons/folder-decorators-open.svg +1 -0
  76. package/public/icons/folder-decorators.svg +1 -0
  77. package/public/icons/folder-desktop-open.svg +1 -0
  78. package/public/icons/folder-desktop.svg +1 -0
  79. package/public/icons/folder-dist-open.svg +1 -0
  80. package/public/icons/folder-dist.svg +1 -0
  81. package/public/icons/folder-docs-open.svg +1 -0
  82. package/public/icons/folder-docs.svg +1 -0
  83. package/public/icons/folder-download-open.svg +1 -0
  84. package/public/icons/folder-download.svg +1 -0
  85. package/public/icons/folder-dtos-open.svg +1 -0
  86. package/public/icons/folder-dtos.svg +1 -0
  87. package/public/icons/folder-element-open.svg +1 -0
  88. package/public/icons/folder-element.svg +1 -0
  89. package/public/icons/folder-environment-open.svg +1 -0
  90. package/public/icons/folder-environment.svg +1 -0
  91. package/public/icons/folder-error-open.svg +1 -0
  92. package/public/icons/folder-error.svg +1 -0
  93. package/public/icons/folder-event-open.svg +1 -0
  94. package/public/icons/folder-event.svg +1 -0
  95. package/public/icons/folder-examples-open.svg +1 -0
  96. package/public/icons/folder-examples.svg +1 -0
  97. package/public/icons/folder-expo-open.svg +1 -0
  98. package/public/icons/folder-expo.svg +1 -0
  99. package/public/icons/folder-export-open.svg +1 -0
  100. package/public/icons/folder-export.svg +1 -0
  101. package/public/icons/folder-features-open.svg +1 -0
  102. package/public/icons/folder-features.svg +1 -0
  103. package/public/icons/folder-filter-open.svg +1 -0
  104. package/public/icons/folder-filter.svg +1 -0
  105. package/public/icons/folder-firebase-open.svg +1 -0
  106. package/public/icons/folder-firebase.svg +1 -0
  107. package/public/icons/folder-firestore-open.svg +1 -0
  108. package/public/icons/folder-firestore.svg +1 -0
  109. package/public/icons/folder-font-open.svg +1 -0
  110. package/public/icons/folder-font.svg +1 -0
  111. package/public/icons/folder-functions-open.svg +1 -0
  112. package/public/icons/folder-functions.svg +1 -0
  113. package/public/icons/folder-gemini-ai-open.svg +1 -0
  114. package/public/icons/folder-gemini-ai.svg +1 -0
  115. package/public/icons/folder-git-open.svg +1 -0
  116. package/public/icons/folder-git.svg +1 -0
  117. package/public/icons/folder-github-open.svg +1 -0
  118. package/public/icons/folder-github.svg +1 -0
  119. package/public/icons/folder-helper-open.svg +1 -0
  120. package/public/icons/folder-helper.svg +1 -0
  121. package/public/icons/folder-home-open.svg +1 -0
  122. package/public/icons/folder-home.svg +1 -0
  123. package/public/icons/folder-icons-open.svg +1 -0
  124. package/public/icons/folder-icons.svg +1 -0
  125. package/public/icons/folder-images-open.svg +1 -0
  126. package/public/icons/folder-images.svg +1 -0
  127. package/public/icons/folder-interface-open.svg +1 -0
  128. package/public/icons/folder-interface.svg +1 -0
  129. package/public/icons/folder-ios-open.svg +1 -0
  130. package/public/icons/folder-ios.svg +1 -0
  131. package/public/icons/folder-java-open.svg +1 -0
  132. package/public/icons/folder-java.svg +1 -0
  133. package/public/icons/folder-javascript-open.svg +1 -0
  134. package/public/icons/folder-javascript.svg +1 -0
  135. package/public/icons/folder-middleware-open.svg +1 -0
  136. package/public/icons/folder-middleware.svg +1 -0
  137. package/public/icons/folder-migrations-open.svg +1 -0
  138. package/public/icons/folder-migrations.svg +1 -0
  139. package/public/icons/folder-other-open.svg +1 -0
  140. package/public/icons/folder-other.svg +1 -0
  141. package/public/icons/folder-packages-open.svg +1 -0
  142. package/public/icons/folder-packages.svg +1 -0
  143. package/public/icons/folder-pdf-open.svg +1 -0
  144. package/public/icons/folder-pdf.svg +1 -0
  145. package/public/icons/folder-plugin-open.svg +1 -0
  146. package/public/icons/folder-plugin.svg +1 -0
  147. package/public/icons/folder-project-open.svg +1 -0
  148. package/public/icons/folder-project.svg +1 -0
  149. package/public/icons/folder-public-open.svg +1 -0
  150. package/public/icons/folder-public.svg +1 -0
  151. package/public/icons/folder-python-open.svg +1 -0
  152. package/public/icons/folder-python.svg +1 -0
  153. package/public/icons/folder-repository-open.svg +1 -0
  154. package/public/icons/folder-repository.svg +1 -0
  155. package/public/icons/folder-routes-open.svg +1 -0
  156. package/public/icons/folder-routes.svg +1 -0
  157. package/public/icons/folder-rules-open.svg +1 -0
  158. package/public/icons/folder-rules.svg +1 -0
  159. package/public/icons/folder-sass-open.svg +1 -0
  160. package/public/icons/folder-sass.svg +1 -0
  161. package/public/icons/folder-scripts-open.svg +1 -0
  162. package/public/icons/folder-scripts.svg +1 -0
  163. package/public/icons/folder-server-open.svg +1 -0
  164. package/public/icons/folder-server.svg +1 -0
  165. package/public/icons/folder-serverless-open.svg +1 -0
  166. package/public/icons/folder-serverless.svg +1 -0
  167. package/public/icons/folder-skills-open.svg +1 -0
  168. package/public/icons/folder-skills.svg +1 -0
  169. package/public/icons/folder-src-open.svg +1 -0
  170. package/public/icons/folder-src.svg +1 -0
  171. package/public/icons/folder-stack-open.svg +1 -0
  172. package/public/icons/folder-stack.svg +1 -0
  173. package/public/icons/folder-store-open.svg +1 -0
  174. package/public/icons/folder-store.svg +1 -0
  175. package/public/icons/folder-supabase-open.svg +1 -0
  176. package/public/icons/folder-supabase.svg +1 -0
  177. package/public/icons/folder-svg-open.svg +1 -0
  178. package/public/icons/folder-svg.svg +1 -0
  179. package/public/icons/folder-target-open.svg +1 -0
  180. package/public/icons/folder-target.svg +1 -0
  181. package/public/icons/folder-tasks-open.svg +1 -0
  182. package/public/icons/folder-tasks.svg +1 -0
  183. package/public/icons/folder-temp-open.svg +1 -0
  184. package/public/icons/folder-temp.svg +1 -0
  185. package/public/icons/folder-template-open.svg +1 -0
  186. package/public/icons/folder-template.svg +1 -0
  187. package/public/icons/folder-test-open.svg +1 -0
  188. package/public/icons/folder-test.svg +1 -0
  189. package/public/icons/folder-tools-open.svg +1 -0
  190. package/public/icons/folder-tools.svg +1 -0
  191. package/public/icons/folder-typescript-open.svg +1 -0
  192. package/public/icons/folder-typescript.svg +1 -0
  193. package/public/icons/folder-ui-open.svg +1 -0
  194. package/public/icons/folder-ui.svg +1 -0
  195. package/public/icons/folder-upload-open.svg +1 -0
  196. package/public/icons/folder-upload.svg +1 -0
  197. package/public/icons/folder-utils-open.svg +1 -0
  198. package/public/icons/folder-utils.svg +1 -0
  199. package/public/icons/folder-video-open.svg +1 -0
  200. package/public/icons/folder-video.svg +1 -0
  201. package/public/icons/folder-views-open.svg +1 -0
  202. package/public/icons/folder-views.svg +1 -0
  203. package/public/icons/font.svg +1 -0
  204. package/public/icons/gemini-ai.svg +1 -0
  205. package/public/icons/gemini.svg +1 -0
  206. package/public/icons/git.svg +1 -0
  207. package/public/icons/google.svg +1 -0
  208. package/public/icons/graphql.svg +1 -0
  209. package/public/icons/html.svg +1 -0
  210. package/public/icons/image.svg +1 -0
  211. package/public/icons/java.svg +1 -0
  212. package/public/icons/javaclass.svg +1 -0
  213. package/public/icons/javascript.svg +1 -0
  214. package/public/icons/jsconfig.svg +1 -0
  215. package/public/icons/json.svg +1 -0
  216. package/public/icons/markdown.svg +1 -0
  217. package/public/icons/nodejs.svg +1 -0
  218. package/public/icons/nodejs_alt.svg +1 -0
  219. package/public/icons/nodemon.svg +1 -0
  220. package/public/icons/npm.svg +1 -0
  221. package/public/icons/pdf.svg +1 -0
  222. package/public/icons/prettier.svg +1 -0
  223. package/public/icons/prisma.svg +1 -0
  224. package/public/icons/python.svg +1 -0
  225. package/public/icons/react.svg +1 -0
  226. package/public/icons/react_ts.svg +1 -0
  227. package/public/icons/readme.svg +1 -0
  228. package/public/icons/remark.svg +1 -0
  229. package/public/icons/sass.svg +1 -0
  230. package/public/icons/svg.svg +1 -0
  231. package/public/icons/tailwindcss.svg +1 -0
  232. package/public/icons/typescript-def.svg +1 -0
  233. package/public/icons/typescript.svg +1 -0
  234. package/public/icons/zip.svg +1 -0
  235. package/public/index.html +1342 -0
  236. package/public/styles.css +4736 -0
  237. package/src/cli.js +175 -0
  238. package/src/lib/files.js +143 -0
  239. package/src/lib/notebook.js +141 -0
  240. package/src/lib/package-exports.js +331 -0
  241. package/src/lib/session.js +1003 -0
  242. package/src/server.js +2232 -0
package/src/cli.js ADDED
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { exec } from "node:child_process";
5
+
6
+ import { startServer } from "./server.js";
7
+
8
+ /**
9
+ * Opens the given URL in the default system browser.
10
+ * Works on macOS, Windows, and Linux.
11
+ */
12
+ function openBrowser(url) {
13
+ const platform = process.platform;
14
+ let command;
15
+
16
+ if (platform === "darwin") {
17
+ command = `open "${url}"`;
18
+ } else if (platform === "win32") {
19
+ command = `start "" "${url}"`;
20
+ } else {
21
+ command = `xdg-open "${url}"`;
22
+ }
23
+
24
+ exec(command, (error) => {
25
+ if (error) {
26
+ // Non-fatal: just print a hint so the user can open manually
27
+ process.stdout.write(` Tip: Open your browser at: ${url}\n`);
28
+ }
29
+ });
30
+ }
31
+
32
+ function parseArgs(argv) {
33
+ const options = {
34
+ workspaceRoot: process.cwd(),
35
+ port: Number(process.env.PORT || 3113),
36
+ openBrowser: true
37
+ };
38
+
39
+ for (let index = 0; index < argv.length; index += 1) {
40
+ const arg = argv[index];
41
+
42
+ // "start" is the primary subcommand — it is the default action, so just skip it
43
+ if (arg === "start") {
44
+ continue;
45
+ }
46
+
47
+ if (arg === "--port" || arg === "-p") {
48
+ const nextValue = argv[index + 1];
49
+ if (!nextValue) {
50
+ throw new Error("Missing value for --port");
51
+ }
52
+ options.port = Number(nextValue);
53
+ index += 1;
54
+ continue;
55
+ }
56
+
57
+ if (arg === "--workspace" || arg === "-w") {
58
+ const nextValue = argv[index + 1];
59
+ if (!nextValue) {
60
+ throw new Error("Missing value for --workspace");
61
+ }
62
+ options.workspaceRoot = path.resolve(nextValue);
63
+ index += 1;
64
+ continue;
65
+ }
66
+
67
+ if (arg === "--no-open") {
68
+ options.openBrowser = false;
69
+ continue;
70
+ }
71
+
72
+ if (arg === "--help" || arg === "-h") {
73
+ options.help = true;
74
+ continue;
75
+ }
76
+
77
+ if (!arg.startsWith("-")) {
78
+ // Positional argument treated as workspace path
79
+ options.workspaceRoot = path.resolve(arg);
80
+ continue;
81
+ }
82
+
83
+ throw new Error(`Unknown argument: ${arg}`);
84
+ }
85
+
86
+ if (!Number.isFinite(options.port) || options.port <= 0) {
87
+ throw new Error("Port must be a positive number");
88
+ }
89
+
90
+ return options;
91
+ }
92
+
93
+ function printHelp() {
94
+ process.stdout.write(
95
+ [
96
+ "marsbook — local JavaScript/TypeScript notebook server",
97
+ "",
98
+ "Usage: marsbook start [options]",
99
+ "",
100
+ "Commands:",
101
+ " start Start the notebook server and open the browser (default)",
102
+ "",
103
+ "Options:",
104
+ " -p, --port Port to bind the server on (default: 3113)",
105
+ " -w, --workspace Workspace directory — where your .ijsnb files live",
106
+ " (default: current working directory)",
107
+ " --no-open Skip opening the browser automatically",
108
+ " -h, --help Show this help message",
109
+ "",
110
+ "Examples:",
111
+ " marsbook start",
112
+ " marsbook start --port 4000",
113
+ " marsbook start --workspace ~/my-notebooks",
114
+ " marsbook start --no-open"
115
+ ].join("\n") + "\n"
116
+ );
117
+ }
118
+
119
+
120
+ function printBanner(serverUrl, workspaceRoot) {
121
+ // Colors
122
+ const O = "\x1b[38;5;208m"; // mars orange
123
+ const OR = "\x1b[38;5;202m"; // dark orange / red-orange
124
+ const R = "\x1b[38;5;196m"; // red
125
+ const W = "\x1b[97m"; // bright white
126
+ const G = "\x1b[38;5;82m"; // green (for URL)
127
+ const C = "\x1b[38;5;117m"; // cyan (for workspace)
128
+ const D = "\x1b[2m"; // dim
129
+ const B = "\x1b[1m"; // bold
130
+ const X = "\x1b[0m"; // reset
131
+
132
+ process.stdout.write("\n");
133
+
134
+ // Mars planet ASCII art + title side by side
135
+ process.stdout.write(`${OR} . . . ${X} ${B}${O}███╗ ███╗ █████╗ ██████╗ ███████╗${X}\n`);
136
+ process.stdout.write(`${OR} . ${O}╭──────────╮${OR} . ${X} ${B}${O}████╗ ████║██╔══██╗██╔══██╗██╔════╝${X}\n`);
137
+ process.stdout.write(`${OR} . ${O}│ ${R}● ╭──╮${O} │${OR} . ${X} ${B}${O}██╔████╔██║███████║██████╔╝███████╗${X}\n`);
138
+ process.stdout.write(`${OR} . ${O}│ ${R}╰──╯ ●${O} │${OR} . ${X} ${B}${O}██║╚██╔╝██║██╔══██║██╔══██╗╚════██║${X}\n`);
139
+ process.stdout.write(`${OR} . ${O}│ ${R} ● ${O} │${OR} . ${X} ${B}${O}██║ ╚═╝ ██║██║ ██║██║ ██║███████║${X}\n`);
140
+ process.stdout.write(`${OR} . ${O}╰──────────╯${OR} . ${X} ${B}${O}╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝${X}\n`);
141
+ process.stdout.write(`${OR} . . . ${X} ${D}${W}The JavaScript Notebook for Developers${X}\n`);
142
+
143
+ process.stdout.write("\n");
144
+ process.stdout.write(`${D} 🪐 ${B}${W}Server is running!${X} \n`);
145
+ process.stdout.write(`${D} \n`);
146
+ process.stdout.write(`${D} ${W}Local ${X} ${G}${serverUrl}${X}\n`);
147
+ process.stdout.write(`${D} ${W}Workspace${X} ${C}${workspaceRoot}${X}\n`);
148
+ process.stdout.write(`${D} \n`);
149
+ process.stdout.write(`${D} ${D}Press Ctrl+C to stop the server${X}\n`);
150
+ process.stdout.write("\n");
151
+ }
152
+
153
+ async function main() {
154
+ const options = parseArgs(process.argv.slice(2));
155
+
156
+ if (options.help) {
157
+ printHelp();
158
+ return;
159
+ }
160
+
161
+ const { host, port, workspaceRoot } = await startServer(options);
162
+ const serverUrl = `http://${host}:${port}`;
163
+
164
+ printBanner(serverUrl, workspaceRoot)
165
+
166
+ if (options.openBrowser) {
167
+ // Small delay so the server is fully ready before the browser connects
168
+ setTimeout(() => openBrowser(serverUrl), 400);
169
+ }
170
+ }
171
+
172
+ main().catch((error) => {
173
+ process.stderr.write(`Error: ${error?.message ?? String(error)}\n`);
174
+ process.exitCode = 1;
175
+ });
@@ -0,0 +1,143 @@
1
+ import path from "node:path";
2
+
3
+ import { NOTEBOOK_EXTENSION } from "./notebook.js";
4
+
5
+ const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".avif"]);
6
+ const TEXT_EXTENSIONS = new Set([".txt", ".md", ".js", ".ts"]);
7
+ const PDF_EXTENSION = ".pdf";
8
+
9
+ // The URL prefix used in the browser for all workspace files.
10
+ // e.g. workspaceRoot/my_notebook.ijsnb → /notebooks/my_notebook.ijsnb
11
+ // workspaceRoot/any_folder/basic.ijsnb → /notebooks/any_folder/basic.ijsnb
12
+ export const NOTEBOOKS_URL_PREFIX = "notebooks";
13
+
14
+ export function getOpenableFileKind(filePath) {
15
+ const extension = path.extname(filePath).toLowerCase();
16
+
17
+ if (extension === NOTEBOOK_EXTENSION) {
18
+ return "notebook";
19
+ }
20
+
21
+ if (IMAGE_EXTENSIONS.has(extension)) {
22
+ return "image";
23
+ }
24
+
25
+ if (TEXT_EXTENSIONS.has(extension)) {
26
+ return "text";
27
+ }
28
+
29
+ if (extension === PDF_EXTENSION) {
30
+ return "pdf";
31
+ }
32
+
33
+ return null;
34
+ }
35
+
36
+ export function isOpenableFile(filePath) {
37
+ return getOpenableFileKind(filePath) !== null;
38
+ }
39
+
40
+ export function sanitizeNotebookTitle(title) {
41
+ const normalized = String(title ?? "")
42
+ .trim()
43
+ .replace(/[<>:"/\\|?*\x00-\x1F]+/g, " ")
44
+ .replace(/\s+/g, "-")
45
+ .replace(/-+/g, "-")
46
+ .replace(/^-|-$/g, "");
47
+
48
+ return normalized || "untitled";
49
+ }
50
+
51
+ export function deriveNotebookPathFromTitle(notebookPath, title) {
52
+ const directory = path.dirname(notebookPath);
53
+ const fileName = `${sanitizeNotebookTitle(title)}${NOTEBOOK_EXTENSION}`;
54
+ return path.join(directory, fileName);
55
+ }
56
+
57
+ /**
58
+ * Convert an absolute workspace file path to the browser URL path.
59
+ *
60
+ * All workspace files are mounted under the /notebooks/ URL prefix so the
61
+ * browser always shows a clean, predictable path regardless of where the
62
+ * physical file lives:
63
+ *
64
+ * workspaceRoot/my_notebook.ijsnb → /notebooks/my_notebook.ijsnb
65
+ * workspaceRoot/any_folder/basic.ijsnb → /notebooks/any_folder/basic.ijsnb
66
+ * workspaceRoot/demo.jpg → /notebooks/demo.jpg
67
+ */
68
+ export function getAppPathFromWorkspacePath(projectRoot, notebooksDir, absolutePath) {
69
+ const relativePath = path.relative(projectRoot, absolutePath).replaceAll(path.sep, "/");
70
+ return `/${NOTEBOOKS_URL_PREFIX}/${relativePath}`;
71
+ }
72
+
73
+ /**
74
+ * Resolve a client-supplied path (which may be a browser URL path like
75
+ * "/notebooks/any_folder/basic.ijsnb" or an absolute filesystem path) to an
76
+ * absolute path on disk inside the workspace.
77
+ *
78
+ * The /notebooks/ URL prefix is stripped before resolution so that URL paths
79
+ * map cleanly onto workspace-root-relative file paths.
80
+ */
81
+ export function resolveWorkspaceOpenPath(projectRoot, notebooksDir, requestedPath, options = {}) {
82
+ const { allowNotebookCreate = false } = options;
83
+ const existsSync = options.existsSync ?? (() => false);
84
+
85
+ if (!requestedPath) {
86
+ return null;
87
+ }
88
+
89
+ const requestedStr = String(requestedPath).trim();
90
+
91
+ // Absolute filesystem path that lives inside the workspace — return as-is.
92
+ // We check startsWith(projectRoot) first so that real OS paths are handled
93
+ // before the URL-prefix stripping logic below.
94
+ if (path.isAbsolute(requestedStr) && requestedStr.startsWith(projectRoot)) {
95
+ return requestedStr;
96
+ }
97
+
98
+ // Strip leading slashes, then strip the /notebooks/ URL prefix if present.
99
+ // This lets the frontend pass browser URL paths like "/notebooks/my_notebook.ijsnb"
100
+ // and have them resolve correctly to workspaceRoot-relative file paths.
101
+ let normalized = requestedStr.replace(/^\/+/, "");
102
+
103
+ // Strip the URL prefix "notebooks/" → remainder is workspace-root-relative
104
+ if (normalized.startsWith(`${NOTEBOOKS_URL_PREFIX}/`)) {
105
+ normalized = normalized.slice(`${NOTEBOOKS_URL_PREFIX}/`.length);
106
+ }
107
+
108
+ if (!normalized) {
109
+ return null;
110
+ }
111
+
112
+ // Resolve the path relative to the workspace root.
113
+ const candidate = path.resolve(projectRoot, normalized);
114
+
115
+ if (!candidate.startsWith(projectRoot)) {
116
+ return null;
117
+ }
118
+
119
+ if (existsSync(candidate)) {
120
+ return candidate;
121
+ }
122
+
123
+ // Try appending the notebook extension if it is missing.
124
+ const notebookWithExtension = candidate.endsWith(NOTEBOOK_EXTENSION)
125
+ ? candidate
126
+ : `${candidate}${NOTEBOOK_EXTENSION}`;
127
+
128
+ if (existsSync(notebookWithExtension)) {
129
+ return notebookWithExtension;
130
+ }
131
+
132
+ if (allowNotebookCreate) {
133
+ const withExtension = candidate.endsWith(NOTEBOOK_EXTENSION)
134
+ ? candidate
135
+ : `${candidate}${NOTEBOOK_EXTENSION}`;
136
+
137
+ if (withExtension.startsWith(projectRoot)) {
138
+ return withExtension;
139
+ }
140
+ }
141
+
142
+ return null;
143
+ }
@@ -0,0 +1,141 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ export const NOTEBOOK_VERSION = 1;
4
+ export const NOTEBOOK_EXTENSION = ".ijsnb";
5
+
6
+ function createPromptConfig(prompt = {}) {
7
+ return {
8
+ provider: typeof prompt.provider === "string" && prompt.provider.trim() ? prompt.provider : "groq",
9
+ model: typeof prompt.model === "string" && prompt.model.trim() ? prompt.model : "llama-3.3-70b-versatile",
10
+ system: typeof prompt.system === "string" ? prompt.system : "",
11
+ temperature: Number.isFinite(prompt.temperature) ? Number(prompt.temperature) : 0.2
12
+ };
13
+ }
14
+
15
+ export function createCell(type = "code", source = "", options = {}) {
16
+ const cell = {
17
+ id: randomUUID(),
18
+ type,
19
+ source,
20
+ executionCount: null,
21
+ outputs: [],
22
+ collapsed: false,
23
+ outputCollapsed: false,
24
+ metrics: {
25
+ aiTokensIn: 0,
26
+ aiTokensOut: 0,
27
+ aiTokensTotal: 0,
28
+ aiTokensUpdatedAt: null
29
+ }
30
+ };
31
+
32
+ if (type === "code") {
33
+ cell.language = options.language === "javascript" ? "javascript" : "typescript";
34
+ }
35
+
36
+ if (type === "prompt") {
37
+ cell.prompt = createPromptConfig(options.prompt);
38
+ }
39
+
40
+ return cell;
41
+ }
42
+
43
+ export function createNotebook(title = "Untitled Nodebook") {
44
+ return {
45
+ format: "ijsnb",
46
+ version: NOTEBOOK_VERSION,
47
+ metadata: {
48
+ title,
49
+ createdAt: new Date().toISOString(),
50
+ env: {}
51
+ },
52
+ cells: [
53
+ createCell(
54
+ "markdown",
55
+ "# Welcome to Mars Book\n\nRun JavaScript and Node.js code with persistent notebook state."
56
+ ),
57
+ createCell(
58
+ "code",
59
+ [
60
+ "console.log('Welcome to Mars Book')",
61
+ ].join("\n"),
62
+ { language: "typescript" }
63
+ )
64
+ ]
65
+ };
66
+ }
67
+
68
+ export function normalizeNotebook(rawNotebook) {
69
+ const notebook = rawNotebook && typeof rawNotebook === "object" ? rawNotebook : {};
70
+ const metadata = notebook.metadata && typeof notebook.metadata === "object" ? notebook.metadata : {};
71
+ const cells = Array.isArray(notebook.cells) ? notebook.cells : [];
72
+
73
+ return {
74
+ format: "ijsnb",
75
+ version: NOTEBOOK_VERSION,
76
+ metadata: {
77
+ title: typeof metadata.title === "string" && metadata.title.trim() ? metadata.title : "Untitled Nodebook",
78
+ createdAt: typeof metadata.createdAt === "string" ? metadata.createdAt : new Date().toISOString(),
79
+ updatedAt: new Date().toISOString(),
80
+ ai:
81
+ metadata.ai && typeof metadata.ai === "object"
82
+ ? {
83
+ provider:
84
+ typeof metadata.ai.provider === "string" && metadata.ai.provider.trim()
85
+ ? metadata.ai.provider
86
+ : "groq",
87
+ model:
88
+ typeof metadata.ai.model === "string" && metadata.ai.model.trim()
89
+ ? metadata.ai.model
90
+ : "llama-3.3-70b-versatile"
91
+ }
92
+ : {
93
+ provider: "groq",
94
+ model: "llama-3.3-70b-versatile"
95
+ },
96
+ env:
97
+ metadata.env && typeof metadata.env === "object"
98
+ ? Object.fromEntries(
99
+ Object.entries(metadata.env)
100
+ .map(([key, value]) => [String(key).trim(), typeof value === "string" ? value : String(value ?? "")])
101
+ .filter(([key]) => key)
102
+ )
103
+ : {}
104
+ },
105
+ cells: cells.length
106
+ ? cells.map((cell) => {
107
+ const cellType = cell.type === "markdown" ? "markdown" : cell.type === "prompt" ? "prompt" : "code";
108
+ const metrics = cell.metrics && typeof cell.metrics === "object" ? cell.metrics : {};
109
+ const normalizedCell = {
110
+ id: typeof cell.id === "string" && cell.id ? cell.id : randomUUID(),
111
+ type: cellType,
112
+ source: typeof cell.source === "string" ? cell.source : "",
113
+ executionCount: Number.isInteger(cell.executionCount) ? cell.executionCount : null,
114
+ outputs: Array.isArray(cell.outputs) ? cell.outputs : [],
115
+ collapsed: Boolean(cell.collapsed),
116
+ outputCollapsed: Boolean(cell.outputCollapsed),
117
+ metrics: {
118
+ aiTokensIn: Number.isFinite(metrics.aiTokensIn) ? metrics.aiTokensIn : 0,
119
+ aiTokensOut: Number.isFinite(metrics.aiTokensOut) ? metrics.aiTokensOut : 0,
120
+ aiTokensTotal: Number.isFinite(metrics.aiTokensTotal)
121
+ ? metrics.aiTokensTotal
122
+ : (Number.isFinite(metrics.aiTokensIn) || Number.isFinite(metrics.aiTokensOut)
123
+ ? (Number(metrics.aiTokensIn) || 0) + (Number(metrics.aiTokensOut) || 0)
124
+ : 0),
125
+ aiTokensUpdatedAt: typeof metrics.aiTokensUpdatedAt === "string" ? metrics.aiTokensUpdatedAt : null
126
+ }
127
+ };
128
+
129
+ if (cellType === "code") {
130
+ normalizedCell.language = cell.language === "javascript" ? "javascript" : "typescript";
131
+ }
132
+
133
+ if (cellType === "prompt") {
134
+ normalizedCell.prompt = createPromptConfig(cell.prompt);
135
+ }
136
+
137
+ return normalizedCell;
138
+ })
139
+ : [createCell("code", "console.log('Nodebook is ready');", { language: "typescript" })]
140
+ };
141
+ }