diff --git a/.github/workflows/publish-vscode.yml b/.github/workflows/publish-vscode.yml new file mode 100644 index 0000000..a841bf7 --- /dev/null +++ b/.github/workflows/publish-vscode.yml @@ -0,0 +1,27 @@ +name: Publish VS Code extension + +# Manual, vscode-only publish — decoupled from the tag-driven npm release so it +# never tries to republish the @loop-lang/* libs (which fail on a duplicate +# version). Bump packages/vscode/package.json `version`, then run this from the +# Actions tab. Uses the same VSCE_PAT secret as release.yml. +on: + workflow_dispatch: + +jobs: + vscode: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run build # build all workspaces so parser/runtime/viz types exist + - run: npm run build -w loop-vscode # syncs the template library + bundles + - run: npx --yes @vscode/vsce@latest publish --no-dependencies + working-directory: packages/vscode + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} diff --git a/.gitignore b/.gitignore index b3b5114..855dc84 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ packages/cli/assets/ packages/cli/AGENTS.md packages/cli/.claude/ .vercel + +# Synced from the repo template library at build (single source of truth: /templates) +packages/vscode/templates/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ca8d943..403069b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,19 @@ Versions track the `@loop-lang/loop` installer package. - `demo.loop` — a copy-and-edit sample pipeline (plan → implement → ship) at the repo root for the README/tutorial. +### Changed +- **`loop-vscode` 0.4.0** — *New from template* now offers the full best-practice + library (`bugfix`, `feature`, `brownfield-feature`, `refactor`, `cicd-check`, + `security`, `clean-architecture`, `test-coverage`, `review-diff`, plus the + multi-file `greenfield-app`/`load-spec` flows), synced from the repo's + `templates/` at build (single source of truth) — replacing the stale + `bmad`/`foreach` bundles. Tutorial now states what the extension gives you and + lists the latest vocabulary it understands. *New Loop from template* is now + reachable from **File ▸ New File…** and an Explorer folder **right-click** (drops + the template straight into that folder), not just the Command Palette. +- A manual `Publish VS Code extension` workflow (`workflow_dispatch`) publishes + the extension on its own, decoupled from the tag-driven npm release. + ## [0.5.0] — 2026-06-28 > `@loop-lang/loop` 0.5.0 · `@loop-lang/{parser,runtime,stdlib,viz}` 0.2.0 diff --git a/docs/index.html b/docs/index.html index bdd4a9f..aa2068e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -737,12 +737,12 @@

Watch it run — live dashboard interm

The page streams over Server-Sent Events (each event carries an id; the server replays on connect and dedupes on reconnect), is self-contained — no external assets — and binds to 127.0.0.1 only.

In VS Code optional — for hand-authoring

-

Secondary to the chat — reach for the extension only when you want to write .loop files by hand in an editor.

+

Secondary to the chat — reach for the extension only when you want to write .loop files by hand in an editor. It turns a plain text file into a real authoring surface: it knows the grammar, catches a loop that can't finish before you run it, lets you launch a run without leaving the editor, and drops in a ready-made best-practice loop to start from. What you get:

Troubleshooting

