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
66 changes: 66 additions & 0 deletions .github/workflows/release-create.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Auto-releases on push to main (@next channel)
# For stable (@latest): cherry-pick commits to stable branch, then manually dispatch this workflow
name: 📦 Release Create

on:
push:
branches:
- main
paths:
- 'apps/create/**'
- 'pnpm-workspace.yaml'
- '!**/*.md'
workflow_dispatch:

permissions:
contents: write
packages: write

concurrency:
group: release-create-${{ github.ref }}
cancel-in-progress: true

jobs:
release:
runs-on: ubuntu-24.04
steps:
- name: Generate token
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.generate_token.outputs.token }}

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: pnpm
registry-url: 'https://registry.npmjs.org'

- uses: oven-sh/setup-bun@v2

- name: Install dependencies
run: pnpm install

- name: Build
run: pnpm --prefix apps/create run build

- name: Test
run: pnpm --prefix apps/create run test

- name: Release
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
LINEAR_TOKEN: ${{ secrets.LINEAR_TOKEN }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
working-directory: apps/create
run: pnpx semantic-release
50 changes: 50 additions & 0 deletions apps/create/.releaserc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-env node */
const branch = process.env.GITHUB_REF_NAME || process.env.CI_COMMIT_BRANCH;

const branches = [
{ name: 'stable', channel: 'latest' },
{ name: 'main', prerelease: 'next', channel: 'next' },
];

const isPrerelease = branches.some((b) => typeof b === 'object' && b.name === branch && b.prerelease);

const notesPlugin = isPrerelease
? '@semantic-release/release-notes-generator'
: ['semantic-release-ai-notes', { style: 'concise' }];

const config = {
branches,
tagFormat: 'create-v${version}',
plugins: [
'semantic-release-commit-filter',
'@semantic-release/commit-analyzer',
notesPlugin,
['@semantic-release/npm'],
],
};

if (!isPrerelease) {
config.plugins.push([
'@semantic-release/git',
{
assets: ['package.json'],
message: 'chore(create): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
},
]);
}

config.plugins.push(['semantic-release-linear-app', {
teamKeys: ['SD'],
addComment: true,
packageName: 'create',
commentTemplate: 'shipped in {package} {releaseLink} {channel}'
}]);

config.plugins.push([
'@semantic-release/github',
{
successComment: ':tada: This ${issue.pull_request ? "PR" : "issue"} is included in **@superdoc-dev/create** v${nextRelease.version}\n\nThe release is available on [GitHub release](${releases.find(release => release.pluginName === "@semantic-release/github").url})',
}
]);

module.exports = config;
23 changes: 23 additions & 0 deletions apps/create/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@superdoc-dev/create",
"version": "0.0.0",
"description": "Set up SuperDoc in your project — AGENTS.md, skills, and MCP configuration",
"type": "module",
"license": "MIT",
"bin": {
"create-superdoc": "./dist/index.js"
},
"files": [
"dist",
"skill"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "bun build src/index.ts --outdir dist --target node",
"test": "bun test",
"release": "pnpx semantic-release",
"release:dry-run": "pnpx semantic-release --dry-run"
}
}
123 changes: 123 additions & 0 deletions apps/create/src/__tests__/templates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { afterEach, describe, expect, test } from 'bun:test';
import { existsSync, mkdirSync, rmSync, writeFileSync, mkdtempSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { detectFramework, generateAgentsMd, type DetectedFramework } from '../templates';

describe('detectFramework', () => {
let testDir: string;

afterEach(() => {
if (testDir && existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});

function createDir(deps: Record<string, string> = {}, devDeps: Record<string, string> = {}): string {
testDir = mkdtempSync(join(tmpdir(), 'superdoc-fw-'));
const pkg: Record<string, unknown> = {};
if (Object.keys(deps).length) pkg.dependencies = deps;
if (Object.keys(devDeps).length) pkg.devDependencies = devDeps;
writeFileSync(join(testDir, 'package.json'), JSON.stringify(pkg));
return testDir;
}

const cases: [string, Record<string, string>, DetectedFramework][] = [
['react', { react: '^19' }, 'react'],
['@superdoc-dev/react', { '@superdoc-dev/react': '^1' }, 'react'],
['next', { next: '^15', react: '^19' }, 'nextjs'],
['vue', { vue: '^3' }, 'vue'],
['nuxt', { nuxt: '^4', vue: '^3' }, 'nuxt'],
['@angular/core', { '@angular/core': '^19' }, 'angular'],
['svelte', { svelte: '^5' }, 'svelte'],
];

for (const [dep, deps, expected] of cases) {
test(`detects ${expected} from ${dep}`, () => {
const dir = createDir(deps);
expect(detectFramework(dir)).toBe(expected);
});
}

test('returns vanilla when no framework deps', () => {
const dir = createDir({});
expect(detectFramework(dir)).toBe('vanilla');
});

test('returns vanilla when no package.json', () => {
testDir = mkdtempSync(join(tmpdir(), 'superdoc-fw-'));
expect(detectFramework(testDir)).toBe('vanilla');
});

test('detects from devDependencies', () => {
testDir = mkdtempSync(join(tmpdir(), 'superdoc-fw-'));
writeFileSync(join(testDir, 'package.json'), JSON.stringify({ devDependencies: { react: '^19' } }));
expect(detectFramework(testDir)).toBe('react');
});

test('prefers next over react', () => {
const dir = createDir({ next: '^15', react: '^19' });
expect(detectFramework(dir)).toBe('nextjs');
});

test('prefers nuxt over vue', () => {
const dir = createDir({ nuxt: '^4', vue: '^3' });
expect(detectFramework(dir)).toBe('nuxt');
});
});

describe('generateAgentsMd', () => {
test('react template includes @superdoc-dev/react', () => {
const md = generateAgentsMd('react');
expect(md).toContain('@superdoc-dev/react');
expect(md).toContain('Embed editor — React');
});

test('nextjs template includes use client', () => {
const md = generateAgentsMd('nextjs');
expect(md).toContain("'use client'");
expect(md).toContain('Embed editor — Next.js');
expect(md).toContain('client component');
});

test('nuxt template includes ClientOnly', () => {
const md = generateAgentsMd('nuxt');
expect(md).toContain('ClientOnly');
expect(md).toContain('Embed editor — Nuxt');
expect(md).toContain('Disable SSR');
});

test('vanilla template uses superdoc import', () => {
const md = generateAgentsMd('vanilla');
expect(md).toContain("import { SuperDoc } from 'superdoc'");
expect(md).toContain("import 'superdoc/style.css'");
});

test('all templates contain knowledge corrections', () => {
for (const fw of ['react', 'nextjs', 'vue', 'nuxt', 'angular', 'svelte', 'vanilla'] as const) {
const md = generateAgentsMd(fw);
expect(md).toContain('Knowledge corrections');
expect(md).toContain('python-docx');
expect(md).toContain('superdoc.activeEditor.commands.addComment');
expect(md).toContain('modules.toolbar.customButtons');
}
});

test('all templates contain MCP and SDK sections', () => {
for (const fw of ['react', 'nextjs', 'vue', 'nuxt', 'angular', 'svelte', 'vanilla'] as const) {
const md = generateAgentsMd(fw);
expect(md).toContain('claude mcp add superdoc');
expect(md).toContain('@superdoc-dev/sdk');
expect(md).toContain('@superdoc-dev/cli');
}
});

test('all templates contain correct API names', () => {
for (const fw of ['react', 'nextjs', 'vue', 'nuxt', 'angular', 'svelte', 'vanilla'] as const) {
const md = generateAgentsMd(fw);
expect(md).toContain("superdoc.on('ready'");
expect(md).not.toContain('content-changed');
expect(md).not.toContain('customItems');
}
});
});
Loading
Loading