Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/preset-nuxt4/src/features/auth-2fa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
2 changes: 1 addition & 1 deletion packages/preset-nuxt4/src/features/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions packages/preset-nuxt4/src/features/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const FEATURE_ID = 'shared:github'
/** GitHub Actions gate: lint, typecheck, coverage, dependency audit. `<pm> 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,
Expand All @@ -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.',
'',
Expand Down Expand Up @@ -86,8 +86,8 @@ async function emit(ctx: RunContext): Promise<string[]> {
)
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,
Expand Down
2 changes: 1 addition & 1 deletion packages/preset-nuxt4/src/features/mastra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/preset-nuxt4/src/features/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
2 changes: 1 addition & 1 deletion packages/preset-nuxt4/src/features/user-admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
32 changes: 31 additions & 1 deletion packages/preset-nuxt4/templates/.github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 != ''
Expand Down
Original file line number Diff line number Diff line change
@@ -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 },
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const toast = useToast()
const headers = useRequestHeaders(['cookie'])
const { data: modelConfigs, refresh: refreshConfigs } = await useAsyncData<ModelConfig[]>(
'ai-model-configs',
() => $fetch('/api/ai/configs', { headers }),
() => $fetch<ModelConfig[]>('/api/ai/configs', { headers }),
)
interface ProxyModel {
id: string
Expand Down Expand Up @@ -76,7 +76,7 @@ interface PromptRow {

const { data: agentRows, refresh: refreshAgents } = await useAsyncData<AgentRow[]>(
'ai-agents',
() => $fetch('/api/ai/agents', { headers }),
() => $fetch<AgentRow[]>('/api/ai/agents', { headers }),
{ default: () => [] },
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const { t } = useI18n()
const headers = useRequestHeaders(['cookie'])
const toast = useToast()
const { data, pending, refresh } = await useAsyncData<PromptList>('prompts-admin-list', () =>
$fetch('/api/prompts', { headers }),
$fetch<PromptList>('/api/prompts', { headers }),
)

const editing = ref<Record<string, string>>({})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const headers = useRequestHeaders(['cookie'])

const { data: user, refresh } = await useAsyncData<UserRow>(
() => `user-${userId.value}`,
() => $fetch(`/api/users/${userId.value}`, { headers }),
() => $fetch<UserRow>(`/api/users/${userId.value}`, { headers }),
)

const form = reactive({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const headers = useRequestHeaders(['cookie'])
const { data, status, refresh } = await useAsyncData<UserList>(
'users-admin-list',
() =>
$fetch('/api/users', {
$fetch<UserList>('/api/users', {
query: {
search: search.value || undefined,
limit: PAGE_SIZE,
Expand Down
13 changes: 13 additions & 0 deletions packages/preset-nuxt4/test/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading