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.
- package/LICENSE +201 -0
- package/README.md +263 -0
- package/package.json +38 -0
- package/public/app.js +6686 -0
- package/public/components/constants.js +421 -0
- package/public/components/elements.js +118 -0
- package/public/components/state.js +53 -0
- package/public/icons/audio.svg +1 -0
- package/public/icons/azure.svg +1 -0
- package/public/icons/babel.svg +1 -0
- package/public/icons/bun.svg +1 -0
- package/public/icons/bun_light.svg +1 -0
- package/public/icons/c.svg +1 -0
- package/public/icons/chrome.svg +1 -0
- package/public/icons/citation.svg +1 -0
- package/public/icons/claude.svg +1 -0
- package/public/icons/console.svg +1 -0
- package/public/icons/cpp.svg +1 -0
- package/public/icons/css-map.svg +1 -0
- package/public/icons/css.svg +1 -0
- package/public/icons/database.svg +1 -0
- package/public/icons/docker.svg +1 -0
- package/public/icons/document.svg +1 -0
- package/public/icons/ejs.svg +1 -0
- package/public/icons/exe.svg +1 -0
- package/public/icons/favicon.svg +1 -0
- package/public/icons/figma.svg +1 -0
- package/public/icons/firebase.svg +1 -0
- package/public/icons/folder-admin-open.svg +1 -0
- package/public/icons/folder-admin.svg +1 -0
- package/public/icons/folder-api-open.svg +1 -0
- package/public/icons/folder-api.svg +1 -0
- package/public/icons/folder-app-open.svg +1 -0
- package/public/icons/folder-app.svg +1 -0
- package/public/icons/folder-archive-open.svg +1 -0
- package/public/icons/folder-archive.svg +1 -0
- package/public/icons/folder-attachment-open.svg +1 -0
- package/public/icons/folder-attachment.svg +1 -0
- package/public/icons/folder-aws-open.svg +1 -0
- package/public/icons/folder-aws.svg +1 -0
- package/public/icons/folder-backup-open.svg +1 -0
- package/public/icons/folder-backup.svg +1 -0
- package/public/icons/folder-class-open.svg +1 -0
- package/public/icons/folder-class.svg +1 -0
- package/public/icons/folder-claude-open.svg +1 -0
- package/public/icons/folder-claude.svg +1 -0
- package/public/icons/folder-client-open.svg +1 -0
- package/public/icons/folder-client.svg +1 -0
- package/public/icons/folder-command-open.svg +1 -0
- package/public/icons/folder-command.svg +1 -0
- package/public/icons/folder-components-open.svg +1 -0
- package/public/icons/folder-components.svg +1 -0
- package/public/icons/folder-config-open.svg +1 -0
- package/public/icons/folder-config.svg +1 -0
- package/public/icons/folder-connection-open.svg +1 -0
- package/public/icons/folder-connection.svg +1 -0
- package/public/icons/folder-console-open.svg +1 -0
- package/public/icons/folder-console.svg +1 -0
- package/public/icons/folder-container-open.svg +1 -0
- package/public/icons/folder-container.svg +1 -0
- package/public/icons/folder-content-open.svg +1 -0
- package/public/icons/folder-content.svg +1 -0
- package/public/icons/folder-context-open.svg +1 -0
- package/public/icons/folder-context.svg +1 -0
- package/public/icons/folder-controller-open.svg +1 -0
- package/public/icons/folder-controller.svg +1 -0
- package/public/icons/folder-core-open.svg +1 -0
- package/public/icons/folder-core.svg +1 -0
- package/public/icons/folder-css-open.svg +1 -0
- package/public/icons/folder-css.svg +1 -0
- package/public/icons/folder-custom-open.svg +1 -0
- package/public/icons/folder-custom.svg +1 -0
- package/public/icons/folder-database-open.svg +1 -0
- package/public/icons/folder-database.svg +1 -0
- package/public/icons/folder-decorators-open.svg +1 -0
- package/public/icons/folder-decorators.svg +1 -0
- package/public/icons/folder-desktop-open.svg +1 -0
- package/public/icons/folder-desktop.svg +1 -0
- package/public/icons/folder-dist-open.svg +1 -0
- package/public/icons/folder-dist.svg +1 -0
- package/public/icons/folder-docs-open.svg +1 -0
- package/public/icons/folder-docs.svg +1 -0
- package/public/icons/folder-download-open.svg +1 -0
- package/public/icons/folder-download.svg +1 -0
- package/public/icons/folder-dtos-open.svg +1 -0
- package/public/icons/folder-dtos.svg +1 -0
- package/public/icons/folder-element-open.svg +1 -0
- package/public/icons/folder-element.svg +1 -0
- package/public/icons/folder-environment-open.svg +1 -0
- package/public/icons/folder-environment.svg +1 -0
- package/public/icons/folder-error-open.svg +1 -0
- package/public/icons/folder-error.svg +1 -0
- package/public/icons/folder-event-open.svg +1 -0
- package/public/icons/folder-event.svg +1 -0
- package/public/icons/folder-examples-open.svg +1 -0
- package/public/icons/folder-examples.svg +1 -0
- package/public/icons/folder-expo-open.svg +1 -0
- package/public/icons/folder-expo.svg +1 -0
- package/public/icons/folder-export-open.svg +1 -0
- package/public/icons/folder-export.svg +1 -0
- package/public/icons/folder-features-open.svg +1 -0
- package/public/icons/folder-features.svg +1 -0
- package/public/icons/folder-filter-open.svg +1 -0
- package/public/icons/folder-filter.svg +1 -0
- package/public/icons/folder-firebase-open.svg +1 -0
- package/public/icons/folder-firebase.svg +1 -0
- package/public/icons/folder-firestore-open.svg +1 -0
- package/public/icons/folder-firestore.svg +1 -0
- package/public/icons/folder-font-open.svg +1 -0
- package/public/icons/folder-font.svg +1 -0
- package/public/icons/folder-functions-open.svg +1 -0
- package/public/icons/folder-functions.svg +1 -0
- package/public/icons/folder-gemini-ai-open.svg +1 -0
- package/public/icons/folder-gemini-ai.svg +1 -0
- package/public/icons/folder-git-open.svg +1 -0
- package/public/icons/folder-git.svg +1 -0
- package/public/icons/folder-github-open.svg +1 -0
- package/public/icons/folder-github.svg +1 -0
- package/public/icons/folder-helper-open.svg +1 -0
- package/public/icons/folder-helper.svg +1 -0
- package/public/icons/folder-home-open.svg +1 -0
- package/public/icons/folder-home.svg +1 -0
- package/public/icons/folder-icons-open.svg +1 -0
- package/public/icons/folder-icons.svg +1 -0
- package/public/icons/folder-images-open.svg +1 -0
- package/public/icons/folder-images.svg +1 -0
- package/public/icons/folder-interface-open.svg +1 -0
- package/public/icons/folder-interface.svg +1 -0
- package/public/icons/folder-ios-open.svg +1 -0
- package/public/icons/folder-ios.svg +1 -0
- package/public/icons/folder-java-open.svg +1 -0
- package/public/icons/folder-java.svg +1 -0
- package/public/icons/folder-javascript-open.svg +1 -0
- package/public/icons/folder-javascript.svg +1 -0
- package/public/icons/folder-middleware-open.svg +1 -0
- package/public/icons/folder-middleware.svg +1 -0
- package/public/icons/folder-migrations-open.svg +1 -0
- package/public/icons/folder-migrations.svg +1 -0
- package/public/icons/folder-other-open.svg +1 -0
- package/public/icons/folder-other.svg +1 -0
- package/public/icons/folder-packages-open.svg +1 -0
- package/public/icons/folder-packages.svg +1 -0
- package/public/icons/folder-pdf-open.svg +1 -0
- package/public/icons/folder-pdf.svg +1 -0
- package/public/icons/folder-plugin-open.svg +1 -0
- package/public/icons/folder-plugin.svg +1 -0
- package/public/icons/folder-project-open.svg +1 -0
- package/public/icons/folder-project.svg +1 -0
- package/public/icons/folder-public-open.svg +1 -0
- package/public/icons/folder-public.svg +1 -0
- package/public/icons/folder-python-open.svg +1 -0
- package/public/icons/folder-python.svg +1 -0
- package/public/icons/folder-repository-open.svg +1 -0
- package/public/icons/folder-repository.svg +1 -0
- package/public/icons/folder-routes-open.svg +1 -0
- package/public/icons/folder-routes.svg +1 -0
- package/public/icons/folder-rules-open.svg +1 -0
- package/public/icons/folder-rules.svg +1 -0
- package/public/icons/folder-sass-open.svg +1 -0
- package/public/icons/folder-sass.svg +1 -0
- package/public/icons/folder-scripts-open.svg +1 -0
- package/public/icons/folder-scripts.svg +1 -0
- package/public/icons/folder-server-open.svg +1 -0
- package/public/icons/folder-server.svg +1 -0
- package/public/icons/folder-serverless-open.svg +1 -0
- package/public/icons/folder-serverless.svg +1 -0
- package/public/icons/folder-skills-open.svg +1 -0
- package/public/icons/folder-skills.svg +1 -0
- package/public/icons/folder-src-open.svg +1 -0
- package/public/icons/folder-src.svg +1 -0
- package/public/icons/folder-stack-open.svg +1 -0
- package/public/icons/folder-stack.svg +1 -0
- package/public/icons/folder-store-open.svg +1 -0
- package/public/icons/folder-store.svg +1 -0
- package/public/icons/folder-supabase-open.svg +1 -0
- package/public/icons/folder-supabase.svg +1 -0
- package/public/icons/folder-svg-open.svg +1 -0
- package/public/icons/folder-svg.svg +1 -0
- package/public/icons/folder-target-open.svg +1 -0
- package/public/icons/folder-target.svg +1 -0
- package/public/icons/folder-tasks-open.svg +1 -0
- package/public/icons/folder-tasks.svg +1 -0
- package/public/icons/folder-temp-open.svg +1 -0
- package/public/icons/folder-temp.svg +1 -0
- package/public/icons/folder-template-open.svg +1 -0
- package/public/icons/folder-template.svg +1 -0
- package/public/icons/folder-test-open.svg +1 -0
- package/public/icons/folder-test.svg +1 -0
- package/public/icons/folder-tools-open.svg +1 -0
- package/public/icons/folder-tools.svg +1 -0
- package/public/icons/folder-typescript-open.svg +1 -0
- package/public/icons/folder-typescript.svg +1 -0
- package/public/icons/folder-ui-open.svg +1 -0
- package/public/icons/folder-ui.svg +1 -0
- package/public/icons/folder-upload-open.svg +1 -0
- package/public/icons/folder-upload.svg +1 -0
- package/public/icons/folder-utils-open.svg +1 -0
- package/public/icons/folder-utils.svg +1 -0
- package/public/icons/folder-video-open.svg +1 -0
- package/public/icons/folder-video.svg +1 -0
- package/public/icons/folder-views-open.svg +1 -0
- package/public/icons/folder-views.svg +1 -0
- package/public/icons/font.svg +1 -0
- package/public/icons/gemini-ai.svg +1 -0
- package/public/icons/gemini.svg +1 -0
- package/public/icons/git.svg +1 -0
- package/public/icons/google.svg +1 -0
- package/public/icons/graphql.svg +1 -0
- package/public/icons/html.svg +1 -0
- package/public/icons/image.svg +1 -0
- package/public/icons/java.svg +1 -0
- package/public/icons/javaclass.svg +1 -0
- package/public/icons/javascript.svg +1 -0
- package/public/icons/jsconfig.svg +1 -0
- package/public/icons/json.svg +1 -0
- package/public/icons/markdown.svg +1 -0
- package/public/icons/nodejs.svg +1 -0
- package/public/icons/nodejs_alt.svg +1 -0
- package/public/icons/nodemon.svg +1 -0
- package/public/icons/npm.svg +1 -0
- package/public/icons/pdf.svg +1 -0
- package/public/icons/prettier.svg +1 -0
- package/public/icons/prisma.svg +1 -0
- package/public/icons/python.svg +1 -0
- package/public/icons/react.svg +1 -0
- package/public/icons/react_ts.svg +1 -0
- package/public/icons/readme.svg +1 -0
- package/public/icons/remark.svg +1 -0
- package/public/icons/sass.svg +1 -0
- package/public/icons/svg.svg +1 -0
- package/public/icons/tailwindcss.svg +1 -0
- package/public/icons/typescript-def.svg +1 -0
- package/public/icons/typescript.svg +1 -0
- package/public/icons/zip.svg +1 -0
- package/public/index.html +1342 -0
- package/public/styles.css +4736 -0
- package/src/cli.js +175 -0
- package/src/lib/files.js +143 -0
- package/src/lib/notebook.js +141 -0
- package/src/lib/package-exports.js +331 -0
- package/src/lib/session.js +1003 -0
- 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
|
+
});
|
package/src/lib/files.js
ADDED
|
@@ -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
|
+
}
|