├── .changeset ├── README.md └── config.json ├── .github └── workflows │ ├── release.yml │ └── run-tests.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── FAQ.md ├── LICENSE ├── PRIVACY-POLICY.md ├── README.md ├── docker-compose.yml ├── package.json ├── packages ├── api │ ├── .eslintrc.cjs │ ├── .npmignore │ ├── CHANGELOG.md │ ├── ai │ │ ├── app-parser.mts │ │ ├── config.mts │ │ ├── generate.mts │ │ ├── logger.mts │ │ ├── plan-parser.mts │ │ └── stream-xml-parser.mts │ ├── apps │ │ ├── app.mts │ │ ├── disk.mts │ │ ├── git.mts │ │ ├── processes.mts │ │ ├── schemas.mts │ │ ├── templates │ │ │ └── react-typescript │ │ │ │ ├── .gitignore │ │ │ │ ├── index.html │ │ │ │ ├── package.json │ │ │ │ ├── postcss.config.js │ │ │ │ ├── src │ │ │ │ ├── App.tsx │ │ │ │ ├── index.css │ │ │ │ ├── main.tsx │ │ │ │ └── vite-env.d.ts │ │ │ │ ├── tailwind.config.js │ │ │ │ ├── tsconfig.json │ │ │ │ └── vite.config.ts │ │ └── utils.mts │ ├── config.mts │ ├── constants.mts │ ├── db │ │ ├── index.mts │ │ └── schema.mts │ ├── deps.mts │ ├── dev-server.mts │ ├── drizzle.config.ts │ ├── drizzle │ │ ├── 0000_initial.sql │ │ ├── 0001_favorite_language.sql │ │ ├── 0002_add-openai-key.sql │ │ ├── 0003_posthog_analytics.sql │ │ ├── 0004_add_ai_config.sql │ │ ├── 0005_new_provider_ai_models.sql │ │ ├── 0006_deprecate_ai_config.sql │ │ ├── 0007_add_subscription_email.sql │ │ ├── 0008_add_secrets.sql │ │ ├── 0009_secret_session_unique.sql │ │ ├── 0010_create_apps.sql │ │ ├── 0011_apps_external_id_unique.sql │ │ ├── 0011_remove_language_from_apps.sql │ │ ├── 0012_add_app_history.sql │ │ ├── 0013_add_x_ai.sql │ │ ├── 0014_Gemini_Integration.sql │ │ ├── 0015_add_custom_api_key.sql │ │ ├── 0016_add_openrouter_api_key.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ ├── 0001_snapshot.json │ │ │ ├── 0002_snapshot.json │ │ │ ├── 0003_snapshot.json │ │ │ ├── 0004_snapshot.json │ │ │ ├── 0005_snapshot.json │ │ │ ├── 0006_snapshot.json │ │ │ ├── 0007_snapshot.json │ │ │ ├── 0008_snapshot.json │ │ │ ├── 0009_snapshot.json │ │ │ ├── 0010_snapshot.json │ │ │ ├── 0011_snapshot.json │ │ │ ├── 0012_snapshot.json │ │ │ ├── 0013_snapshot.json │ │ │ ├── 0014_snapshot.json │ │ │ ├── 0015_snapshot.json │ │ │ ├── 0016_snapshot.json │ │ │ └── _journal.json │ ├── exec.mts │ ├── fs-utils.mts │ ├── index.mts │ ├── package.json │ ├── posthog-client.mts │ ├── processes.mts │ ├── prompts │ │ ├── app-builder.txt │ │ ├── app-editor.txt │ │ ├── cell-generator-javascript.txt │ │ ├── cell-generator-typescript.txt │ │ ├── code-updater-javascript.txt │ │ ├── code-updater-typescript.txt │ │ ├── fix-cell-diagnostics.txt │ │ └── srcbook-generator.txt │ ├── server │ │ ├── channels │ │ │ └── app.mts │ │ ├── http.mts │ │ ├── utils.mts │ │ ├── ws-client.mts │ │ └── ws.mts │ ├── session.mts │ ├── srcbook │ │ ├── config.mts │ │ ├── examples.mts │ │ ├── examples │ │ │ ├── getting-started.src.md │ │ │ ├── langgraph-web-agent.src.md │ │ │ └── websockets.src.md │ │ ├── index.mts │ │ └── path.mts │ ├── srcmd.mts │ ├── srcmd │ │ ├── decoding.mts │ │ ├── encoding.mts │ │ ├── paths.mts │ │ └── types.mts │ ├── test │ │ ├── app-parser.test.mts │ │ ├── plan-chunks-2.txt │ │ ├── plan-chunks.txt │ │ ├── plan-parser.test.mts │ │ ├── srcmd.test.mts │ │ ├── srcmd_files │ │ │ ├── mock_srcbook │ │ │ │ ├── README.md │ │ │ │ ├── package.json │ │ │ │ └── src │ │ │ │ │ └── foo.mjs │ │ │ └── srcbook.src.md │ │ ├── streaming-xml-parser.test.mts │ │ ├── tsserver.test.mts │ │ └── utils.mts │ ├── tsconfig.json │ ├── tsconfig.lint.json │ ├── tsserver │ │ ├── messages.mts │ │ ├── tsserver.mts │ │ ├── tsservers.mts │ │ └── utils.mts │ ├── tsservers.mts │ ├── types.mts │ ├── utils.mts │ ├── vite-env.d.ts │ └── vite.config.ts ├── components │ ├── .eslintrc.cjs │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── ai-generate-tips-dialog.tsx │ │ │ ├── cell-output.tsx │ │ │ ├── cells │ │ │ │ ├── code.tsx │ │ │ │ ├── markdown.tsx │ │ │ │ └── title.tsx │ │ │ ├── delete-cell-dialog.tsx │ │ │ ├── keyboard-shortcut.tsx │ │ │ ├── logos.tsx │ │ │ ├── ui │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── collapsible.tsx │ │ │ │ ├── command.tsx │ │ │ │ ├── context-menu.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── heading.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── navigation-menu.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── resizable.tsx │ │ │ │ ├── scroll-area.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── sheet.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── tabs.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ ├── tooltip.tsx │ │ │ │ └── underline-flat-tabs.tsx │ │ │ ├── use-cell.tsx │ │ │ └── use-theme.tsx │ │ ├── index.tsx │ │ ├── lib │ │ │ ├── code-theme.ts │ │ │ └── utils.ts │ │ ├── types.ts │ │ └── ui │ │ │ └── heading.tsx │ ├── tsconfig.json │ └── tsconfig.lint.json ├── configs │ ├── eslint │ │ ├── library.js │ │ └── react.js │ ├── package.json │ └── ts │ │ ├── base.json │ │ └── react-library.json ├── shared │ ├── .eslintrc.cjs │ ├── .npmignore │ ├── CHANGELOG.md │ ├── index.mts │ ├── package.json │ ├── src │ │ ├── ai.mts │ │ ├── schemas │ │ │ ├── apps.mts │ │ │ ├── cells.mts │ │ │ ├── files.mts │ │ │ ├── tsserver.mts │ │ │ └── websockets.mts │ │ ├── types │ │ │ ├── apps.mts │ │ │ ├── cells.mts │ │ │ ├── feedback.mts │ │ │ ├── history.mts │ │ │ ├── secrets.mts │ │ │ ├── tsserver.mts │ │ │ └── websockets.mts │ │ └── utils.mts │ ├── tsconfig.json │ └── tsconfig.lint.json └── web │ ├── .env.development │ ├── .env.production │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── components.json │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── favicon.ico │ ├── src │ ├── Layout.tsx │ ├── LayoutNavbar.tsx │ ├── clients │ │ ├── http │ │ │ └── apps.ts │ │ └── websocket │ │ │ ├── channel.ts │ │ │ ├── client.ts │ │ │ └── index.ts │ ├── components │ │ ├── ai-generate-tips-dialog.tsx │ │ ├── apps │ │ │ ├── AiFeedbackModal.tsx │ │ │ ├── bottom-drawer.tsx │ │ │ ├── create-modal.tsx │ │ │ ├── diff-modal.tsx │ │ │ ├── diff-stats.tsx │ │ │ ├── editor.tsx │ │ │ ├── header.tsx │ │ │ ├── lib │ │ │ │ ├── diff.ts │ │ │ │ ├── file-tree.ts │ │ │ │ └── path.ts │ │ │ ├── local-storage.ts │ │ │ ├── markdown.tsx │ │ │ ├── package-install-toast.tsx │ │ │ ├── panels │ │ │ │ ├── explorer.tsx │ │ │ │ └── settings.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── types.ts │ │ │ ├── use-app.tsx │ │ │ ├── use-files.tsx │ │ │ ├── use-logs.tsx │ │ │ ├── use-package-json.tsx │ │ │ ├── use-preview.tsx │ │ │ └── use-version.tsx │ │ ├── cells │ │ │ ├── code.tsx │ │ │ ├── generate-ai.tsx │ │ │ ├── get-completions.ts │ │ │ ├── hover.ts │ │ │ └── util.ts │ │ ├── chat.tsx │ │ ├── collapsible-container.tsx │ │ ├── delete-app-dialog.tsx │ │ ├── delete-cell-dialog.tsx │ │ ├── delete-srcbook-dialog.tsx │ │ ├── drag-and-drop-srcmd-modal.tsx │ │ ├── feedback-dialog.tsx │ │ ├── generate-srcbook-modal.tsx │ │ ├── import-export-srcbook-modal.tsx │ │ ├── install-package-modal.tsx │ │ ├── keyboard-shortcuts-dialog.tsx │ │ ├── logos.tsx │ │ ├── mailing-list-card.tsx │ │ ├── navbar.tsx │ │ ├── onboarding.tsx │ │ ├── session-menu │ │ │ ├── index.tsx │ │ │ ├── packages-panel.tsx │ │ │ ├── secrets-panel.tsx │ │ │ ├── settings-panel.tsx │ │ │ └── table-of-contents-panel.tsx │ │ ├── srcbook-cards.tsx │ │ ├── srcmd-upload-drop-zone.tsx │ │ ├── use-effect-once.tsx │ │ ├── use-package-json.tsx │ │ ├── use-settings.tsx │ │ └── use-tsconfig-json.tsx │ ├── config.ts │ ├── error.tsx │ ├── index.css │ ├── lib │ │ ├── environment.ts │ │ ├── file-system-access.ts │ │ ├── server.ts │ │ └── utils.ts │ ├── main.tsx │ ├── routes │ │ ├── apps │ │ │ ├── context.tsx │ │ │ ├── files-show.tsx │ │ │ ├── files.tsx │ │ │ ├── layout.tsx │ │ │ ├── loaders.tsx │ │ │ └── preview.tsx │ │ ├── home.tsx │ │ ├── secrets.tsx │ │ ├── session.tsx │ │ └── settings.tsx │ ├── types.ts │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.lint.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── srcbook ├── .eslintrc.cjs ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin │ └── cli.mts ├── package.json ├── public │ └── .gitkeep ├── src │ ├── cli.mts │ ├── server.mts │ └── utils.mts ├── tsconfig.json └── tsconfig.lint.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["@srcbook/configs"] 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | permissions: 13 | contents: write 14 | packages: write 15 | pull-requests: write 16 | name: Releasing 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Install PNPM 23 | uses: pnpm/action-setup@v4 24 | with: 25 | version: 9.12.1 26 | run_install: false 27 | 28 | - name: Setup Node 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 22 32 | 33 | - name: Install Dependencies 34 | run: pnpm i --frozen-lockfile 35 | 36 | - name: Build 37 | run: pnpm build 38 | 39 | - name: Create Release Pull Request or Publish to Github Registry 40 | uses: changesets/action@v1 41 | with: 42 | publish: pnpm ci:publish 43 | version: pnpm ci:version 44 | commit: 'chore: release package(s)' 45 | title: 'chore: release package(s)' 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test 12 | timeout-minutes: 10 13 | runs-on: ubuntu-latest 14 | # To use enable remote caching just set these env vars up 15 | # env: 16 | # TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 17 | # TURBO_TEAM: ${{ vars.TURBO_TEAM }} 18 | # TURBO_REMOTE_ONLY: true 19 | 20 | strategy: 21 | matrix: 22 | node-version: ['18.x', '22.x'] 23 | 24 | steps: 25 | - name: Check out code 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 2 29 | 30 | - uses: pnpm/action-setup@v3 31 | with: 32 | version: 9 33 | 34 | - name: Setup Node.js ${{ matrix.node-version }} 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: ${{ matrix.node-version }} 38 | cache: 'pnpm' 39 | 40 | - name: Install dependencies 41 | run: pnpm install 42 | 43 | - name: Build 44 | run: pnpm build 45 | 46 | - name: Lint & Check 47 | run: pnpm lint && pnpm check-format 48 | 49 | - name: Test 50 | run: pnpm test 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | srcbook-api-*.tgz 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | *.gitignored.* 29 | 30 | packages/api/.env 31 | packages/api/*.local 32 | 33 | srcbook/public/**/* 34 | !srcbook/public/.gitkeep 35 | 36 | srcbook/lib/**/* 37 | !srcbook/lib/.gitkeep 38 | 39 | # turbo 40 | **/.turbo 41 | 42 | # Aide 43 | *.code-workspace 44 | 45 | # Docs folder 46 | docs/ 47 | 48 | vite.config.ts.timestamp-*.mjs -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.7.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm*.yaml 2 | packages/api/drizzle/* 3 | packages/api/apps/templates/**/* 4 | **/*.src.md 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.0.1-alpha.18 4 | 5 | - Add TS-Server on-hover functionality to editor 6 | - Add output to package.json cell in settings [#205](https://github.com/srcbookdev/srcbook/issues/205) 7 | - Improved home page layout 8 | - Limit title length [#207](https://github.com/srcbookdev/srcbook/issues/207) 9 | - UI tweaks & minor bug fixes 10 | 11 | ### 0.0.1-alpha.17 12 | 13 | - Overhaul monorepo tooling [#201](https://github.com/srcbookdev/srcbook/pull/201) 14 | - Support Node.js 18 and above [#206](https://github.com/srcbookdev/srcbook/pull/206) 15 | - Fix multi-byte unicode character parsing in tsserver [#197](https://github.com/srcbookdev/srcbook/pull/197) 16 | - Support disabling analytics through env var [#191](https://github.com/srcbookdev/srcbook/pull/191) 17 | - Fix process not being properly shutdown when an uncaught exception occurs in the CLI [3615320](https://github.com/srcbookdev/srcbook/commit/361532004d6971b44b49cb884f2e36d11a564736) 18 | - UI tweaks and improvements 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.7.0-alpine3.20 2 | WORKDIR /app 3 | 4 | RUN corepack enable && corepack prepare pnpm@9.12.1 --activate 5 | 6 | # Copy all package files first 7 | COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ 8 | COPY packages packages/ 9 | COPY srcbook srcbook/ 10 | COPY turbo.json ./ 11 | 12 | # Install dependencies 13 | RUN pnpm install 14 | 15 | # Build the application 16 | RUN pnpm build 17 | 18 | # Create necessary directories for volumes 19 | RUN mkdir -p /root/.srcbook /root/.npm 20 | 21 | # Source code will be mounted at runtime 22 | CMD [ "pnpm", "start" ] 23 | 24 | EXPOSE 2150 -------------------------------------------------------------------------------- /PRIVACY-POLICY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | ## Introduction 4 | 5 | Srcbook is committed to protecting your privacy. This policy explains what data we collect, how we collect it, how we use it, and how you can opt-out. 6 | 7 | ## Data We Collect 8 | 9 | We collect behavioral data to understand usage patterns and improve our application. This data includes: 10 | 11 | - Usage metrics (e.g., feature interactions, usage frequency) 12 | - Performance metrics (e.g., load times, error rates) 13 | 14 | ## What We Do Not Collect 15 | 16 | We do not collect any personal information or personally identifiable information (PII). 17 | 18 | ## How We Use the Data 19 | 20 | The data collected is used to: 21 | 22 | - Improve the functionality and performance of Srcbook 23 | - Identify and address issues 24 | - Understand user preferences and usage patterns 25 | 26 | ## Opt-Out Option 27 | 28 | We respect your privacy and provide an option to opt-out of data collection. You can disable data collection at any time by going to the settings within the Srcbook application and toggling the data collection option off. 29 | 30 | ## Data Storage and Security 31 | 32 | All collected data is stored securely and is only accessible by the development team. 33 | 34 | ## Changes to This Policy 35 | 36 | We may update this policy from time to time. Any changes will be reflected in this document. 37 | 38 | ## Contact Us 39 | 40 | If you have any questions about this policy, please contact us at feedback@srcbook.com. 41 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | srcbook: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | ports: 8 | - '${HOST_BIND:-127.0.0.1}:2150:2150' 9 | volumes: 10 | - type: bind 11 | source: ~/.srcbook 12 | target: /root/.srcbook 13 | bind: 14 | create_host_path: true 15 | environment: 16 | - NODE_ENV=production 17 | - HOST=${HOST_BIND:-127.0.0.1} 18 | - SRCBOOK_INSTALL_DEPS=true 19 | command: ['pnpm', 'start'] 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "srcbook-monorepo", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "turbo dev", 8 | "build": "turbo build", 9 | "test": "turbo test", 10 | "lint": "turbo lint", 11 | "check-format": "prettier --check .", 12 | "format": "prettier --write .", 13 | "generate": "pnpm --filter api generate", 14 | "migrate": "pnpm --filter api migrate", 15 | "start": "turbo start", 16 | "ci:publish": "pnpm publish -r", 17 | "ci:version": "pnpm changeset version" 18 | }, 19 | "devDependencies": { 20 | "@changesets/cli": "^2.27.8", 21 | "@srcbook/configs": "workspace:^", 22 | "@types/node": "^20.14.2", 23 | "eslint": "^8.57.0", 24 | "prettier": "^3.3.3", 25 | "typescript": "5.6.2" 26 | }, 27 | "dependencies": { 28 | "turbo": "^2.1.1" 29 | }, 30 | "packageManager": "pnpm@9.12.1", 31 | "engines": { 32 | "node": ">=18" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/api/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: [require.resolve('@srcbook/configs/eslint/library.js')], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | project: './tsconfig.lint.json', 8 | tsconfigRootDir: __dirname, 9 | }, 10 | globals: { 11 | Bun: false, 12 | }, 13 | ignorePatterns: ['apps/templates/**/*'], 14 | }; 15 | -------------------------------------------------------------------------------- /packages/api/.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist-ssr 12 | *.local 13 | 14 | # Editor directories and files 15 | .vscode/* 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | *.gitignored.* 26 | -------------------------------------------------------------------------------- /packages/api/ai/app-parser.mts: -------------------------------------------------------------------------------- 1 | import { XMLParser } from 'fast-xml-parser'; 2 | 3 | // TODO reuse and cleanup types 4 | export interface FileContent { 5 | filename: string; 6 | content: string; 7 | } 8 | 9 | export type Project = { 10 | id: string; 11 | items: (File | Command)[]; 12 | }; 13 | 14 | type File = { 15 | type: 'file'; 16 | filename: string; 17 | content: string; 18 | }; 19 | 20 | type Command = { 21 | type: 'command'; 22 | content: string; 23 | }; 24 | 25 | type ParsedResult = { 26 | project: { 27 | '@_id': string; 28 | file?: { '@_filename': string; '#text': string }[] | { '@_filename': string; '#text': string }; 29 | command?: string[] | string; 30 | }; 31 | }; 32 | 33 | export function parseProjectXML(response: string): Project { 34 | try { 35 | const parser = new XMLParser({ 36 | ignoreAttributes: false, 37 | attributeNamePrefix: '@_', 38 | textNodeName: '#text', 39 | }); 40 | const result = parser.parse(response) as ParsedResult; 41 | 42 | if (!result.project) { 43 | throw new Error('Invalid response: missing project tag'); 44 | } 45 | 46 | const project: Project = { 47 | id: result.project['@_id'], 48 | items: [], 49 | }; 50 | 51 | const files = Array.isArray(result.project.file) 52 | ? result.project.file 53 | : [result.project.file].filter(Boolean); 54 | const commands = Array.isArray(result.project.command) 55 | ? result.project.command 56 | : [result.project.command].filter(Boolean); 57 | 58 | // TODO this ruins the order as it makes all the file changes first. 59 | // @FIXME: later 60 | for (const file of files) { 61 | if (file) { 62 | project.items.push({ 63 | type: 'file', 64 | filename: file['@_filename'], 65 | content: file['#text'], 66 | }); 67 | } 68 | } 69 | 70 | for (const command of commands) { 71 | if (command) { 72 | project.items.push({ 73 | type: 'command', 74 | content: command, 75 | }); 76 | } 77 | } 78 | 79 | return project; 80 | } catch (error) { 81 | console.error('Error parsing XML for the app:', error); 82 | throw new Error('Failed to parse XML response'); 83 | } 84 | } 85 | 86 | export function buildProjectXml(files: FileContent[], projectId: string): string { 87 | const fileXmls = files 88 | .map( 89 | (file) => ` 90 | 91 | 94 | `, 95 | ) 96 | .join('\n'); 97 | 98 | return ` 99 | 100 | ${fileXmls} 101 | 102 | `.trim(); 103 | } 104 | -------------------------------------------------------------------------------- /packages/api/ai/config.mts: -------------------------------------------------------------------------------- 1 | import { createOpenAI } from '@ai-sdk/openai'; 2 | import { createAnthropic } from '@ai-sdk/anthropic'; 3 | import { getConfig } from '../config.mjs'; 4 | import type { LanguageModel } from 'ai'; 5 | import { getDefaultModel, type AiProviderType } from '@srcbook/shared'; 6 | import { createGoogleGenerativeAI } from '@ai-sdk/google'; 7 | 8 | /** 9 | * Get the correct client and model configuration. 10 | * Throws an error if the given API key is not set in the settings. 11 | */ 12 | export async function getModel(): Promise { 13 | const config = await getConfig(); 14 | const { aiModel, aiProvider, aiBaseUrl } = config; 15 | const model = aiModel || getDefaultModel(aiProvider as AiProviderType); 16 | switch (aiProvider as AiProviderType) { 17 | case 'openai': 18 | if (!config.openaiKey) { 19 | throw new Error('OpenAI API key is not set'); 20 | } 21 | const openai = createOpenAI({ 22 | compatibility: 'strict', // strict mode, enabled when using the OpenAI API 23 | apiKey: config.openaiKey, 24 | }); 25 | return openai(model); 26 | 27 | case 'anthropic': 28 | if (!config.anthropicKey) { 29 | throw new Error('Anthropic API key is not set'); 30 | } 31 | const anthropic = createAnthropic({ apiKey: config.anthropicKey }); 32 | return anthropic(model); 33 | 34 | case 'Gemini': 35 | if (!config.geminiKey) { 36 | throw new Error('Gemini API key is not set'); 37 | } 38 | const google = createGoogleGenerativeAI({ apiKey: config.geminiKey }); 39 | return google(model) as LanguageModel; 40 | 41 | case 'Xai': 42 | if (!config.xaiKey) { 43 | throw new Error('Xai API key is not set'); 44 | } 45 | const xai = createOpenAI({ 46 | compatibility: 'compatible', 47 | baseURL: 'https://api.x.ai/v1', 48 | apiKey: config.xaiKey, 49 | }); 50 | return xai(model); 51 | 52 | case 'openrouter': 53 | if (!config.openrouterKey) { 54 | throw new Error('OpenRouter API key is not set'); 55 | } 56 | const openrouter = createOpenAI({ 57 | compatibility: 'compatible', 58 | baseURL: 'https://openrouter.ai/api/v1', 59 | apiKey: config.openrouterKey, 60 | }); 61 | return openrouter(model); 62 | 63 | case 'custom': 64 | if (typeof aiBaseUrl !== 'string') { 65 | throw new Error('Local AI base URL is not set'); 66 | } 67 | const openaiCompatible = createOpenAI({ 68 | compatibility: 'compatible', 69 | apiKey: config.customApiKey || 'bogus', // use custom API key if set, otherwise use a bogus key 70 | baseURL: aiBaseUrl, 71 | }); 72 | return openaiCompatible(model); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/api/ai/logger.mts: -------------------------------------------------------------------------------- 1 | export type AppGenerationLog = { 2 | appId: string; 3 | planId: string; 4 | llm_request: any; 5 | llm_response: any; 6 | }; 7 | 8 | /* 9 | * Log the LLM request / response to the analytics server. 10 | * For now this server is a custom implemention, consider moving to 11 | * a formal LLM log service, or a generic log hosting service. 12 | * In particular, this will not scale well when we split up app generation into 13 | * multiple steps. We will need spans/traces at that point. 14 | */ 15 | export async function logAppGeneration(log: AppGenerationLog): Promise { 16 | try { 17 | const response = await fetch('https://hub.srcbook.com/api/app_generation_log', { 18 | method: 'POST', 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | }, 22 | body: JSON.stringify(log), 23 | }); 24 | 25 | if (!response.ok) { 26 | console.error('Error sending app generation log'); 27 | } 28 | } catch (error) { 29 | console.error('Error sending app generation log:', error); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/api/apps/schemas.mts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | export const CreateAppSchema = z.object({ 4 | name: z.string(), 5 | prompt: z.string().optional(), 6 | }); 7 | 8 | export const CreateAppWithAiSchema = z.object({ 9 | name: z.string(), 10 | prompt: z.string(), 11 | }); 12 | 13 | export type CreateAppSchemaType = z.infer; 14 | export type CreateAppWithAiSchemaType = z.infer; 15 | -------------------------------------------------------------------------------- /packages/api/apps/templates/react-typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # typical gitignore for web apps 2 | node_modules 3 | dist 4 | .DS_Store -------------------------------------------------------------------------------- /packages/api/apps/templates/react-typescript/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/api/apps/templates/react-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-react-typescript-starter", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "lucide-react": "^0.453.0", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.3.1" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.3.6", 19 | "@types/react-dom": "^18.3.0", 20 | "@vitejs/plugin-react": "^4.3.1", 21 | "autoprefixer": "^10.4.20", 22 | "globals": "^15.9.0", 23 | "postcss": "^8.4.47", 24 | "tailwindcss": "^3.4.14", 25 | "typescript": "^5.5.3", 26 | "vite": "^5.4.6" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/api/apps/templates/react-typescript/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/api/apps/templates/react-typescript/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | 3 | function App() { 4 | 5 | return ( 6 |

7 | Hello world! 8 |

9 | ) 10 | } 11 | 12 | export default App 13 | -------------------------------------------------------------------------------- /packages/api/apps/templates/react-typescript/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 7 | } 8 | -------------------------------------------------------------------------------- /packages/api/apps/templates/react-typescript/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /packages/api/apps/templates/react-typescript/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/api/apps/templates/react-typescript/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /packages/api/apps/templates/react-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/api/apps/templates/react-typescript/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /packages/api/apps/utils.mts: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/vitejs/vite/tree/main/packages/create-vite 2 | export function toValidPackageName(projectName: string) { 3 | return projectName 4 | .trim() 5 | .toLowerCase() 6 | .replace(/\s+/g, '-') 7 | .replace(/^[._]/, '') 8 | .replace(/[^a-z\d\-~]+/g, '-'); 9 | } 10 | -------------------------------------------------------------------------------- /packages/api/constants.mts: -------------------------------------------------------------------------------- 1 | import os from 'node:os'; 2 | import path from 'node:path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | // When running Srcbook as an npx executable, the cwd is not reliable. 6 | // Commands that should be run from the root of the package, like npm scripts 7 | // should therefore use DIST_DIR as the cwd. 8 | const _filename = fileURLToPath(import.meta.url); 9 | const _dirname = path.dirname(_filename); 10 | 11 | export const HOME_DIR = os.homedir(); 12 | export const SRCBOOK_DIR = path.join(HOME_DIR, '.srcbook'); 13 | export const SRCBOOKS_DIR = path.join(SRCBOOK_DIR, 'srcbooks'); 14 | export const APPS_DIR = path.join(SRCBOOK_DIR, 'apps'); 15 | export const DIST_DIR = _dirname; 16 | export const PROMPTS_DIR = path.join(DIST_DIR, 'prompts'); 17 | export const IS_PRODUCTION = process.env.NODE_ENV === 'production'; 18 | -------------------------------------------------------------------------------- /packages/api/db/index.mts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { drizzle } from 'drizzle-orm/better-sqlite3'; 3 | import Database from 'better-sqlite3'; 4 | import * as schema from './schema.mjs'; 5 | import { migrate } from 'drizzle-orm/better-sqlite3/migrator'; 6 | import { HOME_DIR, DIST_DIR, SRCBOOKS_DIR } from '../constants.mjs'; 7 | import fs from 'node:fs'; 8 | 9 | // We can't use a relative directory for drizzle since this application 10 | // can get run from anywhere, so use DIST_DIR as ground truth. 11 | const drizzleFolder = path.join(DIST_DIR, 'drizzle'); 12 | 13 | const DB_PATH = `${HOME_DIR}/.srcbook/srcbook.db`; 14 | 15 | // Creates the HOME/.srcbook/srcbooks dir 16 | fs.mkdirSync(SRCBOOKS_DIR, { recursive: true }); 17 | 18 | export const db = drizzle(new Database(DB_PATH), { schema }); 19 | migrate(db, { migrationsFolder: drizzleFolder }); 20 | -------------------------------------------------------------------------------- /packages/api/db/schema.mts: -------------------------------------------------------------------------------- 1 | import { sql } from 'drizzle-orm'; 2 | import { sqliteTable, text, integer, unique } from 'drizzle-orm/sqlite-core'; 3 | import { randomid } from '@srcbook/shared'; 4 | 5 | export const configs = sqliteTable('config', { 6 | // Directory where .src.md files will be stored and searched by default. 7 | baseDir: text('base_dir').notNull(), 8 | defaultLanguage: text('default_language').notNull().default('typescript'), 9 | openaiKey: text('openai_api_key'), 10 | anthropicKey: text('anthropic_api_key'), 11 | xaiKey: text('xai_api_key'), 12 | geminiKey: text('gemini_api_key'), 13 | openrouterKey: text('openrouter_api_key'), 14 | customApiKey: text('custom_api_key'), 15 | // TODO: This is deprecated in favor of SRCBOOK_DISABLE_ANALYTICS env variable. Remove this. 16 | enabledAnalytics: integer('enabled_analytics', { mode: 'boolean' }).notNull().default(true), 17 | // Stable ID for posthog 18 | installId: text('srcbook_installation_id').notNull().default(randomid()), 19 | aiProvider: text('ai_provider').notNull().default('openai'), 20 | aiModel: text('ai_model').default('gpt-4o'), 21 | aiBaseUrl: text('ai_base_url'), 22 | // Null: unset. Email: subscribed. "dismissed": dismissed the dialog. 23 | subscriptionEmail: text('subscription_email'), 24 | }); 25 | 26 | export type Config = typeof configs.$inferSelect; 27 | 28 | export const secrets = sqliteTable('secrets', { 29 | id: integer('id').primaryKey(), 30 | name: text('name').notNull().unique(), 31 | value: text('value').notNull(), 32 | }); 33 | 34 | export type Secret = typeof secrets.$inferSelect; 35 | 36 | export const secretsToSession = sqliteTable( 37 | 'secrets_to_sessions', 38 | { 39 | id: integer('id').primaryKey(), 40 | session_id: text('session_id').notNull(), 41 | secret_id: integer('secret_id') 42 | .notNull() 43 | .references(() => secrets.id), 44 | }, 45 | (t) => ({ 46 | unique_session_secret: unique().on(t.session_id, t.secret_id), 47 | }), 48 | ); 49 | 50 | export type SecretsToSession = typeof secretsToSession.$inferSelect; 51 | 52 | export const apps = sqliteTable('apps', { 53 | id: integer('id').primaryKey(), 54 | name: text('name').notNull(), 55 | externalId: text('external_id').notNull().unique(), 56 | history: text('history').notNull().default('[]'), // JSON encoded value of the history 57 | historyVersion: integer('history_version').notNull().default(1), // internal versioning of history type for migrations 58 | createdAt: integer('created_at', { mode: 'timestamp' }) 59 | .notNull() 60 | .default(sql`(unixepoch())`), 61 | updatedAt: integer('updated_at', { mode: 'timestamp' }) 62 | .notNull() 63 | .default(sql`(unixepoch())`), 64 | }); 65 | 66 | export type App = typeof apps.$inferSelect; 67 | -------------------------------------------------------------------------------- /packages/api/deps.mts: -------------------------------------------------------------------------------- 1 | import Path from 'node:path'; 2 | import { exec } from 'node:child_process'; 3 | import { readFile } from './fs-utils.mjs'; 4 | import { DIST_DIR } from './constants.mjs'; 5 | 6 | export async function shouldNpmInstall(dirPath: string): Promise { 7 | const packageJsonPath = Path.resolve(Path.join(dirPath, 'package.json')); 8 | const packageLockPath = Path.resolve(Path.join(dirPath, 'package-lock.json')); 9 | 10 | const [packageJsonResult, packageLockResult] = await Promise.all([ 11 | readFile(packageJsonPath), 12 | readFile(packageLockPath), 13 | ]); 14 | 15 | if (!packageJsonResult.exists) { 16 | throw new Error(`No package.json was found in ${dirPath}`); 17 | } 18 | 19 | const pkgJson = JSON.parse(packageJsonResult.contents); 20 | const dependencies = Object.keys(pkgJson.dependencies || {}); 21 | const devDependencies = Object.keys(pkgJson.devDependencies || {}); 22 | 23 | // No dependencies == nothing to do 24 | if (dependencies.length === 0 && devDependencies.length === 0) { 25 | return false; 26 | } 27 | 28 | // Dependencies but no lock file == needs install 29 | if (!packageLockResult.exists) { 30 | return true; 31 | } 32 | 33 | const pkgLock = JSON.parse(packageLockResult.contents); 34 | const lockDependencies = pkgLock.packages['']?.dependencies || {}; 35 | const lockDevDependencies = pkgLock.packages['']?.devDependencies || {}; 36 | 37 | for (const dep of dependencies) { 38 | if (!Object.hasOwn(lockDependencies, dep)) { 39 | return true; 40 | } 41 | } 42 | 43 | for (const devDep of devDependencies) { 44 | if (!Object.hasOwn(lockDevDependencies, devDep)) { 45 | return true; 46 | } 47 | } 48 | 49 | return false; // All dependencies are installed 50 | } 51 | 52 | export async function missingUndeclaredDeps(dirPath: string): Promise { 53 | return new Promise((resolve) => { 54 | // Ignore the err argument because depcheck exists with a non zero code (255) when there are missing deps. 55 | exec( 56 | `npm run depcheck ${Path.resolve(dirPath)} -- --json`, 57 | { cwd: DIST_DIR }, 58 | (_err, stdout) => { 59 | const output = stdout || ''; 60 | 61 | // Use regex to extract JSON object 62 | const jsonMatch = output.match(/{.*}/s); 63 | if (!jsonMatch) { 64 | throw new Error('Failed to extract JSON from depcheck output.'); 65 | } 66 | 67 | // Parse the JSON 68 | const parsedResult = JSON.parse(jsonMatch[0]); 69 | 70 | // Process and return the data as needed 71 | resolve(Object.keys(parsedResult.missing)); 72 | }, 73 | ); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /packages/api/dev-server.mts: -------------------------------------------------------------------------------- 1 | import http from 'node:http'; 2 | import { WebSocketServer as WsWebSocketServer } from 'ws'; 3 | 4 | import app from './server/http.mjs'; 5 | import webSocketServer from './server/ws.mjs'; 6 | 7 | export { SRCBOOK_DIR } from './constants.mjs'; 8 | 9 | const server = http.createServer(app); 10 | 11 | const wss = new WsWebSocketServer({ server }); 12 | wss.on('connection', webSocketServer.onConnection); 13 | 14 | const port = process.env.PORT || 2150; 15 | server.listen(port, () => { 16 | console.log(`Server is running at http://localhost:${port}`); 17 | }); 18 | 19 | process.on('SIGINT', async function () { 20 | server.close(); 21 | process.exit(); 22 | }); 23 | if (import.meta.hot) { 24 | import.meta.hot.on('vite:beforeFullReload', () => { 25 | wss.close(); 26 | server.close(); 27 | }); 28 | 29 | import.meta.hot.dispose(() => { 30 | wss.close(); 31 | server.close(); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /packages/api/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'drizzle-kit'; 2 | import Path from 'node:path'; 3 | import { SRCBOOK_DIR } from './constants.mts'; 4 | 5 | export default defineConfig({ 6 | schema: './db/schema.mts', 7 | out: './drizzle', 8 | dialect: 'sqlite', 9 | dbCredentials: { 10 | url: Path.join(SRCBOOK_DIR, 'srcbook.db'), 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/api/drizzle/0000_initial.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `config` ( 2 | `baseDir` text NOT NULL 3 | ); 4 | --> statement-breakpoint 5 | CREATE TABLE `secrets` ( 6 | `id` integer PRIMARY KEY NOT NULL, 7 | `name` text NOT NULL, 8 | `value` text NOT NULL 9 | ); 10 | --> statement-breakpoint 11 | CREATE UNIQUE INDEX `secrets_name_unique` ON `secrets` (`name`); -------------------------------------------------------------------------------- /packages/api/drizzle/0001_favorite_language.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `config` RENAME COLUMN `baseDir` TO `base_dir`;--> statement-breakpoint 2 | ALTER TABLE `config` ADD `default_language` text DEFAULT 'typescript' NOT NULL; -------------------------------------------------------------------------------- /packages/api/drizzle/0002_add-openai-key.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `config` ADD `openai_api_key` text; -------------------------------------------------------------------------------- /packages/api/drizzle/0003_posthog_analytics.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `config` ADD `enabled_analytics` integer DEFAULT true NOT NULL;--> statement-breakpoint 2 | ALTER TABLE `config` ADD `srcbook_installation_id` text DEFAULT '18n70ookj0p2ht8c3aqfu2qjgo' NOT NULL; -------------------------------------------------------------------------------- /packages/api/drizzle/0004_add_ai_config.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `config` ADD `anthropic_api_key` text;--> statement-breakpoint 2 | ALTER TABLE `config` ADD `ai_config` text DEFAULT '{"provider":"openai","model":"gpt-4o"}' NOT NULL; 3 | -------------------------------------------------------------------------------- /packages/api/drizzle/0005_new_provider_ai_models.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `config` ADD `ai_provider` text DEFAULT 'openai' NOT NULL;--> statement-breakpoint 2 | ALTER TABLE `config` ADD `ai_model` text DEFAULT 'gpt-4o';--> statement-breakpoint 3 | ALTER TABLE `config` ADD `ai_base_url` text; 4 | -------------------------------------------------------------------------------- /packages/api/drizzle/0006_deprecate_ai_config.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `config` DROP COLUMN `ai_config`; 2 | -------------------------------------------------------------------------------- /packages/api/drizzle/0007_add_subscription_email.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `config` ADD `subscription_email` text; 2 | -------------------------------------------------------------------------------- /packages/api/drizzle/0008_add_secrets.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `secrets_to_sessions` ( 2 | `id` integer PRIMARY KEY NOT NULL, 3 | `session_id` text NOT NULL, 4 | `secret_id` integer NOT NULL, 5 | FOREIGN KEY (`secret_id`) REFERENCES `secrets`(`id`) ON UPDATE no action ON DELETE CASCADE 6 | ); 7 | -------------------------------------------------------------------------------- /packages/api/drizzle/0009_secret_session_unique.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX `secrets_to_sessions_session_id_secret_id_unique` ON `secrets_to_sessions` (`session_id`,`secret_id`); 2 | -------------------------------------------------------------------------------- /packages/api/drizzle/0010_create_apps.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `apps` ( 2 | `id` integer PRIMARY KEY NOT NULL, 3 | `name` text NOT NULL, 4 | `language` text NOT NULL, 5 | `external_id` text NOT NULL, 6 | `created_at` integer DEFAULT (unixepoch()) NOT NULL, 7 | `updated_at` integer DEFAULT (unixepoch()) NOT NULL 8 | ); 9 | -------------------------------------------------------------------------------- /packages/api/drizzle/0011_apps_external_id_unique.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX `apps_external_id_unique` ON `apps` (`external_id`); -------------------------------------------------------------------------------- /packages/api/drizzle/0011_remove_language_from_apps.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `apps` DROP COLUMN `language`; -------------------------------------------------------------------------------- /packages/api/drizzle/0012_add_app_history.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `apps` ADD `history` text DEFAULT '[]' NOT NULL;--> statement-breakpoint 2 | ALTER TABLE `apps` ADD `history_version` integer DEFAULT 1 NOT NULL; 3 | -------------------------------------------------------------------------------- /packages/api/drizzle/0013_add_x_ai.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `config` ADD `xai_api_key` text; -------------------------------------------------------------------------------- /packages/api/drizzle/0014_Gemini_Integration.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `config` ADD `gemini_api_key` text; -------------------------------------------------------------------------------- /packages/api/drizzle/0015_add_custom_api_key.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `config` ADD `custom_api_key` text; 2 | -------------------------------------------------------------------------------- /packages/api/drizzle/0016_add_openrouter_api_key.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `config` ADD `openrouter_api_key` text; 2 | -------------------------------------------------------------------------------- /packages/api/drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "03bcc665-983c-414c-827f-d69c497ea740", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "config": { 8 | "name": "config", 9 | "columns": { 10 | "baseDir": { 11 | "name": "baseDir", 12 | "type": "text", 13 | "primaryKey": false, 14 | "notNull": true, 15 | "autoincrement": false 16 | } 17 | }, 18 | "indexes": {}, 19 | "foreignKeys": {}, 20 | "compositePrimaryKeys": {}, 21 | "uniqueConstraints": {} 22 | }, 23 | "secrets": { 24 | "name": "secrets", 25 | "columns": { 26 | "id": { 27 | "name": "id", 28 | "type": "integer", 29 | "primaryKey": true, 30 | "notNull": true, 31 | "autoincrement": false 32 | }, 33 | "name": { 34 | "name": "name", 35 | "type": "text", 36 | "primaryKey": false, 37 | "notNull": true, 38 | "autoincrement": false 39 | }, 40 | "value": { 41 | "name": "value", 42 | "type": "text", 43 | "primaryKey": false, 44 | "notNull": true, 45 | "autoincrement": false 46 | } 47 | }, 48 | "indexes": { 49 | "secrets_name_unique": { 50 | "name": "secrets_name_unique", 51 | "columns": ["name"], 52 | "isUnique": true 53 | } 54 | }, 55 | "foreignKeys": {}, 56 | "compositePrimaryKeys": {}, 57 | "uniqueConstraints": {} 58 | } 59 | }, 60 | "enums": {}, 61 | "_meta": { 62 | "schemas": {}, 63 | "tables": {}, 64 | "columns": {} 65 | }, 66 | "internal": { 67 | "indexes": {} 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/api/drizzle/meta/0001_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "38f00b90-fd93-4268-8038-38f32c276138", 5 | "prevId": "03bcc665-983c-414c-827f-d69c497ea740", 6 | "tables": { 7 | "config": { 8 | "name": "config", 9 | "columns": { 10 | "base_dir": { 11 | "name": "base_dir", 12 | "type": "text", 13 | "primaryKey": false, 14 | "notNull": true, 15 | "autoincrement": false 16 | }, 17 | "default_language": { 18 | "name": "default_language", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false, 23 | "default": "'typescript'" 24 | } 25 | }, 26 | "indexes": {}, 27 | "foreignKeys": {}, 28 | "compositePrimaryKeys": {}, 29 | "uniqueConstraints": {} 30 | }, 31 | "secrets": { 32 | "name": "secrets", 33 | "columns": { 34 | "id": { 35 | "name": "id", 36 | "type": "integer", 37 | "primaryKey": true, 38 | "notNull": true, 39 | "autoincrement": false 40 | }, 41 | "name": { 42 | "name": "name", 43 | "type": "text", 44 | "primaryKey": false, 45 | "notNull": true, 46 | "autoincrement": false 47 | }, 48 | "value": { 49 | "name": "value", 50 | "type": "text", 51 | "primaryKey": false, 52 | "notNull": true, 53 | "autoincrement": false 54 | } 55 | }, 56 | "indexes": { 57 | "secrets_name_unique": { 58 | "name": "secrets_name_unique", 59 | "columns": ["name"], 60 | "isUnique": true 61 | } 62 | }, 63 | "foreignKeys": {}, 64 | "compositePrimaryKeys": {}, 65 | "uniqueConstraints": {} 66 | } 67 | }, 68 | "enums": {}, 69 | "_meta": { 70 | "schemas": {}, 71 | "tables": {}, 72 | "columns": { 73 | "\"config\".\"baseDir\"": "\"config\".\"base_dir\"" 74 | } 75 | }, 76 | "internal": { 77 | "indexes": {} 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/api/drizzle/meta/0002_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "fee9ddc3-ef67-45f3-a415-34fbb0b7779e", 5 | "prevId": "38f00b90-fd93-4268-8038-38f32c276138", 6 | "tables": { 7 | "config": { 8 | "name": "config", 9 | "columns": { 10 | "base_dir": { 11 | "name": "base_dir", 12 | "type": "text", 13 | "primaryKey": false, 14 | "notNull": true, 15 | "autoincrement": false 16 | }, 17 | "default_language": { 18 | "name": "default_language", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false, 23 | "default": "'typescript'" 24 | }, 25 | "openai_api_key": { 26 | "name": "openai_api_key", 27 | "type": "text", 28 | "primaryKey": false, 29 | "notNull": false, 30 | "autoincrement": false 31 | } 32 | }, 33 | "indexes": {}, 34 | "foreignKeys": {}, 35 | "compositePrimaryKeys": {}, 36 | "uniqueConstraints": {} 37 | }, 38 | "secrets": { 39 | "name": "secrets", 40 | "columns": { 41 | "id": { 42 | "name": "id", 43 | "type": "integer", 44 | "primaryKey": true, 45 | "notNull": true, 46 | "autoincrement": false 47 | }, 48 | "name": { 49 | "name": "name", 50 | "type": "text", 51 | "primaryKey": false, 52 | "notNull": true, 53 | "autoincrement": false 54 | }, 55 | "value": { 56 | "name": "value", 57 | "type": "text", 58 | "primaryKey": false, 59 | "notNull": true, 60 | "autoincrement": false 61 | } 62 | }, 63 | "indexes": { 64 | "secrets_name_unique": { 65 | "name": "secrets_name_unique", 66 | "columns": [ 67 | "name" 68 | ], 69 | "isUnique": true 70 | } 71 | }, 72 | "foreignKeys": {}, 73 | "compositePrimaryKeys": {}, 74 | "uniqueConstraints": {} 75 | } 76 | }, 77 | "enums": {}, 78 | "_meta": { 79 | "schemas": {}, 80 | "tables": {}, 81 | "columns": {} 82 | }, 83 | "internal": { 84 | "indexes": {} 85 | } 86 | } -------------------------------------------------------------------------------- /packages/api/drizzle/meta/0003_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "59446000-b2a1-442d-8cd1-560a6a8fa7bf", 5 | "prevId": "fee9ddc3-ef67-45f3-a415-34fbb0b7779e", 6 | "tables": { 7 | "config": { 8 | "name": "config", 9 | "columns": { 10 | "base_dir": { 11 | "name": "base_dir", 12 | "type": "text", 13 | "primaryKey": false, 14 | "notNull": true, 15 | "autoincrement": false 16 | }, 17 | "default_language": { 18 | "name": "default_language", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false, 23 | "default": "'typescript'" 24 | }, 25 | "openai_api_key": { 26 | "name": "openai_api_key", 27 | "type": "text", 28 | "primaryKey": false, 29 | "notNull": false, 30 | "autoincrement": false 31 | }, 32 | "enabled_analytics": { 33 | "name": "enabled_analytics", 34 | "type": "integer", 35 | "primaryKey": false, 36 | "notNull": true, 37 | "autoincrement": false, 38 | "default": true 39 | }, 40 | "srcbook_installation_id": { 41 | "name": "srcbook_installation_id", 42 | "type": "text", 43 | "primaryKey": false, 44 | "notNull": true, 45 | "autoincrement": false, 46 | "default": "'18n70ookj0p2ht8c3aqfu2qjgo'" 47 | } 48 | }, 49 | "indexes": {}, 50 | "foreignKeys": {}, 51 | "compositePrimaryKeys": {}, 52 | "uniqueConstraints": {} 53 | }, 54 | "secrets": { 55 | "name": "secrets", 56 | "columns": { 57 | "id": { 58 | "name": "id", 59 | "type": "integer", 60 | "primaryKey": true, 61 | "notNull": true, 62 | "autoincrement": false 63 | }, 64 | "name": { 65 | "name": "name", 66 | "type": "text", 67 | "primaryKey": false, 68 | "notNull": true, 69 | "autoincrement": false 70 | }, 71 | "value": { 72 | "name": "value", 73 | "type": "text", 74 | "primaryKey": false, 75 | "notNull": true, 76 | "autoincrement": false 77 | } 78 | }, 79 | "indexes": { 80 | "secrets_name_unique": { 81 | "name": "secrets_name_unique", 82 | "columns": [ 83 | "name" 84 | ], 85 | "isUnique": true 86 | } 87 | }, 88 | "foreignKeys": {}, 89 | "compositePrimaryKeys": {}, 90 | "uniqueConstraints": {} 91 | } 92 | }, 93 | "enums": {}, 94 | "_meta": { 95 | "schemas": {}, 96 | "tables": {}, 97 | "columns": {} 98 | }, 99 | "internal": { 100 | "indexes": {} 101 | } 102 | } -------------------------------------------------------------------------------- /packages/api/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1718315667825, 9 | "tag": "0000_initial", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "6", 15 | "when": 1718740690892, 16 | "tag": "0001_favorite_language", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "6", 22 | "when": 1720548526434, 23 | "tag": "0002_add-openai-key", 24 | "breakpoints": true 25 | }, 26 | { 27 | "idx": 3, 28 | "version": "6", 29 | "when": 1720719921668, 30 | "tag": "0003_posthog_analytics", 31 | "breakpoints": true 32 | }, 33 | { 34 | "idx": 4, 35 | "version": "6", 36 | "when": 1722982500003, 37 | "tag": "0004_add_ai_config", 38 | "breakpoints": true 39 | }, 40 | { 41 | "idx": 5, 42 | "version": "6", 43 | "when": 1723676786701, 44 | "tag": "0005_new_provider_ai_models", 45 | "breakpoints": true 46 | }, 47 | { 48 | "idx": 6, 49 | "version": "6", 50 | "when": 1723745583289, 51 | "tag": "0006_deprecate_ai_config", 52 | "breakpoints": true 53 | }, 54 | { 55 | "idx": 7, 56 | "version": "6", 57 | "when": 1725736805777, 58 | "tag": "0007_add_subscription_email", 59 | "breakpoints": true 60 | }, 61 | { 62 | "idx": 8, 63 | "version": "6", 64 | "when": 1726084159070, 65 | "tag": "0008_add_secrets", 66 | "breakpoints": true 67 | }, 68 | { 69 | "idx": 9, 70 | "version": "6", 71 | "when": 1726250539939, 72 | "tag": "0009_secret_session_unique", 73 | "breakpoints": true 74 | }, 75 | { 76 | "idx": 10, 77 | "version": "6", 78 | "when": 1726808187994, 79 | "tag": "0010_create_apps", 80 | "breakpoints": true 81 | }, 82 | { 83 | "idx": 11, 84 | "version": "6", 85 | "when": 1729112512747, 86 | "tag": "0011_remove_language_from_apps", 87 | "breakpoints": true 88 | }, 89 | { 90 | "idx": 12, 91 | "version": "6", 92 | "when": 1729193497907, 93 | "tag": "0012_add_app_history", 94 | "breakpoints": true 95 | }, 96 | { 97 | "idx": 13, 98 | "version": "6", 99 | "when": 1731347691803, 100 | "tag": "0013_add_x_ai", 101 | "breakpoints": true 102 | }, 103 | { 104 | "idx": 14, 105 | "version": "6", 106 | "when": 1732197490638, 107 | "tag": "0014_Gemini_Integration", 108 | "breakpoints": true 109 | }, 110 | { 111 | "idx": 15, 112 | "version": "6", 113 | "when": 1737324288698, 114 | "tag": "0015_add_custom_api_key", 115 | "breakpoints": true 116 | }, 117 | { 118 | "idx": 16, 119 | "version": "6", 120 | "when": 1743191674243, 121 | "tag": "0016_add_openrouter_api_key", 122 | "breakpoints": true 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /packages/api/fs-utils.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | 3 | export async function fileExists(filepath: string) { 4 | try { 5 | await fs.access(filepath, fs.constants.F_OK); 6 | return true; 7 | } catch (error) { 8 | return false; 9 | } 10 | } 11 | 12 | export async function directoryExists(dirpath: string) { 13 | try { 14 | const result = await fs.stat(dirpath); 15 | return result.isDirectory(); 16 | } catch (error) { 17 | return false; 18 | } 19 | } 20 | 21 | export async function readFile( 22 | path: string, 23 | ): Promise<{ exists: true; contents: string } | { exists: false }> { 24 | try { 25 | const contents = await fs.readFile(path, 'utf8'); 26 | return { exists: true, contents }; 27 | } catch (e) { 28 | const error = e as NodeJS.ErrnoException; 29 | if (error && error.code === 'ENOENT') { 30 | return { exists: false }; 31 | } 32 | throw error; 33 | } 34 | } 35 | 36 | export async function readdir( 37 | path: string, 38 | ): Promise<{ exists: true; files: string[] } | { exists: false }> { 39 | try { 40 | const files = await fs.readdir(path); 41 | return { exists: true, files }; 42 | } catch (e) { 43 | const error = e as NodeJS.ErrnoException; 44 | if (error && error.code === 'ENOENT') { 45 | return { exists: false }; 46 | } 47 | throw error; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/api/index.mts: -------------------------------------------------------------------------------- 1 | import app from './server/http.mjs'; 2 | import wss from './server/ws.mjs'; 3 | import { SRCBOOKS_DIR } from './constants.mjs'; 4 | import { posthog } from './posthog-client.mjs'; 5 | 6 | export { app, wss, SRCBOOKS_DIR, posthog }; 7 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@srcbook/api", 3 | "version": "0.0.18", 4 | "type": "module", 5 | "main": "./dist/index.mjs", 6 | "types": "./dist/index.d.mts", 7 | "files": [ 8 | "dist/**" 9 | ], 10 | "scripts": { 11 | "dev": "vite-node -w dev-server.mts", 12 | "test": "vitest", 13 | "prebuild": "rm -rf ./dist", 14 | "build": "tsc && cp -R ./drizzle ./dist/drizzle && cp -R ./srcbook/examples ./dist/srcbook/examples && cp -R ./prompts ./dist/prompts && cp -R ./apps/templates ./dist/apps/templates", 15 | "lint": "eslint . --max-warnings 0", 16 | "check-types": "tsc", 17 | "depcheck": "depcheck", 18 | "generate": "drizzle-kit generate", 19 | "migrate": "drizzle-kit migrate", 20 | "prepublishOnly": "tsc", 21 | "preversion": "vitest run && pnpm run build", 22 | "postversion": "git push && git push --tags" 23 | }, 24 | "dependencies": { 25 | "@ai-sdk/anthropic": "catalog:", 26 | "@ai-sdk/google": "^1.0.3", 27 | "@ai-sdk/openai": "catalog:", 28 | "@ai-sdk/provider": "^1.0.1", 29 | "@srcbook/shared": "workspace:^", 30 | "ai": "^3.4.33", 31 | "archiver": "^7.0.1", 32 | "better-sqlite3": "^11.3.0", 33 | "cors": "^2.8.5", 34 | "depcheck": "^1.4.7", 35 | "drizzle-orm": "^0.33.0", 36 | "express": "^4.20.0", 37 | "fast-xml-parser": "^4.5.0", 38 | "marked": "catalog:", 39 | "posthog-node": "^4.2.0", 40 | "simple-git": "^3.27.0", 41 | "ws": "catalog:", 42 | "zod": "catalog:" 43 | }, 44 | "devDependencies": { 45 | "@types/archiver": "^6.0.2", 46 | "@types/better-sqlite3": "^7.6.11", 47 | "@types/cors": "^2.8.17", 48 | "@types/express": "^4.17.21", 49 | "@types/ws": "^8.5.12", 50 | "drizzle-kit": "^0.24.2", 51 | "vite": "^5.4.4", 52 | "vite-node": "^2.0.5", 53 | "vitest": "^2.0.5" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/api/posthog-client.mts: -------------------------------------------------------------------------------- 1 | import { PostHog } from 'posthog-node'; 2 | import { getConfig } from './config.mjs'; 3 | import { IS_PRODUCTION } from './constants.mjs'; 4 | 5 | const POSTHOG_API_KEY = 'phc_bQjmPYXmbl76j8gW289Qj9XILuu1STRnIfgCSKlxdgu'; 6 | const POSTHOG_HOST = 'https://us.i.posthog.com'; 7 | 8 | type EventType = { 9 | event: string; 10 | properties?: Record; 11 | }; 12 | 13 | class PostHogClient { 14 | private installId: string; 15 | private client: PostHog; 16 | 17 | constructor(config: { installId: string }) { 18 | this.installId = config.installId; 19 | this.client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST }); 20 | } 21 | 22 | private get analyticsEnabled(): boolean { 23 | const disabled = process.env.SRCBOOK_DISABLE_ANALYTICS || ''; 24 | return disabled.toLowerCase() !== 'true'; 25 | } 26 | 27 | private get isEnabled(): boolean { 28 | return this.analyticsEnabled && IS_PRODUCTION; 29 | } 30 | 31 | public capture(event: EventType): void { 32 | if (!this.isEnabled) { 33 | return; 34 | } 35 | 36 | this.client.capture({ ...event, distinctId: this.installId }); 37 | } 38 | 39 | public shutdown() { 40 | this.client.shutdown(); 41 | } 42 | } 43 | 44 | export const posthog = new PostHogClient(await getConfig()); 45 | -------------------------------------------------------------------------------- /packages/api/processes.mts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from 'node:child_process'; 2 | 3 | export class Processes { 4 | private processes: Record = {}; 5 | 6 | add(sessionId: string, cellId: string, process: ChildProcess) { 7 | const key = this.toKey(sessionId, cellId); 8 | 9 | if (typeof process.pid !== 'number') { 10 | throw new Error('Cannot add a process with no pid'); 11 | } 12 | 13 | if (process.killed) { 14 | throw new Error('Cannot add a process that has been killed'); 15 | } 16 | 17 | this.processes[key] = process; 18 | 19 | process.on('exit', () => { 20 | delete this.processes[key]; 21 | }); 22 | } 23 | 24 | kill(sessionId: string, cellId: string) { 25 | const key = this.toKey(sessionId, cellId); 26 | 27 | const process = this.processes[key]; 28 | 29 | if (!process) { 30 | throw new Error( 31 | `Cannot kill process: no process for session ${sessionId} and cell ${cellId} exists`, 32 | ); 33 | } 34 | 35 | if (process.killed) { 36 | throw new Error( 37 | `Cannot kill process for session ${sessionId} and cell ${cellId}: process has already been killed`, 38 | ); 39 | } 40 | 41 | return process.kill('SIGTERM'); 42 | } 43 | 44 | private toKey(sessionId: string, cellId: string) { 45 | return sessionId + ':' + cellId; 46 | } 47 | } 48 | 49 | export default new Processes(); 50 | -------------------------------------------------------------------------------- /packages/api/prompts/fix-cell-diagnostics.txt: -------------------------------------------------------------------------------- 1 | You are tasked with suggesting a TypeScript diagnostics fix to a code block (or "cell") in a Srcbook. 2 | 3 | A Srcbook is a TypeScript notebook which follows a markdown-compatible format. 4 | 5 | The user is already working on an existing Srcbook, and the TypeScript linter has flagged an issue in one of the cells. 6 | 7 | You will be given: 8 | * the entire Srcbook as useful context, surrounded with "==== BEGIN SRCBOOK ====" and "==== END SRCBOOK ====". 9 | * the specific code cell that needs to be fixed, surrounded with "==== BEGIN CODE CELL ====" and "==== END CODE CELL ====". 10 | * the diagnostics output from tsserver, surrounded with "==== BEGIN DIAGNOSTICS ====" and "==== END DIAGNOSTICS ====". 11 | 12 | Your job is to fix the issues and suggest new code for the cell. Your response will be fed to a diffing algorithm against the original cell code, so you *have* to replace all of the code in the cell. 13 | ONLY RETURN THE CODE. NO PREAMBULE, NO BACKTICKS, NO MARKDOWN, NO SUFFIX, ONLY THE TYPESCRIPT CODE. 14 | -------------------------------------------------------------------------------- /packages/api/server/utils.mts: -------------------------------------------------------------------------------- 1 | import { ServerResponse } from 'node:http'; 2 | import { StreamToIterable } from '@srcbook/shared'; 3 | 4 | /** 5 | * Pipe a `ReadableStream` through a Node `ServerResponse` object. 6 | */ 7 | export async function streamJsonResponse( 8 | stream: ReadableStream, 9 | response: ServerResponse, 10 | options?: { 11 | headers?: Record; 12 | status?: number; 13 | }, 14 | ) { 15 | options ??= {}; 16 | 17 | response.writeHead(options.status || 200, { 18 | ...options.headers, 19 | 'Content-Type': 'text/plain', 20 | 'Transfer-Encoding': 'chunked', 21 | }); 22 | 23 | for await (const chunk of StreamToIterable(stream)) { 24 | response.write(chunk); 25 | } 26 | 27 | response.end(); 28 | } 29 | -------------------------------------------------------------------------------- /packages/api/srcbook/config.mts: -------------------------------------------------------------------------------- 1 | export function buildJSPackageJson() { 2 | return { 3 | type: 'module', 4 | devDependencies: { 5 | prettier: 'latest', 6 | }, 7 | prettier: { 8 | semi: true, 9 | singleQuote: true, 10 | }, 11 | }; 12 | } 13 | 14 | export function buildTSPackageJson() { 15 | return { 16 | type: 'module', 17 | dependencies: { 18 | tsx: 'latest', 19 | typescript: 'latest', 20 | '@types/node': 'latest', 21 | }, 22 | devDependencies: { 23 | prettier: 'latest', 24 | }, 25 | prettier: { 26 | semi: true, 27 | singleQuote: true, 28 | }, 29 | }; 30 | } 31 | 32 | export function buildTsconfigJson() { 33 | return { 34 | compilerOptions: { 35 | types: [], 36 | strict: true, 37 | module: 'nodenext', 38 | moduleResolution: 'nodenext', 39 | target: 'es2022', 40 | resolveJsonModule: true, 41 | noEmit: true, 42 | allowImportingTsExtensions: true, 43 | noPropertyAccessFromIndexSignature: true, 44 | }, 45 | include: ['src/**/*', 'env.d.ts'], 46 | exclude: ['node_modules'], 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /packages/api/srcbook/examples.mts: -------------------------------------------------------------------------------- 1 | import Path from 'node:path'; 2 | import { DIST_DIR, SRCBOOKS_DIR } from '../constants.mjs'; 3 | 4 | // TODO: Put this in a migration and move to sqlite? 5 | 6 | /////////////////////////////////////////////////////////// 7 | // Hardcoded ids so that we can easily track directories // 8 | /////////////////////////////////////////////////////////// 9 | 10 | const GETTING_STARTED_SRCBOOK = { 11 | id: '30v2av4eee17m59dg2c29758to', 12 | path: Path.join(DIST_DIR, 'srcbook', 'examples', 'getting-started.src.md'), 13 | title: 'Getting started', 14 | language: 'javascript', 15 | description: 'Quick tutorial to explore the basic concepts in Srcbooks.', 16 | tags: ['Srcbook', 'Learn'], 17 | get dirname() { 18 | return Path.join(SRCBOOKS_DIR, this.id); 19 | }, 20 | }; 21 | 22 | const LANGGRAPH_AGENT_SRCBOOK = { 23 | id: 'i72jjpkqepmg5olneffvk7hgto', 24 | path: Path.join(DIST_DIR, 'srcbook', 'examples', 'langgraph-web-agent.src.md'), 25 | title: 'LangGraph agent', 26 | description: 'Learn to write a stateful agent with memory using LangGraph and Tavily.', 27 | language: 'typescript', 28 | tags: ['AI', 'Web'], 29 | get dirname() { 30 | return Path.join(SRCBOOKS_DIR, this.id); 31 | }, 32 | }; 33 | 34 | const INTRO_TO_WEBSOCKETS_SRCBOOK = { 35 | id: 'vnovpn5dbrthpdllvoeqahufc4', 36 | path: Path.join(DIST_DIR, 'srcbook', 'examples', 'websockets.src.md'), 37 | title: 'Intro to WebSockets', 38 | language: 'javascript', 39 | description: 'Learn to build a simple WebSocket client and server in Node.js.', 40 | tags: ['Web', 'WebSockets'], 41 | get dirname() { 42 | return Path.join(SRCBOOKS_DIR, this.id); 43 | }, 44 | }; 45 | 46 | export const EXAMPLE_SRCBOOKS = [ 47 | GETTING_STARTED_SRCBOOK, 48 | LANGGRAPH_AGENT_SRCBOOK, 49 | INTRO_TO_WEBSOCKETS_SRCBOOK, 50 | ]; 51 | -------------------------------------------------------------------------------- /packages/api/srcbook/path.mts: -------------------------------------------------------------------------------- 1 | import Path from 'node:path'; 2 | import { SRCBOOKS_DIR } from '../constants.mjs'; 3 | 4 | export function pathToSrcbook(id: string) { 5 | return Path.join(SRCBOOKS_DIR, id); 6 | } 7 | 8 | export function pathToReadme(baseDir: string) { 9 | return Path.join(baseDir, 'README.md'); 10 | } 11 | 12 | export function pathToPackageJson(baseDir: string) { 13 | return Path.join(baseDir, 'package.json'); 14 | } 15 | 16 | export function pathToTsconfigJson(baseDir: string) { 17 | return Path.join(baseDir, 'tsconfig.json'); 18 | } 19 | 20 | export function pathToCodeFile(baseDir: string, filename: string) { 21 | return Path.join(baseDir, 'src', filename); 22 | } 23 | 24 | export function filenameFromPath(filePath: string) { 25 | return Path.basename(filePath); 26 | } 27 | -------------------------------------------------------------------------------- /packages/api/srcmd.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { 3 | pathToCodeFile, 4 | pathToPackageJson, 5 | pathToReadme, 6 | pathToTsconfigJson, 7 | } from './srcbook/path.mjs'; 8 | import type { DecodeResult } from './srcmd/types.mjs'; 9 | 10 | import { encode } from './srcmd/encoding.mjs'; 11 | import { decode, decodeCells } from './srcmd/decoding.mjs'; 12 | 13 | export { encode, decode, decodeCells }; 14 | 15 | /** 16 | * Decode a compatible directory into a set of cells. 17 | * 18 | * The directory must contain a README.md file and a package.json file. 19 | * We assume the README.md file contains the srcbook content, in particular 20 | * the first 2 cells should be a title cell and then a package.json.cell 21 | * 22 | * We leverage the decode() function first to decode the README.md file, and then 23 | * we replace the contents of the referenced package.json and code files into the cells. 24 | */ 25 | export async function decodeDir(dir: string): Promise { 26 | try { 27 | const readmePath = pathToReadme(dir); 28 | const readmeContents = await fs.readFile(readmePath, 'utf-8'); 29 | // Decode the README.md file into cells. 30 | // The code blocks and the package.json will only contain the filename at this point, 31 | // the actual source for each file will be read from the file system in the next step. 32 | const readmeResult = decode(readmeContents); 33 | 34 | if (readmeResult.error) { 35 | return readmeResult; 36 | } 37 | 38 | const srcbook = readmeResult.srcbook; 39 | 40 | const cells = srcbook.cells; 41 | const pendingFileReads: Promise[] = []; 42 | 43 | // Let's replace all the code cells with the actual file contents for each one 44 | for (const cell of cells) { 45 | if (cell.type === 'code' || cell.type === 'package.json') { 46 | const filePath = 47 | cell.type === 'package.json' 48 | ? pathToPackageJson(dir) 49 | : pathToCodeFile(dir, cell.filename); 50 | 51 | pendingFileReads.push( 52 | fs.readFile(filePath, 'utf-8').then((source) => { 53 | cell.source = source; 54 | }), 55 | ); 56 | } 57 | } 58 | 59 | // Wait for all file reads to complete 60 | await Promise.all(pendingFileReads); 61 | 62 | if (srcbook.language === 'typescript') { 63 | const tsconfig = await fs.readFile(pathToTsconfigJson(dir), 'utf8'); 64 | return { 65 | error: false, 66 | srcbook: { language: srcbook.language, cells, 'tsconfig.json': tsconfig }, 67 | }; 68 | } else { 69 | return { error: false, srcbook: { language: srcbook.language, cells } }; 70 | } 71 | } catch (e) { 72 | const error = e as unknown as Error; 73 | return { error: true, errors: [error.message] }; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/api/srcmd/paths.mts: -------------------------------------------------------------------------------- 1 | export function isSrcmdPath(path: string) { 2 | return path.endsWith('.src.md'); 3 | } 4 | -------------------------------------------------------------------------------- /packages/api/srcmd/types.mts: -------------------------------------------------------------------------------- 1 | import type { SessionType } from '../types.mjs'; 2 | 3 | export type SrcbookType = Pick; 4 | 5 | export type DecodeErrorResult = { 6 | error: true; 7 | errors: string[]; 8 | }; 9 | 10 | export type DecodeSuccessResult = { 11 | error: false; 12 | srcbook: SrcbookType; 13 | }; 14 | 15 | export type DecodeCellsSuccessResult = { 16 | error: false; 17 | srcbook: Pick; 18 | }; 19 | 20 | // This represents the result of decoding a complete .src.md file. 21 | export type DecodeResult = DecodeErrorResult | DecodeSuccessResult; 22 | 23 | // This represents the result of decoding a subset of content from a .src.md file. 24 | export type DecodeCellsResult = DecodeErrorResult | DecodeCellsSuccessResult; 25 | -------------------------------------------------------------------------------- /packages/api/test/app-parser.test.mts: -------------------------------------------------------------------------------- 1 | import { parseProjectXML } from '../ai/app-parser.mjs'; 2 | 3 | describe.skip('parseProjectXML', () => { 4 | it('should correctly parse XML and return a Project object', () => { 5 | const testXML = ` 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | `; 18 | 19 | const result = parseProjectXML(testXML); 20 | 21 | const expectedResult = { 22 | id: 'test-project', 23 | items: [ 24 | { type: 'file', filename: './test1.txt', content: 'Test content 1' }, 25 | { type: 'file', filename: './test2.txt', content: 'Test content 2' }, 26 | { type: 'command', content: 'npm install' }, 27 | ], 28 | }; 29 | 30 | expect(result).toEqual(expectedResult); 31 | }); 32 | 33 | it('should throw an error for invalid XML', () => { 34 | const invalidXML = 'XML'; 35 | 36 | expect(() => parseProjectXML(invalidXML)).toThrow('Failed to parse XML response'); 37 | }); 38 | 39 | it('should throw an error for XML without a project tag', () => { 40 | const noProjectXML = 'Content'; 41 | 42 | expect(() => parseProjectXML(noProjectXML)).toThrow('Failed to parse XML response'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/api/test/srcmd_files/mock_srcbook/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Srcbook 4 | 5 | ###### package.json 6 | 7 | [package.json](./package.json) 8 | 9 | With some words right behind it. 10 | 11 | ## Markdown cell 12 | 13 | With some **bold** text and some _italic_ text. 14 | 15 | > And a quote, why not! 16 | 17 | ###### foo.mjs 18 | 19 | [foo.mjs](./src/foo.mjs) 20 | 21 | ```json 22 | { "simple": "codeblock" } 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/api/test/srcmd_files/mock_srcbook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": {} 3 | } 4 | -------------------------------------------------------------------------------- /packages/api/test/srcmd_files/mock_srcbook/src/foo.mjs: -------------------------------------------------------------------------------- 1 | const foo = 42; 2 | export const bar = true; 3 | console.log(foo, bar); 4 | -------------------------------------------------------------------------------- /packages/api/test/srcmd_files/srcbook.src.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Srcbook title 4 | 5 | ###### package.json 6 | 7 | ```json 8 | { 9 | "dependencies": {} 10 | } 11 | ``` 12 | 13 | Opening paragraph here. 14 | 15 | ## Section h2 16 | 17 | Another paragraph. 18 | 19 | Followed by: 20 | 21 | 1. An 22 | 2. Ordered 23 | 3. List 24 | 25 | ###### index.mjs 26 | 27 | ```javascript 28 | // A code snippet here. 29 | export function add(a, b) { return a + b } 30 | ``` 31 | 32 | ## Another section 33 | 34 | Description goes here. `inline code` works. 35 | 36 | ```javascript 37 | // This will render as markdown, not a code cell. 38 | foo() + bar() 39 | ``` 40 | 41 | ###### foo.mjs 42 | 43 | ```javascript 44 | import {add} from './index.mjs'; 45 | const res = add(2, 3); 46 | console.log(res); 47 | ``` 48 | 49 | Paragraph here. 50 | -------------------------------------------------------------------------------- /packages/api/test/utils.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { fileURLToPath } from 'node:url'; 3 | import path from 'node:path'; 4 | 5 | export async function getRelativeFileContents(relativePath: string) { 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | return fs.readFile(path.join(__dirname, relativePath), { encoding: 'utf8' }); 9 | } 10 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@srcbook/configs/ts/base.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "allowImportingTsExtensions": false, 6 | "outDir": "dist", 7 | "types": ["vitest/globals", "vite/client"] 8 | }, 9 | "include": ["**/*.mts"], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/api/tsconfig.lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.mts", "vite.config.ts", "drizzle.config.ts", "vite-env.d.ts"], 3 | "exclude": ["node_modules", "dist"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/api/tsserver/tsservers.mts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import { TsServer } from './tsserver.mjs'; 3 | 4 | /** 5 | * This object is responsible for managing multiple tsserver instances. 6 | */ 7 | export class TsServers { 8 | private servers: Record = {}; 9 | 10 | get(id: string) { 11 | const server = this.servers[id]; 12 | 13 | if (!server) { 14 | throw new Error(`tsserver for ${id} does not exist.`); 15 | } 16 | 17 | return server; 18 | } 19 | 20 | set(id: string, server: TsServer) { 21 | if (this.servers[id]) { 22 | throw new Error(`tsserver for ${id} already exists.`); 23 | } 24 | 25 | this.servers[id] = server; 26 | } 27 | 28 | has(id: string) { 29 | return this.servers[id] !== undefined; 30 | } 31 | 32 | del(id: string) { 33 | delete this.servers[id]; 34 | } 35 | 36 | create(id: string, options: { cwd: string }) { 37 | if (this.has(id)) { 38 | throw new Error(`tsserver for ${id} already exists.`); 39 | } 40 | 41 | // This is using the TypeScript dependency in the user's Srcbook. 42 | // 43 | // Note: If a user creates a typescript Srcbook, when it is first 44 | // created, the dependencies are not installed and thus this will 45 | // shut down immediately. Make sure that we handle this case after 46 | // package.json has finished installing its deps. 47 | const child = spawn('npx', ['tsserver'], { 48 | cwd: options.cwd, 49 | }); 50 | 51 | const server = new TsServer(child); 52 | 53 | this.set(id, server); 54 | 55 | child.on('exit', () => { 56 | this.del(id); 57 | }); 58 | 59 | return server; 60 | } 61 | 62 | shutdown(id: string) { 63 | if (!this.has(id)) { 64 | console.warn(`tsserver for ${id} does not exist. Skipping shutdown.`); 65 | return; 66 | } 67 | 68 | // The server is removed from this.servers in the 69 | // process exit handler which covers all exit cases. 70 | return this.get(id).shutdown(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/api/tsserver/utils.mts: -------------------------------------------------------------------------------- 1 | import type { server as tsserver } from 'typescript'; 2 | import type { TsServerDiagnosticType } from '@srcbook/shared'; 3 | 4 | export function normalizeDiagnostic( 5 | diagnostic: tsserver.protocol.Diagnostic | tsserver.protocol.DiagnosticWithLinePosition, 6 | ): TsServerDiagnosticType { 7 | if (isDiagnosticWithLinePosition(diagnostic)) { 8 | return { 9 | code: diagnostic.code, 10 | category: diagnostic.category, 11 | text: diagnostic.message, 12 | start: diagnostic.startLocation, 13 | end: diagnostic.endLocation, 14 | }; 15 | } else { 16 | return { 17 | // From what I can tell, code should always be present despite the type. 18 | // If it's not, we use 1000 as the 'unknown' error code, which is not a 19 | // code defined in diagnosticMessages.json in TypeScript's source. 20 | code: diagnostic.code || 1000, 21 | category: diagnostic.category, 22 | text: diagnostic.text, 23 | start: diagnostic.start, 24 | end: diagnostic.end, 25 | }; 26 | } 27 | } 28 | 29 | // No elegant implementation for this. 30 | function isDiagnosticWithLinePosition( 31 | diagnostic: tsserver.protocol.Diagnostic | tsserver.protocol.DiagnosticWithLinePosition, 32 | ): diagnostic is tsserver.protocol.DiagnosticWithLinePosition { 33 | return 'startLocation' in diagnostic; 34 | } 35 | -------------------------------------------------------------------------------- /packages/api/tsservers.mts: -------------------------------------------------------------------------------- 1 | import { TsServers } from './tsserver/tsservers.mjs'; 2 | 3 | export default new TsServers(); 4 | -------------------------------------------------------------------------------- /packages/api/types.mts: -------------------------------------------------------------------------------- 1 | import type { CellType, CodeLanguageType } from '@srcbook/shared'; 2 | 3 | export type SessionType = { 4 | id: string; 5 | /** 6 | * Path to the directory containing the srcbook files. 7 | */ 8 | dir: string; 9 | cells: CellType[]; 10 | 11 | /** 12 | * The language of the srcbook, i.e.: 'typescript' or 'javascript' 13 | */ 14 | language: CodeLanguageType; 15 | 16 | /** 17 | * The tsconfig.json file contents. 18 | */ 19 | 'tsconfig.json'?: string; 20 | 21 | /** 22 | * Replace this with updatedAt once we store srcbooks in sqlite 23 | */ 24 | openedAt: number; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/api/utils.mts: -------------------------------------------------------------------------------- 1 | export function take(obj: T, ...keys: Array): Pick { 2 | const result = {} as Pick; 3 | 4 | for (const key of Object.keys(obj) as K[]) { 5 | if (keys.includes(key)) { 6 | result[key] = obj[key]; 7 | } 8 | } 9 | 10 | return result; 11 | } 12 | 13 | export function toFormattedJSON(o: any) { 14 | return JSON.stringify(o, null, 2); 15 | } 16 | -------------------------------------------------------------------------------- /packages/api/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/api/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | include: ['test/**/*.test.mts'], 7 | globals: true, 8 | }, 9 | server: { 10 | hmr: true, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/components/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: [require.resolve('@srcbook/configs/eslint/react.js')], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | project: './tsconfig.lint.json', 8 | tsconfigRootDir: __dirname, 9 | }, 10 | globals: { 11 | Bun: false, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/components/.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist-ssr 12 | *.local 13 | 14 | # Editor directories and files 15 | .vscode/* 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | *.gitignored.* 26 | -------------------------------------------------------------------------------- /packages/components/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @srcbook/components 2 | 3 | ## 0.0.7 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [a236470] 8 | - @srcbook/shared@0.0.13 9 | 10 | ## 0.0.6 11 | 12 | ### Patch Changes 13 | 14 | - 2e774d4: Support streaming of the responses! 15 | - Updated dependencies [727bab7] 16 | - Updated dependencies [2e774d4] 17 | - @srcbook/shared@0.0.12 18 | 19 | ## 0.0.5 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [b49bdf4] 24 | - Updated dependencies [bbbd5d6] 25 | - @srcbook/shared@0.0.11 26 | 27 | ## 0.0.4 28 | 29 | ### Patch Changes 30 | 31 | - ccd8d01: Introduce app builder 32 | - Updated dependencies [ccd8d01] 33 | - Updated dependencies [73cd6e8] 34 | - @srcbook/shared@0.0.10 35 | 36 | ## 0.0.3 37 | 38 | ### Patch Changes 39 | 40 | - 459b18d: Deploy all packages 41 | - Updated dependencies [459b18d] 42 | - @srcbook/shared@0.0.9 43 | 44 | ## 0.0.2 45 | 46 | ### Patch Changes 47 | 48 | - 24c841e: Update websocket client to pass context and connection 49 | - Updated dependencies [24c841e] 50 | - @srcbook/shared@0.0.8 51 | -------------------------------------------------------------------------------- /packages/components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@srcbook/components", 3 | "version": "0.0.7", 4 | "type": "module", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "prebuild": "rm -rf ./dist", 8 | "build": "tsc", 9 | "dev": "tsc --watch --project .", 10 | "lint": "eslint . --max-warnings 0", 11 | "check-types": "tsc", 12 | "prepublishOnly": "npm run build" 13 | }, 14 | "dependencies": { 15 | "@codemirror/autocomplete": "^6.18.1", 16 | "@codemirror/lang-javascript": "^6.2.2", 17 | "@codemirror/lang-markdown": "^6.2.5", 18 | "@codemirror/lint": "^6.8.1", 19 | "@codemirror/merge": "^6.7.0", 20 | "@codemirror/state": "^6.4.1", 21 | "@lezer/highlight": "^1.2.1", 22 | "@lezer/javascript": "^1.4.17", 23 | "@radix-ui/react-collapsible": "^1.1.0", 24 | "@radix-ui/react-context-menu": "^2.2.2", 25 | "@radix-ui/react-dialog": "^1.1.1", 26 | "@radix-ui/react-dropdown-menu": "^2.1.1", 27 | "@radix-ui/react-icons": "^1.3.0", 28 | "@radix-ui/react-navigation-menu": "^1.2.0", 29 | "@radix-ui/react-popover": "^1.1.1", 30 | "@radix-ui/react-scroll-area": "^1.2.0", 31 | "@radix-ui/react-select": "^2.1.1", 32 | "@radix-ui/react-slot": "^1.1.0", 33 | "@radix-ui/react-switch": "^1.1.0", 34 | "@radix-ui/react-tabs": "^1.1.0", 35 | "@radix-ui/react-tooltip": "^1.1.2", 36 | "@srcbook/shared": "workspace:^", 37 | "@uiw/codemirror-themes": "^4.23.2", 38 | "@uiw/react-codemirror": "^4.23.2", 39 | "class-variance-authority": "^0.7.0", 40 | "cmdk": "^1.0.0", 41 | "marked": "catalog:", 42 | "marked-react": "^2.0.0", 43 | "mermaid": "^11.2.0", 44 | "react-resizable-panels": "^2.1.2" 45 | }, 46 | "peerDependencies": { 47 | "@codemirror/autocomplete": "*", 48 | "@types/react": "*", 49 | "@uiw/react-codemirror": "*", 50 | "clsx": "*", 51 | "codemirror": "*", 52 | "lucide-react": "*", 53 | "react": "*", 54 | "react-hotkeys-hook": "*", 55 | "react-router-dom": "*", 56 | "react-textarea-autosize": "*", 57 | "sonner": "*", 58 | "tailwind-merge": "*", 59 | "use-debounce": "*" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/components/src/components/ai-generate-tips-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle } from './ui/dialog.js'; 3 | 4 | export default function AiGenerateTipsDialog({ children }: { children: React.ReactNode }) { 5 | const [open, setOpen] = useState(false); 6 | return ( 7 | 8 | {children} 9 | 10 | 11 | Prompt tips 12 |
13 |