diff --git a/packages/vscode/.vscodeignore b/packages/vscode/.vscodeignore index d0b63a7..06f23b3 100644 --- a/packages/vscode/.vscodeignore +++ b/packages/vscode/.vscodeignore @@ -1,5 +1,6 @@ src/** test/** +scripts/** .vscode/** tsconfig.json **/*.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index be75f08..091e2e8 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -1,7 +1,7 @@ { "name": "loop-vscode", "displayName": "LoopFlow", - "version": "0.3.0", + "version": "0.4.0", "description": "Syntax, formatting, best-practice templates, completion, and ▶ Run / Claude-session buttons for the LoopFlow (.loop) DSL.", "license": "Apache-2.0", "publisher": "loop-lang", @@ -73,9 +73,18 @@ }, { "command": "loop.newFromTemplate", - "title": "LoopFlow: New from template" + "title": "New Loop from template…", + "category": "LoopFlow" } ], + "menus": { + "file/newFile": [ + { "command": "loop.newFromTemplate", "group": "loop" } + ], + "explorer/context": [ + { "command": "loop.newFromTemplate", "when": "explorerResourceIsFolder", "group": "navigation@20" } + ] + }, "configuration": { "title": "LoopFlow", "properties": { @@ -114,7 +123,8 @@ }, "scripts": { "vscode:prepublish": "npm run build", - "build": "tsc -p tsconfig.json && npm run bundle", + "sync:templates": "node scripts/sync-templates.mjs", + "build": "npm run sync:templates && tsc -p tsconfig.json && npm run bundle", "bundle": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node --target=node18 --minify", "package": "npx --yes @vscode/vsce@latest package --no-dependencies", "test": "node --test test/*.test.mjs" diff --git a/packages/vscode/scripts/sync-templates.mjs b/packages/vscode/scripts/sync-templates.mjs new file mode 100644 index 0000000..5ffca32 --- /dev/null +++ b/packages/vscode/scripts/sync-templates.mjs @@ -0,0 +1,15 @@ +// Copy the repo's canonical template library into the extension so the .vsix +// ships it. Single source of truth = ../../../templates. Run in `build` before +// packaging. ponytail: a copy, not a registry — the picker's file list lives in +// src/extension.ts. +import { cp, rm } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const src = join(here, "..", "..", "..", "templates"); +const dest = join(here, "..", "templates"); + +await rm(dest, { recursive: true, force: true }); +await cp(src, dest, { recursive: true }); +console.log(`synced templates → ${dest}`); diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 4245756..1a6bcc3 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -26,7 +26,7 @@ export function activate(context: vscode.ExtensionContext) { if (!target) return void vscode.window.showErrorMessage("Loop: no .loop file to run."); runInSession(target); }), - vscode.commands.registerCommand("loop.newFromTemplate", () => newFromTemplate(context)) + vscode.commands.registerCommand("loop.newFromTemplate", (uri?: vscode.Uri) => newFromTemplate(context, uri)) ); // Live parse diagnostics (red squiggles) — debounced. @@ -47,45 +47,56 @@ export function activate(context: vscode.ExtensionContext) { export function deactivate() {} -async function newFromTemplate(context: vscode.ExtensionContext): Promise { +// The bundled best-practice library (synced from the repo's /templates at build). +// `deps` are the shared support files a multi-file flow needs alongside its entry. +interface Tmpl { file: string; label: string; blurb: string; deps?: string[] } +const TEMPLATES: Tmpl[] = [ + // Spec-driven — a whole app / a written backlog + { file: "greenfield-app.loop", label: "greenfield-app", blurb: "Build an app A→Z: discover → design → per-story", deps: ["discover.loop", "design.loop", "story-template.loop", "sprint.yaml", "plan.md"] }, + { file: "load-spec.loop", label: "load-spec", blurb: "Deliver an existing backlog story by story", deps: ["story-template.loop", "sprint.yaml", "plan.md"] }, + // Change — build / fix / restructure + { file: "feature.loop", label: "feature", blurb: "Ship one feature: build → regression → security → 👤 review" }, + { file: "brownfield-feature.loop", label: "brownfield-feature", blurb: "Add a feature to an existing codebase without breaking it" }, + { file: "bugfix.loop", label: "bugfix", blurb: "Fix one bug, proven by a named test" }, + { file: "refactor.loop", label: "refactor", blurb: "Improve structure, behavior unchanged" }, + // Quality gates — drive existing checks to green + { file: "cicd-check.loop", label: "cicd-check", blurb: "Make every CI check pass locally" }, + { file: "security.loop", label: "security", blurb: "Security pass before shipping: sast → deps → 👤 secrets" }, + { file: "clean-architecture.loop", label: "clean-architecture", blurb: "Enforce architecture boundaries — deps point inward" }, + { file: "test-coverage.loop", label: "test-coverage", blurb: "Raise coverage to a threshold with real tests" }, + { file: "review-diff.loop", label: "review-diff", blurb: "Review + clean the current branch diff" }, +]; + +async function newFromTemplate(context: vscode.ExtensionContext, folder?: vscode.Uri): Promise { const fs = await import("node:fs/promises"); const { join } = await import("node:path"); const templatesDir = join(context.extensionPath, "templates"); - let names: string[]; - try { - const ents = await fs.readdir(templatesDir, { withFileTypes: true }); - names = ents.filter((d) => d.isDirectory()).map((d) => d.name); - } catch { - vscode.window.showErrorMessage("Loop: no templates bundled with the extension."); - return; - } - if (names.length === 0) { vscode.window.showErrorMessage("Loop: no templates found."); return; } - - const blurbs: Record = { - foreach: "Generic: iterate a plan, run a checklist per item", - bmad: "BMAD A-to-Z: discover → design → per-story checklist", - }; const pick = await vscode.window.showQuickPick( - names.map((n) => ({ label: n, description: blurbs[n] ?? "" })), + TEMPLATES.map((t) => ({ label: t.label, description: t.blurb, tmpl: t })), { placeHolder: "Choose a Loop template" } ); if (!pick) return; - const ws = vscode.workspace.workspaceFolders?.[0]; - if (!ws) { vscode.window.showErrorMessage("Loop: open a folder first."); return; } - - const sub = await vscode.window.showInputBox({ - prompt: "Copy the template into which folder (relative to the workspace)?", - value: pick.label, - }); - if (sub === undefined) return; - - const srcDir = join(templatesDir, pick.label); - const destDir = join(ws.uri.fsPath, sub); + // Right-clicked an Explorer folder → drop it there, no prompt. Otherwise ask, + // relative to the workspace root. + let destDir: string; + if (folder) { + destDir = folder.fsPath; + } else { + const ws = vscode.workspace.workspaceFolders?.[0]; + if (!ws) { vscode.window.showErrorMessage("Loop: open a folder first."); return; } + const sub = await vscode.window.showInputBox({ + prompt: "Copy into which folder (relative to the workspace)?", + value: ".", + }); + if (sub === undefined) return; + destDir = join(ws.uri.fsPath, sub); + } await fs.mkdir(destDir, { recursive: true }); + const where = vscode.workspace.asRelativePath(destDir); - const files = await fs.readdir(srcDir); + const files = [pick.tmpl.file, ...(pick.tmpl.deps ?? [])]; let copied = 0; let skipped = 0; for (const f of files) { @@ -93,21 +104,22 @@ async function newFromTemplate(context: vscode.ExtensionContext): Promise let exists = false; try { await fs.access(dest); exists = true; } catch { /* missing → copy */ } if (exists) { - const ans = await vscode.window.showWarningMessage(`${f} already exists in ${sub}. Overwrite?`, "Overwrite", "Skip"); + const ans = await vscode.window.showWarningMessage(`${f} already exists in ${where}. Overwrite?`, "Overwrite", "Skip"); if (ans !== "Overwrite") { skipped++; continue; } } - await fs.copyFile(join(srcDir, f), dest); - copied++; + try { + await fs.copyFile(join(templatesDir, f), dest); + copied++; + } catch { + vscode.window.showWarningMessage(`Loop: template file "${f}" is missing from the extension bundle.`); + } } - vscode.window.showInformationMessage(`Loop: copied ${copied} file(s) to ${sub}${skipped ? `, skipped ${skipped}` : ""}.`); + vscode.window.showInformationMessage(`Loop: copied ${copied} file(s) to ${where}${skipped ? `, skipped ${skipped}` : ""}. Edit the # TODO lines before running.`); // Open the entry .loop so the user lands on the thing to run. - const entry = files.find((f) => /^(epic|deliver)\.loop$/.test(f)) ?? files.find((f) => f.endsWith(".loop")); - if (entry) { - const doc = await vscode.workspace.openTextDocument(join(destDir, entry)); - await vscode.window.showTextDocument(doc); - } + const doc = await vscode.workspace.openTextDocument(join(destDir, pick.tmpl.file)); + await vscode.window.showTextDocument(doc); } /** ▶ Run lens above every loop/pipeline definition. */ diff --git a/packages/vscode/templates/bmad/README.md b/packages/vscode/templates/bmad/README.md deleted file mode 100644 index 5030561..0000000 --- a/packages/vscode/templates/bmad/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# BMAD A-to-Z — discover → design → implement every story - -This bundle shows the full A-to-Z shape with BMAD as one example method. -The same pattern works with any method — BMAD is not special to the runtime. - -## Shape - -``` -discover (interview) → design (human approves) → for each story → story checklist -``` - -1. **discover** — the agent interviews you and writes `sprint.yaml` (done when the file exists). -2. **design** — the agent turns `sprint.yaml` into an architecture note; a human approves before stopping. -3. **for each story** — runs `story-template.loop` once per story in `sprint.yaml` (implement → security → manual QA). - -## Files - -| File | Purpose | -|---|---| -| `sprint.yaml` | The story list written by `discover`. Seed data is provided; `discover` overwrites it. | -| `discover.loop` | Interview loop — done when `sprint.yaml` exists. | -| `design.loop` | Architecture loop — done when `design.md` exists; human reviews before stop. | -| `story-template.loop` | The checklist every story runs through (implement → security → manual QA). | -| `epic.loop` | The full flow: discover → design → for each story → template. | - -## Run it - -```bash -/loopflow run epic.loop # inside Claude Code conversation -loop run epic.loop # via the CLI -``` - -## BMAD is one example - -`for each story in "sprint.yaml": run "story-template.loop"` is plain Loop syntax. -Replace the discover/design loops with your method's equivalents, or skip them -and point `for each` at any YAML list of work items. diff --git a/packages/vscode/templates/bmad/design.loop b/packages/vscode/templates/bmad/design.loop deleted file mode 100644 index ddee950..0000000 --- a/packages/vscode/templates/bmad/design.loop +++ /dev/null @@ -1,7 +0,0 @@ -loop "design": - goal: turn sprint.yaml into an architecture note the team approves - look at: sprint.yaml, docs, and the last failure - allow edits automatically - each cycle: plan, then act, then observe - done when "test -s design.md" passes - a human reviews before stopping diff --git a/packages/vscode/templates/bmad/discover.loop b/packages/vscode/templates/bmad/discover.loop deleted file mode 100644 index 7322b87..0000000 --- a/packages/vscode/templates/bmad/discover.loop +++ /dev/null @@ -1,9 +0,0 @@ -# Interactive discovery — run in conversation (/loopflow run). The agent interviews you -# and writes sprint.yaml. It is DONE when sprint.yaml exists (the artifact check), -# NOT when the chat goes quiet. Point the check at a real validator for stricter gating. -loop "discover": - goal: interview the team and capture the epic as a sprint.yaml with a stories list - look at: docs, and the last failure - allow edits automatically - each cycle: plan, then act, then observe - done when "test -s sprint.yaml" passes diff --git a/packages/vscode/templates/bmad/epic.loop b/packages/vscode/templates/bmad/epic.loop deleted file mode 100644 index eac8347..0000000 --- a/packages/vscode/templates/bmad/epic.loop +++ /dev/null @@ -1,7 +0,0 @@ -# A-to-Z, run in conversation: /loopflow run epic.loop -# discover (interview) -> design (approve) -> run the checklist for each story. -flow "epic: authentication": - run "discover.loop" - then run "design.loop" - then for each story in "sprint.yaml": - run "story-template.loop" diff --git a/packages/vscode/templates/bmad/sprint.yaml b/packages/vscode/templates/bmad/sprint.yaml deleted file mode 100644 index 9797166..0000000 --- a/packages/vscode/templates/bmad/sprint.yaml +++ /dev/null @@ -1,7 +0,0 @@ -stories: - - title: User can log in - acceptance: a registered user logs in with email + password - - title: User can sign up - acceptance: a new user creates an account and gets a verification email - - title: User can reset their password - acceptance: user requests a reset, gets a link, and sets a new password diff --git a/packages/vscode/templates/bmad/story-template.loop b/packages/vscode/templates/bmad/story-template.loop deleted file mode 100644 index 08edf42..0000000 --- a/packages/vscode/templates/bmad/story-template.loop +++ /dev/null @@ -1,21 +0,0 @@ -# Runs once per story in sprint.yaml. The current story text arrives as context. -pipeline "story": - - stage implement: - goal: implement the story described in the provided context - look at: src, and the last failure - allow edits automatically, but ask me before migrations or pushes - each cycle: plan, then act, then observe - done when "pnpm test" passes - when it fails: reflect on which layer broke, then plan again - after 6 tries: stop and warn "thrashing on this story" - - stage security: - goal: no high or critical vulnerabilities introduced - each cycle: plan, then act, then observe - done when "semgrep --config auto --severity=high" finds nothing - - stage manual: - goal: the story works end-to-end when a human tries it - each cycle: observe - a human reviews before stopping diff --git a/packages/vscode/templates/foreach/README.md b/packages/vscode/templates/foreach/README.md deleted file mode 100644 index b2c02b9..0000000 --- a/packages/vscode/templates/foreach/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# foreach — iterate a plan, run a template per item - -This bundle shows the core `for each` pattern: a list of work items lives in -`plan.yaml`; the same checklist (`item-template.loop`) runs once per item. - -## Files - -| File | Purpose | -|---|---| -| `plan.yaml` | The list of work items (a YAML list under `items:`). Edit this to change the work. | -| `item-template.loop` | The checklist every item runs through (implement → security → review). | -| `deliver.loop` | The flow: `for each item in "plan.yaml": run "item-template.loop"`. | - -## How `for each` works - -`for each in "":` reads the list from the YAML file and runs the -template once per entry. Each entry's text (title + detail) becomes the template's -context — what to build. If an item fails its checklist, the runtime pauses and -asks whether to continue with the next item or stop. - -## Commands - -```bash -loop parse deliver.loop # inspect the parsed AST -loop run deliver.loop # execute — one template run per item -``` diff --git a/packages/vscode/templates/foreach/deliver.loop b/packages/vscode/templates/foreach/deliver.loop deleted file mode 100644 index 9c3e262..0000000 --- a/packages/vscode/templates/foreach/deliver.loop +++ /dev/null @@ -1,5 +0,0 @@ -# The whole point of `for each`: run the same checklist for every item in a plan, -# without naming each one. Edit plan.yaml to change the work; this file stays put. -flow "deliver": - for each item in "plan.yaml": - run "item-template.loop" diff --git a/packages/vscode/templates/foreach/item-template.loop b/packages/vscode/templates/foreach/item-template.loop deleted file mode 100644 index f74632e..0000000 --- a/packages/vscode/templates/foreach/item-template.loop +++ /dev/null @@ -1,23 +0,0 @@ -# Runs once per item in plan.yaml. The current item's text arrives as context -# (what to build); the checks below are the SAME for every item. -pipeline "work item": - - stage implement: - goal: implement the work item described in the provided context - look at: src, and the last failure - allow edits automatically, but ask me before migrations or pushes - each cycle: plan, then act, then observe - done when "pnpm test" passes - when it fails: reflect on which layer broke, then plan again - after 6 tries: stop and warn "thrashing on this item" - - stage security: - goal: no high or critical vulnerabilities introduced - each cycle: plan, then act, then observe - done when "semgrep --config auto --severity=high" finds nothing - after 4 tries: stop and warn "security needs a human" - - stage review: - goal: lints clean and types check - each cycle: act, then observe - done when "pnpm lint && pnpm typecheck" passes diff --git a/packages/vscode/templates/foreach/plan.yaml b/packages/vscode/templates/foreach/plan.yaml deleted file mode 100644 index d6c865e..0000000 --- a/packages/vscode/templates/foreach/plan.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# A plain list of work items. `for each` runs the template once per entry; -# each entry's text becomes the template's context (what to build). -items: - - title: Parse the uploaded CSV into rows - detail: accept a multipart upload, stream-parse, reject malformed input - - title: Validate rows against the schema - detail: required fields, types, and sane per-row error messages - - title: Export the validated rows as JSON - detail: stable key order; gzip when the payload is large