Compare commits

..

No commits in common. "732b9944d66f40b9276ecc30cb0426f86212743a" and "6dc7ad86083cb5153b27b28764404ddc05742e66" have entirely different histories.

7 changed files with 88 additions and 159 deletions

3
.gitignore vendored
View File

@ -5,9 +5,6 @@ node_modules
pub/client/index.js pub/client/index.js
toes/ toes/
# generated
src/lib/templates.data.ts
# output # output
out out
dist dist

View File

@ -555,10 +555,6 @@ app.on('GET', ['/:repo{.+\\.git}/info/refs', '/:repo/info/refs'], async c => {
return c.text('Invalid service', 400) return c.text('Invalid service', 400)
} }
if (service === 'git-receive-pack' && c.req.header('x-sneaker')) {
return c.text('Push access denied over sneaker', 403)
}
if (service === 'git-receive-pack') { if (service === 'git-receive-pack') {
await ensureBareRepo(repoParam) await ensureBareRepo(repoParam)
} }
@ -590,10 +586,6 @@ app.on('POST', ['/:repo{.+\\.git}/git-upload-pack', '/:repo/git-upload-pack'], a
// POST /:repo[.git]/git-receive-pack // POST /:repo[.git]/git-receive-pack
app.on('POST', ['/:repo{.+\\.git}/git-receive-pack', '/:repo/git-receive-pack'], async c => { app.on('POST', ['/:repo{.+\\.git}/git-receive-pack', '/:repo/git-receive-pack'], async c => {
if (c.req.header('x-sneaker')) {
return c.text('Push access denied over sneaker', 403)
}
const repoParam = c.req.param('repo').replace(/\.git$/, '') const repoParam = c.req.param('repo').replace(/\.git$/, '')
if (!validRepoName(repoParam)) { if (!validRepoName(repoParam)) {

View File

@ -7,7 +7,7 @@
"dependencies": { "dependencies": {
"@because/forge": "^0.0.1", "@because/forge": "^0.0.1",
"@because/hype": "^0.0.2", "@because/hype": "^0.0.2",
"@because/sneaker": "^0.0.4", "@because/sneaker": "^0.0.3",
"commander": "14.0.3", "commander": "14.0.3",
"diff": "^8.0.3", "diff": "^8.0.3",
"kleur": "^4.1.5", "kleur": "^4.1.5",
@ -26,15 +26,15 @@
"@because/hype": ["@because/hype@0.0.2", "https://npm.nose.space/@because/hype/-/hype-0.0.2.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-fdKeII6USGC1loVVj+tPz086cKz+Bm+XozNee3NOnK4VP+q4yNPP2Fq1Yujw5xeDYE+ZvJn40gKwlngRvmX2hA=="], "@because/hype": ["@because/hype@0.0.2", "https://npm.nose.space/@because/hype/-/hype-0.0.2.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-fdKeII6USGC1loVVj+tPz086cKz+Bm+XozNee3NOnK4VP+q4yNPP2Fq1Yujw5xeDYE+ZvJn40gKwlngRvmX2hA=="],
"@because/sneaker": ["@because/sneaker@0.0.4", "https://npm.nose.space/@because/sneaker/-/sneaker-0.0.4.tgz", { "dependencies": { "hono": "^4.9.8", "unique-names-generator": "^4.7.1" }, "peerDependencies": { "typescript": "^5" } }, "sha512-juklirqLPOzCQTlY3Vf6elXO7bPTEfc1QB4ephdWONZwllovtAEF4H0O6CoOcoV5g5P0i8qUu+ffNVqtkC3SBw=="], "@because/sneaker": ["@because/sneaker@0.0.3", "https://npm.nose.space/@because/sneaker/-/sneaker-0.0.3.tgz", { "dependencies": { "hono": "^4.9.8", "unique-names-generator": "^4.7.1" }, "peerDependencies": { "typescript": "^5" } }, "sha512-4cG8w/tYPGbDtLw89k1PiASJKfWUdd1NXv+GKad2d7Ckw3FpZ+dnN2+gR2ihs81dqAkNaZomo+9RznBju2WaOw=="],
"@types/bun": ["@types/bun@1.3.10", "https://npm.nose.space/@types/bun/-/bun-1.3.10.tgz", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], "@types/bun": ["@types/bun@1.3.9", "https://npm.nose.space/@types/bun/-/bun-1.3.9.tgz", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/diff": ["@types/diff@8.0.0", "https://npm.nose.space/@types/diff/-/diff-8.0.0.tgz", { "dependencies": { "diff": "*" } }, "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw=="], "@types/diff": ["@types/diff@8.0.0", "https://npm.nose.space/@types/diff/-/diff-8.0.0.tgz", { "dependencies": { "diff": "*" } }, "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw=="],
"@types/node": ["@types/node@25.2.3", "https://npm.nose.space/@types/node/-/node-25.2.3.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], "@types/node": ["@types/node@25.2.3", "https://npm.nose.space/@types/node/-/node-25.2.3.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
"bun-types": ["bun-types@1.3.10", "https://npm.nose.space/bun-types/-/bun-types-1.3.10.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], "bun-types": ["bun-types@1.3.9", "https://npm.nose.space/bun-types/-/bun-types-1.3.9.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"commander": ["commander@14.0.3", "https://npm.nose.space/commander/-/commander-14.0.3.tgz", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "commander": ["commander@14.0.3", "https://npm.nose.space/commander/-/commander-14.0.3.tgz", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],

View File

@ -15,7 +15,7 @@
"toes": "src/cli/index.ts" "toes": "src/cli/index.ts"
}, },
"scripts": { "scripts": {
"check": "bun run templates && bunx tsc --noEmit", "check": "bunx tsc --noEmit",
"build": "./scripts/build.sh", "build": "./scripts/build.sh",
"cli:build": "bun run scripts/build.ts", "cli:build": "bun run scripts/build.ts",
"cli:build:all": "bun run scripts/build.ts --all", "cli:build:all": "bun run scripts/build.ts --all",
@ -24,7 +24,7 @@
"cli:uninstall": "sudo rm /usr/local/bin", "cli:uninstall": "sudo rm /usr/local/bin",
"deploy": "./scripts/deploy.sh", "deploy": "./scripts/deploy.sh",
"debug": "DEBUG=1 bun run dev", "debug": "DEBUG=1 bun run dev",
"dev": "bun run templates && rm -f pub/client/index.js && bun run --hot src/server/index.tsx", "dev": "rm -f pub/client/index.js && bun run --hot src/server/index.tsx",
"remote:deploy": "./scripts/deploy.sh", "remote:deploy": "./scripts/deploy.sh",
"remote:migrate": "bun run scripts/migrate.ts", "remote:migrate": "bun run scripts/migrate.ts",
"remote:install": "./scripts/remote-install.sh", "remote:install": "./scripts/remote-install.sh",
@ -33,7 +33,6 @@
"remote:start": "./scripts/remote-start.sh", "remote:start": "./scripts/remote-start.sh",
"remote:stop": "./scripts/remote-stop.sh", "remote:stop": "./scripts/remote-stop.sh",
"start": "bun run src/server/index.tsx", "start": "bun run src/server/index.tsx",
"templates": "bun run scripts/embed-templates.ts",
"test": "bun test" "test": "bun test"
}, },
"devDependencies": { "devDependencies": {
@ -46,7 +45,7 @@
"dependencies": { "dependencies": {
"@because/forge": "^0.0.1", "@because/forge": "^0.0.1",
"@because/hype": "^0.0.2", "@because/hype": "^0.0.2",
"@because/sneaker": "^0.0.4", "@because/sneaker": "^0.0.3",
"commander": "14.0.3", "commander": "14.0.3",
"diff": "^8.0.3", "diff": "^8.0.3",
"kleur": "^4.1.5" "kleur": "^4.1.5"

View File

@ -23,6 +23,47 @@ const TARGETS: BuildTarget[] = [
{ os: 'linux', arch: 'x64', name: 'toes-linux-x64' }, { os: 'linux', arch: 'x64', name: 'toes-linux-x64' },
] ]
// Ensure dist directory exists
if (!existsSync(DIST_DIR)) {
mkdirSync(DIST_DIR, { recursive: true })
}
// Parse command line args
const args = process.argv.slice(2)
const buildAll = args.includes('--all')
const targetArg = args.find(arg => arg.startsWith('--target='))?.split('=')[1]
async function buildTarget(target: BuildTarget) {
console.log(`Building ${target.name}...`)
const output = join(DIST_DIR, target.name)
const proc = Bun.spawn([
'bun',
'build',
ENTRY_POINT,
'--compile',
'--target',
`bun-${target.os}-${target.arch}`,
'--minify',
'--sourcemap=external',
'--outfile',
output,
], {
stdout: 'inherit',
stderr: 'inherit',
})
const exitCode = await proc.exited
if (exitCode === 0) {
console.log(`✓ Built ${target.name}`)
} else {
console.error(`✗ Failed to build ${target.name}`)
process.exit(exitCode)
}
}
async function buildCurrent() { async function buildCurrent() {
const platform = process.platform const platform = process.platform
const arch = process.arch const arch = process.arch
@ -59,57 +100,6 @@ async function buildCurrent() {
} }
} }
async function buildTarget(target: BuildTarget) {
console.log(`Building ${target.name}...`)
const output = join(DIST_DIR, target.name)
const proc = Bun.spawn([
'bun',
'build',
ENTRY_POINT,
'--compile',
'--target',
`bun-${target.os}-${target.arch}`,
'--minify',
'--sourcemap=external',
'--outfile',
output,
], {
stdout: 'inherit',
stderr: 'inherit',
})
const exitCode = await proc.exited
if (exitCode === 0) {
console.log(`✓ Built ${target.name}`)
} else {
console.error(`✗ Failed to build ${target.name}`)
process.exit(exitCode)
}
}
// Embed template files before compiling
const embedProc = Bun.spawn(['bun', 'run', join(import.meta.dir, 'embed-templates.ts')], {
stdout: 'inherit',
stderr: 'inherit',
})
if (await embedProc.exited !== 0) {
console.error('✗ Failed to embed templates')
process.exit(1)
}
// Ensure dist directory exists
if (!existsSync(DIST_DIR)) {
mkdirSync(DIST_DIR, { recursive: true })
}
// Parse command line args
const args = process.argv.slice(2)
const buildAll = args.includes('--all')
const targetArg = args.find(arg => arg.startsWith('--target='))?.split('=')[1]
// Main build logic // Main build logic
if (buildAll) { if (buildAll) {
console.log('Building for all targets...\n') console.log('Building for all targets...\n')

View File

@ -1,71 +0,0 @@
#!/usr/bin/env bun
// Generates src/lib/templates.data.ts with embedded template file contents.
// Run: bun run templates
import { readdirSync, readFileSync, statSync } from 'fs'
import { extname, join, relative } from 'path'
const BINARY_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.svg'])
const TEMPLATES_DIR = join(import.meta.dir, '..', 'templates')
const binary: Record<string, Record<string, string>> = {}
const shared: Record<string, string> = {}
const templates: Record<string, Record<string, string>> = {}
const isBinary = (path: string) =>
BINARY_EXTENSIONS.has(extname(path))
function readDir(dir: string): string[] {
const files: string[] = []
for (const entry of readdirSync(dir)) {
const path = join(dir, entry)
if (statSync(path).isDirectory()) {
files.push(...readDir(path))
} else {
files.push(path)
}
}
return files.sort()
}
// First pass: collect shared files (root level)
for (const entry of readdirSync(TEMPLATES_DIR).sort()) {
const path = join(TEMPLATES_DIR, entry)
if (!statSync(path).isDirectory()) {
shared[entry] = readFileSync(path, 'utf-8')
}
}
// Second pass: build template maps with shared files folded in
for (const entry of readdirSync(TEMPLATES_DIR).sort()) {
const path = join(TEMPLATES_DIR, entry)
if (statSync(path).isDirectory()) {
templates[entry] = { ...shared }
binary[entry] = {}
for (const filePath of readDir(path)) {
const filename = relative(path, filePath)
if (isBinary(filePath)) {
binary[entry]![filename] = readFileSync(filePath).toString('base64')
} else {
templates[entry]![filename] = readFileSync(filePath, 'utf-8')
}
}
if (Object.keys(binary[entry]!).length === 0) {
delete binary[entry]
}
}
}
// Generate TypeScript module
const lines: string[] = [
'// Auto-generated by scripts/embed-templates.ts',
'// Run `bun run templates` to regenerate',
'',
`export const TEMPLATES: Record<string, Record<string, string>> = ${JSON.stringify(templates, null, 2)}`,
'',
`export const BINARY: Record<string, Record<string, string>> = ${JSON.stringify(binary, null, 2)}`,
'',
]
const outPath = join(import.meta.dir, '..', 'src', 'lib', 'templates.data.ts')
await Bun.write(outPath, lines.join('\n'))
console.log(`✓ Embedded templates → ${relative(join(import.meta.dir, '..'), outPath)}`)

View File

@ -1,10 +1,10 @@
import { DEFAULT_EMOJI } from '@types' import { DEFAULT_EMOJI } from '@types'
import { readdirSync, readFileSync, statSync } from 'fs'
import { BINARY, TEMPLATES } from './templates.data' import { join, relative } from 'path'
export type TemplateType = 'ssr' | 'bare' | 'spa' export type TemplateType = 'ssr' | 'bare' | 'spa'
export type AppTemplates = Record<string, string | Uint8Array> export type AppTemplates = Record<string, string>
interface TemplateOptions { interface TemplateOptions {
tool?: boolean tool?: boolean
@ -15,6 +15,28 @@ interface TemplateVars {
APP_NAME: string APP_NAME: string
} }
const SHARED_FILES = ['CLAUDE.md', '.gitignore', '.npmrc', 'package.json', 'tsconfig.json']
const TEMPLATES_DIR = join(import.meta.dir, '../../templates')
function readDir(dir: string): string[] {
const files: string[] = []
for (const entry of readdirSync(dir)) {
const path = join(dir, entry)
if (statSync(path).isDirectory()) {
files.push(...readDir(path))
} else {
files.push(path)
}
}
return files
}
function replaceVars(content: string, vars: TemplateVars): string {
return content
.replace(/\$\$APP_NAME\$\$/g, vars.APP_NAME)
.replace(/\$\$APP_EMOJI\$\$/g, vars.APP_EMOJI)
}
export function generateTemplates(appName: string, template: TemplateType = 'ssr', options: TemplateOptions = {}): AppTemplates { export function generateTemplates(appName: string, template: TemplateType = 'ssr', options: TemplateOptions = {}): AppTemplates {
const vars: TemplateVars = { const vars: TemplateVars = {
APP_EMOJI: DEFAULT_EMOJI, APP_EMOJI: DEFAULT_EMOJI,
@ -23,29 +45,29 @@ export function generateTemplates(appName: string, template: TemplateType = 'ssr
const result: AppTemplates = {} const result: AppTemplates = {}
// Text files (shared + template-specific, merged at embed time) // Read shared files from templates/
for (const [filename, content] of Object.entries(TEMPLATES[template] ?? {})) { for (const filename of SHARED_FILES) {
let processed = replaceVars(content, vars) const path = join(TEMPLATES_DIR, filename)
let content = readFileSync(path, 'utf-8')
content = replaceVars(content, vars)
// Add tool option to package.json if specified
if (filename === 'package.json' && options.tool) { if (filename === 'package.json' && options.tool) {
const pkg = JSON.parse(processed) const pkg = JSON.parse(content)
pkg.toes.tool = true pkg.toes.tool = true
processed = JSON.stringify(pkg, null, 2) + '\n' content = JSON.stringify(pkg, null, 2) + '\n'
} }
result[filename] = processed result[filename] = content
} }
// Binary files (base64 encoded) // Read template-specific files
for (const [filename, base64] of Object.entries(BINARY[template] ?? {})) { const templateDir = join(TEMPLATES_DIR, template)
result[filename] = Buffer.from(base64, 'base64') for (const path of readDir(templateDir)) {
const filename = relative(templateDir, path)
const content = readFileSync(path, 'utf-8')
result[filename] = replaceVars(content, vars)
} }
return result return result
} }
function replaceVars(content: string, vars: TemplateVars): string {
return content
.replace(/\$\$APP_NAME\$\$/g, vars.APP_NAME)
.replace(/\$\$APP_EMOJI\$\$/g, vars.APP_EMOJI)
}