Here are a few tips to get the AI to work well for you.

14 |
    15 |
  • The AI knows already knows about all of the contents of this notebook.
  • 16 |
  • It also knows what cell you're updating.
  • 17 |
  • You can ask the code to add or improve comments or jsdoc.
  • 18 |
  • You can ask the AI to refactor or rewrite the whole thing.
  • 19 |
  • 20 | Try asking the AI to refactor, improve or modularize your code, simply by asking for 21 | it. 22 |
  • 23 |
24 |
25 |
26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /packages/components/src/components/cells/title.tsx: -------------------------------------------------------------------------------- 1 | import { TitleCellType, TitleCellUpdateAttrsType } from '@srcbook/shared'; 2 | import { EditableH1 } from '../ui/heading.js'; 3 | 4 | type BaseProps = { 5 | cell: TitleCellType; 6 | }; 7 | type RegularProps = BaseProps & { 8 | readOnly?: false; 9 | updateCellOnClient: (cell: TitleCellType) => void; 10 | updateCellOnServer: (cell: TitleCellType, attrs: TitleCellUpdateAttrsType) => void; 11 | }; 12 | type ReadOnlyProps = BaseProps & { readOnly: true }; 13 | 14 | export default function TitleCell(props: RegularProps | ReadOnlyProps) { 15 | function updateCell(text: string) { 16 | if (props.readOnly) { 17 | return; 18 | } 19 | props.updateCellOnClient({ ...props.cell, text }); 20 | props.updateCellOnServer(props.cell, { text }); 21 | } 22 | 23 | return ( 24 |
25 | {props.readOnly ? ( 26 |

{props.cell.text}

27 | ) : ( 28 | 29 | )} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/components/src/components/delete-cell-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Button } from './ui/button.js'; 3 | import { 4 | Dialog, 5 | DialogTrigger, 6 | DialogContent, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogDescription, 10 | } from './ui/dialog.js'; 11 | 12 | export default function DeleteCellWithConfirmationModal({ 13 | onDeleteCell, 14 | children, 15 | }: { 16 | onDeleteCell: () => void; 17 | children: React.ReactNode; 18 | }) { 19 | const [open, setOpen] = useState(false); 20 | return ( 21 | 22 | {children} 23 | 24 | 25 | Delete this cell 26 | 27 | We currently don't support history, are you sure you want to delete it? 28 | 29 |
30 | 38 | 41 |
42 |
43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /packages/components/src/components/keyboard-shortcut.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Platform = 'mac' | 'windows' | 'linux' | 'other'; 4 | type KeyType = 'mod' | 'alt'; 5 | 6 | const getPlatform = () => { 7 | const platform = navigator.platform.toLowerCase(); 8 | const userAgent = navigator.userAgent.toLowerCase(); 9 | 10 | if (platform.includes('mac') || userAgent.includes('mac')) { 11 | return 'mac'; 12 | } else if (platform.includes('win') || userAgent.includes('win')) { 13 | return 'windows'; 14 | } else if (platform.includes('linux') || userAgent.includes('linux')) { 15 | return 'linux'; 16 | } else { 17 | return 'other'; 18 | } 19 | }; 20 | 21 | const keyMappings: Record> = { 22 | mod: { 23 | mac: '⌘', 24 | windows: 'Ctrl', 25 | linux: 'Ctrl', 26 | other: 'Ctrl', 27 | }, 28 | alt: { 29 | mac: '⌥', 30 | windows: 'Alt', 31 | linux: 'Alt', 32 | other: 'Alt', 33 | }, 34 | }; 35 | 36 | const getPlatformSpecificKey = (keyType: KeyType): string => { 37 | const platform = getPlatform(); 38 | return keyMappings[keyType][platform]; 39 | }; 40 | 41 | export default function Shortcut({ keys }: { keys: string[] }) { 42 | // Replace keys that are in the keyMappings with the platform specific key 43 | keys = keys.map((key) => { 44 | if (key === 'mod') { 45 | return getPlatformSpecificKey('mod'); 46 | } else if (key === 'alt') { 47 | return getPlatformSpecificKey('alt'); 48 | } else { 49 | return key; 50 | } 51 | }); 52 | return ( 53 | <> 54 | {keys.map((key) => { 55 | return ( 56 | 57 | 58 | {key} 59 | 60 | 61 | ); 62 | })} 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /packages/components/src/components/logos.tsx: -------------------------------------------------------------------------------- 1 | type PropsType = { 2 | size?: string | number; 3 | className?: string; 4 | }; 5 | 6 | export function PrettierLogo(props: PropsType) { 7 | return ( 8 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/components/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slot } from '@radix-ui/react-slot'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | 5 | import { cn } from '../../lib/utils.js'; 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-sm text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 active:translate-y-0.5', 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | 'bg-primary border border-primary text-primary-foreground hover:bg-primary-hover disabled:opacity-100 disabled:bg-muted disabled:text-muted-foreground disabled:border-muted-foreground', 14 | destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 15 | run: 'bg-run text-run-foreground border border-run hover:bg-sb-yellow-30', 16 | ai: 'bg-ai-btn text-sb-core-0 border-ai-btn hover:bg-ai-btn/90', 17 | 'ai-secondary': 18 | 'bg-secondary text-ai-ring border border-ai-ring hover:bg-muted hover:text-ai-foreground', 19 | secondary: 20 | 'bg-secondary text-secondary-foreground border border-border hover:bg-muted hover:text-secondary-hover', 21 | link: 'text-primary underline-offset-4 hover:underline', 22 | ghost: 'border border-transparent hover:bg-sb-core-20 dark:hover:bg-sb-core-110', 23 | icon: 'bg-transparent text-secondary-foreground hover:bg-muted', 24 | }, 25 | size: { 26 | default: 'h-8 px-3 py-2', 27 | 'default-with-icon': 'h-8 pl-[0.625rem] pr-3 py-2 gap-1.5', 28 | sm: 'h-6 rounded-sm px-3 text-xs', 29 | lg: 'rounded-md px-3 py-2', 30 | icon: 'h-8 w-8', 31 | }, 32 | }, 33 | defaultVariants: { 34 | variant: 'default', 35 | size: 'default', 36 | }, 37 | }, 38 | ); 39 | 40 | export interface ButtonProps 41 | extends React.ButtonHTMLAttributes, 42 | VariantProps { 43 | asChild?: boolean; 44 | } 45 | 46 | const Button = React.forwardRef( 47 | ({ className, variant, size, asChild = false, ...props }, ref) => { 48 | const Comp = asChild ? Slot : 'button'; 49 | return ( 50 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = 'Button'; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /packages/components/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '../../lib/utils.js'; 4 | 5 | const Card = React.forwardRef>( 6 | ({ className, ...props }, ref) => ( 7 |
12 | ), 13 | ); 14 | Card.displayName = 'Card'; 15 | 16 | const CardHeader = React.forwardRef>( 17 | ({ className, ...props }, ref) => ( 18 |
19 | ), 20 | ); 21 | CardHeader.displayName = 'CardHeader'; 22 | 23 | const CardTitle = React.forwardRef>( 24 | ({ className, ...props }, ref) => ( 25 | // eslint-disable-next-line jsx-a11y/heading-has-content 26 |

31 | ), 32 | ); 33 | CardTitle.displayName = 'CardTitle'; 34 | 35 | const CardDescription = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |

40 | )); 41 | CardDescription.displayName = 'CardDescription'; 42 | 43 | const CardContent = React.forwardRef>( 44 | ({ className, ...props }, ref) => ( 45 |

46 | ), 47 | ); 48 | CardContent.displayName = 'CardContent'; 49 | 50 | const CardFooter = React.forwardRef>( 51 | ({ className, ...props }, ref) => ( 52 |
53 | ), 54 | ); 55 | CardFooter.displayName = 'CardFooter'; 56 | 57 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; 58 | -------------------------------------------------------------------------------- /packages/components/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; 2 | 3 | const Collapsible = CollapsiblePrimitive.Root; 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 10 | -------------------------------------------------------------------------------- /packages/components/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '../../lib/utils.js'; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }, 19 | ); 20 | Input.displayName = 'Input'; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /packages/components/src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as PopoverPrimitive from '@radix-ui/react-popover'; 3 | 4 | import { cn } from '../../lib/utils.js'; 5 | 6 | const Popover = PopoverPrimitive.Root; 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger; 9 | 10 | const PopoverAnchor = PopoverPrimitive.Anchor; 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; 32 | -------------------------------------------------------------------------------- /packages/components/src/components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | import { DragHandleDots2Icon } from '@radix-ui/react-icons'; 2 | import * as ResizablePrimitive from 'react-resizable-panels'; 3 | 4 | import { cn } from '../../lib/utils.js'; 5 | 6 | const ResizablePanelGroup = ({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) => ( 10 | 14 | ); 15 | 16 | const ResizablePanel = ResizablePrimitive.Panel; 17 | 18 | const ResizableHandle = ({ 19 | withHandle, 20 | className, 21 | ...props 22 | }: React.ComponentProps & { 23 | withHandle?: boolean; 24 | }) => ( 25 | div]:rotate-90', 28 | className, 29 | )} 30 | {...props} 31 | > 32 | {withHandle && ( 33 |
34 | 35 |
36 | )} 37 |
38 | ); 39 | 40 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; 41 | -------------------------------------------------------------------------------- /packages/components/src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; 5 | 6 | import { cn } from '../../lib/utils.js'; 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )); 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = 'vertical', ...props }, ref) => ( 30 | 41 | 42 | 43 | )); 44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 45 | 46 | export { ScrollArea, ScrollBar }; 47 | -------------------------------------------------------------------------------- /packages/components/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster as Sonner } from 'sonner'; 2 | 3 | type ToasterProps = React.ComponentProps; 4 | 5 | const Toaster = ({ ...props }: ToasterProps) => { 6 | return ( 7 | 22 | ); 23 | }; 24 | 25 | export { Toaster }; 26 | -------------------------------------------------------------------------------- /packages/components/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as SwitchPrimitives from '@radix-ui/react-switch'; 3 | 4 | import { cn } from '../../lib/utils.js'; 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )); 25 | Switch.displayName = SwitchPrimitives.Root.displayName; 26 | 27 | export { Switch }; 28 | -------------------------------------------------------------------------------- /packages/components/src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as TabsPrimitive from '@radix-ui/react-tabs'; 3 | 4 | import { cn } from '../../lib/utils.js'; 5 | 6 | const Tabs = TabsPrimitive.Root; 7 | 8 | const TabsList = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | TabsList.displayName = TabsPrimitive.List.displayName; 22 | 23 | const TabsTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 35 | )); 36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 37 | 38 | const TabsContent = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 | 50 | )); 51 | TabsContent.displayName = TabsPrimitive.Content.displayName; 52 | 53 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 54 | -------------------------------------------------------------------------------- /packages/components/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '../../lib/utils.js'; 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.TextareaHTMLAttributes 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |