diff --git a/packages/preset-nuxt4/src/features/auth-2fa.ts b/packages/preset-nuxt4/src/features/auth-2fa.ts index 3daad1ba..ca28bd2a 100644 --- a/packages/preset-nuxt4/src/features/auth-2fa.ts +++ b/packages/preset-nuxt4/src/features/auth-2fa.ts @@ -7,7 +7,7 @@ import { STAGE } from '@battlestack/core' /** TOTP-based two-factor auth. Secrets encrypted at rest with AES-256-GCM via `NUXT_TOTP_ENCRYPTION_KEY`. */ export const auth2faFeature: Feature = { id: 'nuxt4:auth-2fa', - version: '1.2.1', + version: '1.2.2', label: 'Two-factor auth (TOTP)', description: 'Authenticator-app 2FA; TOTP secrets encrypted at rest.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/src/features/docs.ts b/packages/preset-nuxt4/src/features/docs.ts index 782fc5be..a5a51128 100644 --- a/packages/preset-nuxt4/src/features/docs.ts +++ b/packages/preset-nuxt4/src/features/docs.ts @@ -15,7 +15,7 @@ import { /** Generates AGENTS.md, CLAUDE.md and README.md from `collectDocs()`. Always rewritten. */ export const docsFeature: Feature = { id: 'nuxt4:docs', - version: '1.0.24', + version: '1.0.25', label: 'Generate AGENTS.md + CLAUDE.md + README.md', frameworks: ['nuxt4'], stage: STAGE.DOCS, diff --git a/packages/preset-nuxt4/src/features/github.ts b/packages/preset-nuxt4/src/features/github.ts index c12fa8a9..15d2a53f 100644 --- a/packages/preset-nuxt4/src/features/github.ts +++ b/packages/preset-nuxt4/src/features/github.ts @@ -12,7 +12,7 @@ const FEATURE_ID = 'shared:github' /** GitHub Actions gate: lint, typecheck, coverage, dependency audit. ` audit` is advisory. */ export const githubFeature: Feature = { id: 'shared:github', - version: '3.1.0', + version: '3.1.1', label: 'GitHub Actions workflows', stage: STAGE.GITIGNORE, failureIsNonFatal: true, @@ -38,7 +38,7 @@ export const githubFeature: Feature = { '', 'A freshly scaffolded project\'s `' + audit + '` will report findings in transitive dependencies of the AI and framework stack. That is the npm-ecosystem baseline for an unpinned dependency tree, not a defect the scaffold introduced, and not something this project can unilaterally clear. The CI gate is shaped around that fact: `dependency-review-action` blocks vulnerabilities your changes *introduce*, while the audit step reports the standing baseline without failing the build.', '', - 'To run it on a self-hosted runner instead, set the repo/org variable `CI_RUNNER` to your runner\'s label; no template edit needed. If that runner\'s image is missing packages `ubuntu-latest` already has, set `CI_RUNNER_APT_PACKAGES` (space-separated) to have the workflow `apt-get install` them first; leave it unset to skip that step entirely.', + 'To run it on a self-hosted runner instead, set the repo/org variable `CI_RUNNER` to your runner\'s label; no template edit needed. If that runner\'s image is missing packages `ubuntu-latest` already has, set `CI_RUNNER_APT_PACKAGES` (space-separated) to have both workflows `apt-get install` them first; leave it unset to skip that step entirely.', '', 'Secrets: any Docker build secrets declared by enabled features (see the Docker section for the exact env var names). This workflow itself needs none.', '', @@ -86,8 +86,8 @@ async function emit(ctx: RunContext): Promise { ) written.push(workflowRel) - // Copied verbatim: it runs git and gh, never the project's package manager, so there is - // nothing in it to render per manager. + // Copied verbatim: it never runs the project's package manager, so there is nothing in it + // to render per manager. const releaseRel = '.github/workflows/release.yml' await writeRecorded( ctx, diff --git a/packages/preset-nuxt4/src/features/mastra.ts b/packages/preset-nuxt4/src/features/mastra.ts index b8a03205..687fa03d 100644 --- a/packages/preset-nuxt4/src/features/mastra.ts +++ b/packages/preset-nuxt4/src/features/mastra.ts @@ -29,7 +29,7 @@ import { /** Mastra AI runtime. Talks to an OpenAI-compatible AI gateway (sluis.ai preset, or any compatible URL) via `@ai-sdk/openai-compatible`. */ export const mastraFeature: Feature = { id: 'nuxt4:mastra', - version: '2.1.2', + version: '2.1.3', label: 'Mastra AI runtime', frameworks: ['nuxt4'], stage: STAGE.AI_CORE, diff --git a/packages/preset-nuxt4/src/features/prompts.ts b/packages/preset-nuxt4/src/features/prompts.ts index fe1fbc56..6ea57614 100644 --- a/packages/preset-nuxt4/src/features/prompts.ts +++ b/packages/preset-nuxt4/src/features/prompts.ts @@ -6,7 +6,7 @@ import type { Feature } from '@battlestack/core' /** Admin-editable AI agent prompts. */ export const promptsFeature: Feature = { id: 'nuxt4:prompts', - version: '1.2.0', + version: '1.2.1', label: 'AI prompt management', description: 'Admin-editable agent prompts with shipped registry defaults.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/src/features/user-admin.ts b/packages/preset-nuxt4/src/features/user-admin.ts index a0233d97..8566b5be 100644 --- a/packages/preset-nuxt4/src/features/user-admin.ts +++ b/packages/preset-nuxt4/src/features/user-admin.ts @@ -6,7 +6,7 @@ import { STAGE } from '@battlestack/core' /** Admin-gated user CRUD + admin pages + admin route middleware. */ export const userAdminFeature: Feature = { id: 'nuxt4:user-admin', - version: '1.0.3', + version: '1.0.4', label: 'User administration (admin-gated CRUD)', description: 'Admin-only /dashboard/users for listing, creating, editing, and deleting users.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/templates/.github/workflows/release.yml b/packages/preset-nuxt4/templates/.github/workflows/release.yml index 8b7ae452..82bf5fc5 100644 --- a/packages/preset-nuxt4/templates/.github/workflows/release.yml +++ b/packages/preset-nuxt4/templates/.github/workflows/release.yml @@ -35,6 +35,12 @@ jobs: # Set the repo/org variable `CI_RUNNER` to a self-hosted runner label to point this job at your own fleet, no template edit needed. runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }} steps: + # Same knob as lint-test.yml: a minimal self-hosted image may lack git or curl. + # Before checkout: without git it downloads a tarball with no tags. + - name: Install extra runner packages + if: vars.CI_RUNNER_APT_PACKAGES != '' + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends ${{ vars.CI_RUNNER_APT_PACKAGES }} + - uses: actions/checkout@v7 with: # Tags *are* the version history here, and a shallow clone fetches none of them. @@ -96,7 +102,31 @@ jobs: # tagger date is what dates the release when someone reads the history later. git tag -a "$TAG" -m "$TAG" git push origin "refs/tags/${TAG}" - gh release create "$TAG" --title "$TAG" --generate-notes --target "$GITHUB_SHA" + # curl, not gh: a self-hosted `CI_RUNNER` image may lack the GitHub CLI. + # TAG is vMAJOR.MINOR.PATCH and GITHUB_SHA is hex, so printf builds safe JSON. + body="${RUNNER_TEMP}/release.json" + # The auth header goes in on stdin: argv is visible to every user on a shared runner. + status=$(curl --silent --show-error --retry 3 -o "$body" -w '%{http_code}' \ + -X POST "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases" \ + -H @- <<< "Authorization: Bearer ${GH_TOKEN}" \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + -d "$(printf '{"tag_name":"%s","name":"%s","target_commitish":"%s","generate_release_notes":true}' "$TAG" "$TAG" "$GITHUB_SHA")") || true + if [ "$status" != 201 ]; then + # A retried POST fails with 422 when the first attempt already created the release. + exists=$(curl --silent --retry 3 -o /dev/null -w '%{http_code}' \ + -H @- <<< "Authorization: Bearer ${GH_TOKEN}" \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}") || true + if [ "$exists" = 200 ]; then + echo "::warning::creating the ${TAG} release returned HTTP ${status}, but the release exists" + exit 0 + fi + echo "::error::creating the ${TAG} release returned HTTP ${status}; the tag is pushed, so if the release is missing, create it from the tag by hand" + cat "$body" 2>/dev/null + exit 1 + fi - name: Say what shipped if: always() && steps.version.outputs.tag != '' diff --git a/packages/preset-nuxt4/templates/auth-2fa/app/composables/use2fa.ts b/packages/preset-nuxt4/templates/auth-2fa/app/composables/use2fa.ts index fd4710b4..3e8195a1 100644 --- a/packages/preset-nuxt4/templates/auth-2fa/app/composables/use2fa.ts +++ b/packages/preset-nuxt4/templates/auth-2fa/app/composables/use2fa.ts @@ -1,32 +1,32 @@ /** Client wrapper around the 2FA endpoints. */ export function use2fa() { async function status(): Promise<{ enabled: boolean; enabledAt: string | null }> { - return $fetch('/api/auth/2fa/status') + return $fetch<{ enabled: boolean; enabledAt: string | null }>('/api/auth/2fa/status') } async function setup(): Promise<{ secret: string; otpauthUrl: string }> { - return $fetch('/api/auth/2fa/setup', { method: 'POST' }) + return $fetch<{ secret: string; otpauthUrl: string }>('/api/auth/2fa/setup', { method: 'POST' }) } // Enabling 2FA also returns the freshly-generated backup codes (shown once). async function verify(code: string): Promise<{ ok: true; backupCodes: string[] }> { - return $fetch('/api/auth/2fa/verify', { method: 'POST', body: { code } }) + return $fetch<{ ok: true; backupCodes: string[] }>('/api/auth/2fa/verify', { method: 'POST', body: { code } }) } async function disable(code: string): Promise<{ ok: true }> { - return $fetch('/api/auth/2fa/disable', { method: 'POST', body: { code } }) + return $fetch<{ ok: true }>('/api/auth/2fa/disable', { method: 'POST', body: { code } }) } async function backupCodesStatus(): Promise<{ unused: number }> { - return $fetch('/api/auth/2fa/backup-codes') + return $fetch<{ unused: number }>('/api/auth/2fa/backup-codes') } async function generateBackupCodes(): Promise<{ codes: string[] }> { - return $fetch('/api/auth/2fa/backup-codes/generate', { method: 'POST' }) + return $fetch<{ codes: string[] }>('/api/auth/2fa/backup-codes/generate', { method: 'POST' }) } async function redeemBackupCode(code: string): Promise<{ ok: true }> { - return $fetch('/api/auth/2fa/backup-codes/redeem', { + return $fetch<{ ok: true }>('/api/auth/2fa/backup-codes/redeem', { method: 'POST', body: { code }, }) diff --git a/packages/preset-nuxt4/templates/mastra-admin/app/pages/dashboard/settings/ai.vue b/packages/preset-nuxt4/templates/mastra-admin/app/pages/dashboard/settings/ai.vue index 39235273..5e9acdef 100644 --- a/packages/preset-nuxt4/templates/mastra-admin/app/pages/dashboard/settings/ai.vue +++ b/packages/preset-nuxt4/templates/mastra-admin/app/pages/dashboard/settings/ai.vue @@ -16,7 +16,7 @@ const toast = useToast() const headers = useRequestHeaders(['cookie']) const { data: modelConfigs, refresh: refreshConfigs } = await useAsyncData( 'ai-model-configs', - () => $fetch('/api/ai/configs', { headers }), + () => $fetch('/api/ai/configs', { headers }), ) interface ProxyModel { id: string @@ -76,7 +76,7 @@ interface PromptRow { const { data: agentRows, refresh: refreshAgents } = await useAsyncData( 'ai-agents', - () => $fetch('/api/ai/agents', { headers }), + () => $fetch('/api/ai/agents', { headers }), { default: () => [] }, ) diff --git a/packages/preset-nuxt4/templates/prompts/app/pages/dashboard/prompts.vue b/packages/preset-nuxt4/templates/prompts/app/pages/dashboard/prompts.vue index 37f48e90..d7f31063 100644 --- a/packages/preset-nuxt4/templates/prompts/app/pages/dashboard/prompts.vue +++ b/packages/preset-nuxt4/templates/prompts/app/pages/dashboard/prompts.vue @@ -23,7 +23,7 @@ const { t } = useI18n() const headers = useRequestHeaders(['cookie']) const toast = useToast() const { data, pending, refresh } = await useAsyncData('prompts-admin-list', () => - $fetch('/api/prompts', { headers }), + $fetch('/api/prompts', { headers }), ) const editing = ref>({}) diff --git a/packages/preset-nuxt4/templates/user-admin/app/pages/dashboard/users/[id].vue b/packages/preset-nuxt4/templates/user-admin/app/pages/dashboard/users/[id].vue index b1917141..33efea41 100644 --- a/packages/preset-nuxt4/templates/user-admin/app/pages/dashboard/users/[id].vue +++ b/packages/preset-nuxt4/templates/user-admin/app/pages/dashboard/users/[id].vue @@ -20,7 +20,7 @@ const headers = useRequestHeaders(['cookie']) const { data: user, refresh } = await useAsyncData( () => `user-${userId.value}`, - () => $fetch(`/api/users/${userId.value}`, { headers }), + () => $fetch(`/api/users/${userId.value}`, { headers }), ) const form = reactive({ diff --git a/packages/preset-nuxt4/templates/user-admin/app/pages/dashboard/users/index.vue b/packages/preset-nuxt4/templates/user-admin/app/pages/dashboard/users/index.vue index e330e85d..79f38100 100644 --- a/packages/preset-nuxt4/templates/user-admin/app/pages/dashboard/users/index.vue +++ b/packages/preset-nuxt4/templates/user-admin/app/pages/dashboard/users/index.vue @@ -27,7 +27,7 @@ const headers = useRequestHeaders(['cookie']) const { data, status, refresh } = await useAsyncData( 'users-admin-list', () => - $fetch('/api/users', { + $fetch('/api/users', { query: { search: search.value || undefined, limit: PAGE_SIZE, diff --git a/packages/preset-nuxt4/test/github.test.ts b/packages/preset-nuxt4/test/github.test.ts index d244bf62..1b6bef99 100644 --- a/packages/preset-nuxt4/test/github.test.ts +++ b/packages/preset-nuxt4/test/github.test.ts @@ -202,6 +202,19 @@ describe('githubFeature', () => { expect(workflow).not.toMatch(/^\s{2}schedule:/m) }) + /** A self-hosted `CI_RUNNER` image may lack the GitHub CLI. */ + it('publishes the release through the REST API, never the gh CLI', async () => { + await githubFeature.execute!(ctx()) + const workflow = await readFile(path.join(projectDir, '.github/workflows/release.yml'), 'utf8') + const commands = workflow.split('\n').filter((line) => !line.trim().startsWith('#')) + + expect(commands.some((line) => /\bgh\b/.test(line))).toBe(false) + expect(workflow).toContain('${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases') + expect(workflow).toContain( + `'{"tag_name":"%s","name":"%s","target_commitish":"%s","generate_release_notes":true}' "$TAG" "$TAG" "$GITHUB_SHA"`, + ) + }) + it('collectDocs documents the quality gate under a single GitHub Actions heading', () => { const docs = githubFeature.collectDocs!(ctx()) expect(docs).toHaveLength(1)