diff --git a/scripts/create-basic.sh b/scripts/create-basic.sh
new file mode 100755
index 00000000..5a15b26b
--- /dev/null
+++ b/scripts/create-basic.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+# Create a minimal Lean template
+# usage: create-basic.sh WORK_DIR TEMPLATE_ID TOOLCHAIN
+#
+# example:
+# create-basic.sh /tmp/abcd new-template leanprover/lean4:v4.32.0
+#
+# expects $WORK_DIR/build/Main.lean must exist
+
+set -euo pipefail
+
+ROOT=${LEAN_WORKBENCH_DATA_DIR:?No data directory was specified}
+export ELAN_HOME="$ROOT/elan"
+export PATH="$ELAN_HOME/bin:$PATH"
+
+WORK_DIR="$1"; shift 1
+TEMPLATE_ID="$1"; shift 1
+TOOLCHAIN="$1"; shift 1
+
+trap 'rm -rf "$WORK_DIR"' EXIT
+
+if [ -d "$ROOT/templates/$TEMPLATE_ID" ]; then
+ echo "ERROR: template '$TEMPLATE_ID' already exists"
+ exit 1
+fi
+
+echo "[[ progress 1/4 Constructing project ]]"
+BUILD_DIR="$WORK_DIR/build"
+cd "$BUILD_DIR"
+
+echo "$TOOLCHAIN" > lean-toolchain
+
+cat > lakefile.toml < lean-toolchain
+
+cat > lakefile.toml < "$PACKAGE_SET_DIR/packages.txt"
+
+echo "[[ progress 7/8 Constructing template ]]"
+TEMPLATE_DIR="$WORK_DIR/template"
+mkdir -p "$TEMPLATE_DIR"
+
+mv "$BUILD_DIR/lean-toolchain" "$TEMPLATE_DIR/"
+mv "$BUILD_DIR/lakefile.toml" "$TEMPLATE_DIR/"
+mv "$BUILD_DIR/lake-manifest.json" "$TEMPLATE_DIR/"
+mv "$BUILD_DIR/Main.lean" "$TEMPLATE_DIR/"
+mv "$BUILD_DIR/metadata.json" "$TEMPLATE_DIR/"
+
+echo "[[ progress 8/8 Placing package set and template ]]"
+# NOTE: it's possible for the first placement to succeed and the second to fail;
+# the package set placement won't be rolled back if this happens.
+PACKAGE_SET_PLACED="$ROOT/package-sets/$TEMPLATE_ID"
+TEMPLATE_PLACED="$ROOT/templates/$TEMPLATE_ID"
+
+mv "$PACKAGE_SET_DIR" "$PACKAGE_SET_PLACED"
+mv "$TEMPLATE_DIR" "$TEMPLATE_PLACED"
+
+# --- Summary ---
+OLEAN_COUNT=$(find "$PACKAGE_SET_PLACED" -name "*.olean" | wc -l)
+TOTAL_SIZE=$(du -sh "$PACKAGE_SET_PLACED" | cut -f1)
+PKG_COUNT=$(wc -l < "$PACKAGE_SET_PLACED/packages.txt")
+
+echo ""
+echo "[create-template] Done."
+echo " Package set: $PACKAGE_SET_PLACED"
+echo " Template: $TEMPLATE_PLACED"
+echo " Packages: $PKG_COUNT"
+echo " .olean files: $OLEAN_COUNT"
+echo " Total size: $TOTAL_SIZE"
diff --git a/shared/shared.ts b/shared/shared.ts
index 5ff95e0a..9d09fef0 100644
--- a/shared/shared.ts
+++ b/shared/shared.ts
@@ -42,12 +42,59 @@ export const zTemplateId = z.string().regex(TEMPLATE_ID_RE, 'Invalid template ID
*/
export const EXPECTED_TOOLCHAIN_ID_RE = /^[a-z][a-z0-9:/_.-]*$/
+/**
+ * Expected form of a standard installed stable/beta/nightly toolchain.
+ * - If `match[1] === 'lean'`,
+ * then `match[2]` is a candidate for a tag of .
+ * - If `match[1] === 'lean4-nightly'`,
+ * then `match[2]` is a candidate for a tag of
+ */
+export const STANDARD_TOOLCHAIN_ID_RE = /^leanprover\/(lean4|lean4-nightly):([a-z0-9.-]+)$/
+
export const LEAN_STABLE_VERSION_RE = /^v4\.[0-9]+\.[0-9]+$/
export const LEAN_BETA_VERSION_RE = /^v4\.[0-9]+\.[0-9]+-rc[0-9]+$/
export const LEAN_NIGHTLY_VERSION_RE = /^nightly-[0-9-]+$/
/** Matches stable or beta Lean versions (not nightly) */
-export const LEAN_VERSION_RE = /^v4\.\d+\.\d+(-rc\d+)?$/
+export const LEAN_VERSION_RE = /^v4\.(\d+)\.(\d+)(-rc(\d+))?$/
+
+/**
+ * Compares two lean versions matching `LEAN_VERSION_RE`.
+ *
+ * ```
+ * leanVersionCompare("v4.1.3", "v4.32.2") < 0
+ * leanVersionCompare("v4.30.4", "v4.31.0-rc1") < 0
+ * leanVersionCompare("v4.31.1-rc10", "v4.31.0-rc9") > 0
+ * ```
+ */
+export function leanVersionCompare(v1: string, v2: string) {
+ const m1 = v1.match(LEAN_VERSION_RE)
+ const m2 = v2.match(LEAN_VERSION_RE)
+ if (!m1 || !m2) throw new Error(`Either ${v1} and/or ${v2} are not valid Lean version numbers`)
+ const [primary1, primary2] = [Number(m1[1]), Number(m2[1])]
+ if (primary1 !== primary2) return primary1 - primary2
+ const [secondary1, secondary2] = [Number(m1[2]), Number(m2[2])]
+ if (secondary1 !== secondary2) return secondary1 - secondary2
+ if (!m1[4]) return m2[4] ? 1 : 0
+ if (!m2[4]) return -1
+ return Number(m1[4]) - Number(m2[4])
+}
+
+/**
+ * Does a toolchain match STANDARD_TOOLCHAIN_ID_RE and do Lean, Mathlib, and CSLib
+ * work with the lean module system at that version?
+ *
+ * For stable releases, returns true for v4.27.0 and beyond.
+ * For nighties, very conservatively returns true in February 2026 and beyond.
+ */
+export function toolchainHasModules(toolchain: string) {
+ const m = toolchain.match(STANDARD_TOOLCHAIN_ID_RE)
+ if (!m) return false
+ if (m[1] === 'lean4') {
+ return LEAN_VERSION_RE.test(m[2]!) && leanVersionCompare(m[2]!, 'v4.27.0') >= 0
+ }
+ return LEAN_NIGHTLY_VERSION_RE.test(m[2]!) && m[2]! >= 'nightly-2026-02-01'
+}
/** Metadata of a Lean Workbench project workspace. */
export type WorkspaceMetadata = z.infer
diff --git a/src/app/admin/actions.ts b/src/app/admin/actions.ts
index 54833879..dc1469df 100644
--- a/src/app/admin/actions.ts
+++ b/src/app/admin/actions.ts
@@ -8,6 +8,7 @@ import {
LEAN_BETA_VERSION_RE,
LEAN_NIGHTLY_VERSION_RE,
LEAN_STABLE_VERSION_RE,
+ STANDARD_TOOLCHAIN_ID_RE,
zProjectId,
zTemplateId,
zUserId,
@@ -22,7 +23,13 @@ import { getConfig, saveConfig, zGithubAuthConfig } from '@/lib/server/config'
import { getDb } from '@/lib/server/db'
import { getEditorSessionManager } from '@/lib/server/editorSessions'
import { elanUninstall, startElanInstall } from '@/lib/server/elan'
-import { readTemplateMetadata, saveTemplateMetadata, type TemplateMetadata } from '@/lib/server/projectTemplate'
+import {
+ getAvailableTemplateSchemas,
+ readTemplateMetadata,
+ saveTemplateMetadata,
+ startSchemaTemplate,
+ type TemplateMetadata,
+} from '@/lib/server/projectTemplate'
import { getTrackedCommandState } from '@/lib/server/trackedCommand'
import { serverAction, submitAction } from '@/lib/server/util'
import { type ActionResponse } from '@/lib/util'
@@ -279,6 +286,32 @@ export const editTemplateMetadata = submitAction(
},
)
+export async function availableTemplateSchemas(toolchain: string) {
+ await requireAdmin()
+ return getAvailableTemplateSchemas(toolchain)
+}
+
+const zTemplateCreation = z.object({
+ toolchain: z.string().regex(STANDARD_TOOLCHAIN_ID_RE),
+ schema: z.enum(['basic', 'mathlib', 'cslib']),
+})
+
+export const doTemplateCreation = submitAction(
+ zTemplateCreation,
+ async ({ toolchain, schema }): Promise> => {
+ await requireAdmin()
+
+ try {
+ const emitter = await startSchemaTemplate(toolchain, schema)
+ return { ok: !!emitter }
+ } catch (e) {
+ return { error: e instanceof Error ? e.message : String(e) }
+ }
+ },
+)
+
+// -- Tracked command infrastructure
+
export async function isTrackedCommandRunning(key: string) {
await requireAdmin()
return getTrackedCommandState(key)?.status === 'running'
diff --git a/src/app/admin/components/TemplateManagement.tsx b/src/app/admin/components/TemplateManagement.tsx
index fcd7cf1e..23f9aa23 100644
--- a/src/app/admin/components/TemplateManagement.tsx
+++ b/src/app/admin/components/TemplateManagement.tsx
@@ -1,32 +1,47 @@
'use client'
+import { STANDARD_TOOLCHAIN_ID_RE, toolchainHasModules } from '@leanprover/workbench-shared'
+import { useRouter } from 'next/navigation'
import { use, useState } from 'react'
-import { editTemplateMetadata } from '@/app/admin/actions'
+import { availableTemplateSchemas, doTemplateCreation, editTemplateMetadata } from '@/app/admin/actions'
import CatchySuspense from '@/app/components/CatchySuspense'
-import { useServerAction } from '@/lib/client/util'
+import TrackedCommandForm from '@/app/components/TrackedCommandForm'
+import { useServerAction, useThrowingSWR } from '@/lib/client/util'
import { type TemplateInfo } from '@/lib/server/projectTemplate'
interface TemplateManagementProps {
- templates: Promise
+ templatesPromise: Promise
+ installedToolchainsPromise: Promise
}
export function TemplateManagement(props: TemplateManagementProps) {
+ const router = useRouter()
+ const templates = use(props.templatesPromise)
+ const installedStandardToolchains = use(props.installedToolchainsPromise).filter(tc => toolchainHasModules(tc))
+
return (
-
- Project Templates
-
-
-
-
+ <>
+
+ router.refresh()}
+ >
+ Loading available toolchains…
}>
+
+
+
+ >
)
}
-function TemplateManagementList(props: TemplateManagementProps) {
- const templates = use(props.templates)
+function TemplateManagementList(props: { templates: TemplateInfo[] }) {
return (
- {templates.map(template => (
+ {props.templates.map(template => (
))}
@@ -106,3 +121,57 @@ function TemplateRow(props: TemplateInfo) {
)
}
+
+export function TemplateCreationForm(props: { installedToolchains: string[] }) {
+ const [toolchain, setToolchain] = useState(props.installedToolchains[0]!)
+ const [_toolchain, namespace, tag] = toolchain.match(STANDARD_TOOLCHAIN_ID_RE)!
+ const { data: schemas } = useThrowingSWR(
+ `toolchain-schema-${namespace}-${tag}`,
+ async () => {
+ const schemaIds = await availableTemplateSchemas(toolchain)
+ return schemaIds.map(key => {
+ switch (key) {
+ case 'basic':
+ return { key, name: 'Basic Lean template' }
+ case 'mathlib':
+ return { key, name: 'Mathlib template' }
+ case 'cslib':
+ return { key, name: 'CSLib template' }
+ }
+ })
+ },
+ {
+ fallbackData: [{ key: 'basic', name: 'Loading…' } as const],
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ },
+ )
+
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/src/app/admin/components/ToolchainManagement.tsx b/src/app/admin/components/ToolchainManagement.tsx
index 2d5259ca..33cb3348 100644
--- a/src/app/admin/components/ToolchainManagement.tsx
+++ b/src/app/admin/components/ToolchainManagement.tsx
@@ -155,7 +155,7 @@ function NewToolchainForm() {
Nightly
-