diff --git a/.github/workflows/npm-docker.yml b/.github/workflows/npm-docker.yml new file mode 100644 index 0000000..099be8c --- /dev/null +++ b/.github/workflows/npm-docker.yml @@ -0,0 +1,34 @@ +name: Build and Push NPM Docker Image + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.TEMP_DOCKER_USERNAME }} + password: ${{ secrets.TEMP_DOCKER_PAT }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: prompts/npm + platforms: linux/amd64,linux/arm64 + push: true + tags: vonwig/extractor-node:latest diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..9d87b3b --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v19 \ No newline at end of file diff --git a/prompts/Dockerfile b/prompts/Dockerfile index 8ee8c96..b7d25bf 100644 --- a/prompts/Dockerfile +++ b/prompts/Dockerfile @@ -1,6 +1,6 @@ FROM babashka/babashka:latest@sha256:4bc4beea38406782845ae8effaa9bd2f45345d46a4290ea4c96037970a0ca430 AS bb -FROM eclipse-temurin:latest@sha256:2e387a63a9086232a53fb668f78bcda1f233118f234326fcb88b0bb2a968ec39 AS deps +FROM eclipse-temurin:latest AS deps WORKDIR /app diff --git a/prompts/README.md b/prompts/README.md index 9020cbf..8c4ceeb 100644 --- a/prompts/README.md +++ b/prompts/README.md @@ -1,8 +1,8 @@ ## Building ```sh -#docker:command=build -docker build -t vonwig/prompts -f Dockerfile . +#docker:command=builds +docker build -t vonwig/prompts -f prompts/Dockerfile prompts ``` ```sh @@ -16,7 +16,7 @@ To run this project, use the following run command: ```sh #docker:command=run -docker run --rm -v /var/run/docker.sock:/var/run/docker.sock vonwig/prompts /Users/slim/docker/labs-make-runbook jimclark106 darwin npm +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock vonwig/prompts $PWD my_docker_username darwin npm ``` The four arguments are `project root dir`, `docker username`, `platform`, and a top-level prompt folder. diff --git a/prompts/npm/010_system_prompt.md b/prompts/npm/010_system_prompt.md index effa78e..3f36e47 100644 --- a/prompts/npm/010_system_prompt.md +++ b/prompts/npm/010_system_prompt.md @@ -1,47 +1,41 @@ -You are an assistant who specializes in making runbooks for NPM projects, -allowing any developer to quickly run a docker project locally for development. +You are an assistant who specializes in making runbooks for NPM projects, allowing any developer to quickly run a docker project locally for development. + Since you are an expert and know about their project, be definitive about recommendations. -A runbook for an npm project contains the following steps: +A runbook for an npm project contains the following: # Setup: + NVM: Check for NVM or install it with the system's package manager. + Example: +```sh + brew install nvm +``` -Node and NPM: - Prepare node using nvm, and select the correct package manager - -Run Package Manager: - Depending on npm vs yarn, run an install - -# Run: -Analyze package.json for scripts. +Then, for each node root, you need to do the following: +# Node Root -{{#project.node_roots}} - The project has a node root package.json at {{path}} with the contents {{content}}. - Because there is already a root, the project does not need to be converted to npm. -{{/project.node_roots}} -{{^project.node_roots}} - The project does not have a node root, so the user should run `npm init` -{{/project.node_roots}} +Add a block to cd into the node root +```sh +cd $node_root +``` +Node and NPM: + Prepare node using nvm, and select the correct package manager. + Example: + ```sh + nvm use 20 + ``` -{{#project.version_artifacts}} - The project has a version declaration file {{path}} with the contents {{content}} -{{/project.version_artifacts}} -{{^project.version_artifacts}} - The project does not have any version artifacts, so default to latest node LTS -{{/project.version_artifacts}} - -The user has the following top level project files: - -{{#project.files}} - {{.}} -{{/project.files}} - -If there is a yarn.lock, use `yarn` in place of npm commands. +Run Package Manager: + Depending on npm vs yarn, run an install + Example + ```sh + yarn install + ``` -Run `npm install` + Run scripts diff --git a/prompts/npm/020_user_prompt.md b/prompts/npm/020_user_prompt.md index 1eaf8c7..dc8066e 100644 --- a/prompts/npm/020_user_prompt.md +++ b/prompts/npm/020_user_prompt.md @@ -1 +1,26 @@ -I would like to run this node project. +I have the following project open: + +--- Project --- + +My project has these files: +{{#project.files}} + {{.}} +{{/project.files}} + +If you see that I have a yarn.lock, please use `yarn` in place of npm commands. + +{{#project.node_roots}} + --- Node Root --- + My project has a node root package.json at {{path}} and uses node version {{version}}. + The node root has the following scripts + {{scripts}} + ----------------- +{{/project.node_roots}} +{{^project.node_roots}} + The project does not have a node root, so help me run `npm init` +{{/project.node_roots}} + +Please generate a runbook for me. + + + diff --git a/prompts/npm/Dockerfile b/prompts/npm/Dockerfile index a271fbf..43653a5 100644 --- a/prompts/npm/Dockerfile +++ b/prompts/npm/Dockerfile @@ -1,15 +1,17 @@ -FROM ubuntu:20.04 +FROM alpine:3.20 -WORKDIR /app +# Get bash, jq, fdfind +RUN apk add --no-cache bash jq fd -RUN < /app/run.sh <<'EOF2' -#!/bin/bash +VOLUME /project -echo '{"project": {"node_roots": [{"path": "package.json", "content": "some stuff"}]}}' -EOF2 -chmod 755 /app/run.sh -EOF +WORKDIR / +COPY ./scripts/build-node-roots.sh /build-node-roots.sh + +# MAke the thing executable +RUN chmod +x build-node-roots.sh + +COPY ./scripts/payload.json /payload.json # when the container is running the project is mounted at /project read-only -ENTRYPOINT ["/app/run.sh"] +ENTRYPOINT ["/build-node-roots.sh"] diff --git a/prompts/npm/README.md b/prompts/npm/README.md index f1642ab..77a0099 100644 --- a/prompts/npm/README.md +++ b/prompts/npm/README.md @@ -8,7 +8,7 @@ extractors: - none - --workspace - /docker - - image: vonwig/node-extractor:latest + - image: vonwig/extractor-node:latest --- ## Description @@ -24,13 +24,13 @@ It relies on an image to extract some additional facts about the project ```sh #docker:command=build-npm-extractor -docker build -t vonwig/node-extractor -f ./npm/Dockerfile ./npm +docker build -t vonwig/extractor-node -f ./prompts/npm/Dockerfile ./prompts/npm ``` ## Running the extraction image ```sh #docker:command=run-npm-extractor -docker run --rm -v $PWD:/project:ro vonwig/node-extractor +docker run --rm -v $PWD:/project:ro vonwig/extractor-node ``` diff --git a/prompts/npm/scripts/build-node-roots.sh b/prompts/npm/scripts/build-node-roots.sh new file mode 100644 index 0000000..367eced --- /dev/null +++ b/prompts/npm/scripts/build-node-roots.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +function get_node_version() { + local DEFAULT_NODE_VERSION=20 + local NODE_VERSION=DEFAULT_NODE_VERSION + local NODE_VERSION_FILE=null + local NODE_VERSION_FILE_PATH=null + local NODE_VERSION_FILE_NAME=null + + if [ -f ".node-version" ]; then + NODE_VERSION_FILE=".node-version" + elif [ -f ".nvmrc" ]; then + NODE_VERSION_FILE=".nvmrc" + elif [ -f "package.json" ]; then + NODE_VERSION_FILE="package.json" + fi + + if [ $NODE_VERSION_FILE == "package.json" ]; then + NODE_VERSION=$(jq -r '.engines.node' "$NODE_VERSION_FILE") + else + NODE_VERSION=$(cat $NODE_VERSION_FILE) + fi + + NODE_VERSION_FILE_PATH=$(pwd) + NODE_VERSION_FILE_NAME=$NODE_VERSION_FILE + + if [ $NODE_VERSION == null ]; then + NODE_VERSION=DEFAULT_NODE_VERSION + NODE_VERSION_FILE=null + NODE_VERSION_FILE_PATH=null + fi + + # Strip non-numeric and non-dot characters + NODE_VERSION=$(echo $NODE_VERSION | tr -dc '0-9.') + + # Echo json payload + echo "{\"node_version\": \"$NODE_VERSION\", \"node_version_file\": \"$NODE_VERSION_FILE_NAME\", \"node_version_file_path\": \"$NODE_VERSION_FILE_PATH\"}" +} + + +PROJECT_DIR="/project" + +cd $PROJECT_DIR + +# If package.json at root level +if [ -f package.json ]; then + NODE_ROOTS="$PROJECT_DIR/package.json" +else + #TODO if a package.json found contains workspaces, ignore those roots + NODE_ROOTS=$(fd -d 3 package.json) # newline separated +fi + +PAYLOAD_NODE_ROOTS=() + +# CD into each node root +for NODE_ROOT in $NODE_ROOTS; do + root_dirname=$(dirname $NODE_ROOT) + root_dirname="$PROJECT_DIR/$root_dirname" + cd $root_dirname + # Version is json payload + node_root_version=$(get_node_version | tr -d '\n' | tr -d '\r') + + node_root_path=$root_dirname + + node_root_scripts=$(jq -r '.scripts' package.json) + # Append json payload + PAYLOAD_NODE_ROOTS+=("{\"node_root_path\": \"$node_root_path\", \"version\": $node_root_version, \"node_root_scripts\": $node_root_scripts},") +done + +# Remove trailing comma from last element +PAYLOAD_NODE_ROOTS[-1]=${PAYLOAD_NODE_ROOTS[-1]%?} + +# Echo project.node_roots json payload, comma separated +echo "{\"project\": {\"node_roots\": [${PAYLOAD_NODE_ROOTS[@]}]}}" diff --git a/prompts/npm/scripts/payload.json b/prompts/npm/scripts/payload.json new file mode 100644 index 0000000..a577378 --- /dev/null +++ b/prompts/npm/scripts/payload.json @@ -0,0 +1,45 @@ +{ + "project": { + "node_roots": [ + { + "path": "client/package.json", + "version": "18", + "scripts": { + "vscode:prepublish": "yarn run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "pretest": "yarn run compile && yarn run lint", + "lint": "eslint src --ext ts", + "test": "vscode-test", + "package": "vsce package" + } + }, + { + "path": "client/desktop-ui/package.json", + "version": "18", + "scripts": { + "vscode:prepublish": "yarn run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "pretest": "yarn run compile && yarn run lint", + "lint": "eslint src --ext ts", + "test": "vscode-test", + "package": "vsce package" + } + }, + { + "path": "client/desktop/package.json", + "version": "20", + "scripts": { + "vscode:prepublish": "yarn run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "pretest": "yarn run compile && yarn run lint", + "lint": "eslint src --ext ts", + "test": "vscode-test", + "package": "vsce package" + } + } + ] + } +} \ No newline at end of file diff --git a/prompts/readme/Dockerfile b/prompts/readme/Dockerfile new file mode 100644 index 0000000..aabf998 --- /dev/null +++ b/prompts/readme/Dockerfile @@ -0,0 +1 @@ +FROM alpine:3.20 \ No newline at end of file diff --git a/prompts/readme/README.md b/prompts/readme/README.md new file mode 100644 index 0000000..29ba5d0 --- /dev/null +++ b/prompts/readme/README.md @@ -0,0 +1,30 @@ +--- +extractors: + - image: vonwig/readme-extractor:latest + entrypoint: /project + - image: vonwig/extractor-node:latest +--- + +## Description + +The prompts for docker rely only on the classic lsp project extraction function. + +The output of running this container is a json document that will be merged into the +context that is provided to the moustache template based prompts. + +It relies on an image to extract some additional facts about the project + +## Building the extraction image + +```sh +#docker:command=build-npm-extractor +docker build -t vonwig/extractor-node -f ./npm/Dockerfile ./npm +``` + +## Running the extraction image + +```sh +#docker:command=run-npm-extractor +docker run --rm -v $PWD:/project:ro vonwig/extractor-node +``` + diff --git a/prompts/src/docker.clj b/prompts/src/docker.clj index c26c195..df8ed5a 100644 --- a/prompts/src/docker.clj +++ b/prompts/src/docker.clj @@ -77,7 +77,7 @@ (pprint (json/parse-string (extract-facts - {:image "vonwig/node-extractor:latest"} + {:image "vonwig/extractor-node:latest"} "/Users/slim/docker/labs-make-runbook") keyword)) ) diff --git a/src/commands/runHotCommand.ts b/src/commands/runHotCommand.ts index b040225..9151c65 100644 --- a/src/commands/runHotCommand.ts +++ b/src/commands/runHotCommand.ts @@ -1,52 +1,43 @@ import * as vscode from "vscode"; import { workspaceCommands } from "../extension"; -export const runHotCommand = async () => { - if (!vscode.workspace.workspaceFolders) { - return vscode.window.showErrorMessage("No workspace open."); - } - - let workspace = vscode.workspace.workspaceFolders[0]; - - if (vscode.window.activeTextEditor) { - workspace = vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri) || workspace; +const groupCommands = (blocks: (typeof workspaceCommands)[string]) => blocks.reduce((acc, { command, script }) => { + if (!acc[command]) { + acc[command] = ''; } + acc[command] += script; + return acc; +}, {} as Record); - const commands: { command: string; script: string }[] = - workspaceCommands[`docker-run-${workspace.uri.fsPath}`] || []; - - if (!commands || commands.length === 0) { - vscode.window.showErrorMessage("No commands bound to workspace"); - return; +export const runHotCommand = async () => { + const quickPicks = []; + if (Object.keys(workspaceCommands).length === 0) { + return vscode.window.showErrorMessage('No runbooks found in workspace'); } - - // Combines commands with mathcing names - const combinedCommands = commands.reduce( - (acc: { [key: string]: string }, { command, script }) => { - if (acc[command]) { - acc[command] += `\n${script}`; - } else { - acc[command] = script; - } - return acc; - }, - {} - ) as { [key: string]: string }; - - const quickPicks = Object.entries(combinedCommands).map( - ([tag, command]) => ({ - label: tag, - description: `${command.split(/\s+/).splice(0,3).join(" ")} ...`, - detail: command, - }) - ); + for (const [runbookFSPath, blocks] of Object.entries(workspaceCommands)) { + quickPicks.push({ + label: runbookFSPath, + description: ``, + detail: ``, + kind: vscode.QuickPickItemKind.Separator, + workspace: runbookFSPath, + }); + + const groupedBlocks = groupCommands(blocks); + + quickPicks.push(...Object.entries(groupedBlocks).map(([command, script]) => ({ + label: command, + detail: script, + workspace: runbookFSPath, + }))); + }; void vscode.window.showQuickPick(quickPicks).then((tag) => { if (!tag) { return; } - const terminalIdentifier = vscode.workspace.workspaceFolders!.length > 1 ? `[${workspace.name}]-${tag.label}` : tag.label; + const terminalIdentifier = `${tag.label}-${tag.workspace}`; const existingTerminal = vscode.window.terminals.find( terminal => terminal.name === terminalIdentifier diff --git a/src/extension.ts b/src/extension.ts index 3688e60..94030fc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -86,11 +86,9 @@ export async function activate(context: vscode.ExtensionContext) { }) => { const blocks = args.blocks; - const workspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.parse(args.uri)); + const runbookURI = vscode.Uri.parse(args.uri); - const id = `docker-run-${workspace?.uri.fsPath}`; - - workspaceCommands[id] = blocks; + workspaceCommands[runbookURI.fsPath] = blocks; }); dockerLSP.onNotification("$terminal/run", async (args: { content: string }) => {