@takuhon/cli 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,201 @@
1
+ // src/scaffold/deps.ts
2
+ var TAKUHON_DEP_RANGE = "^0.21.0";
3
+
4
+ // src/scaffold/vercel.ts
5
+ function isValidVercelProjectName(name) {
6
+ return /^[a-z0-9](?:[a-z0-9._-]{0,98}[a-z0-9])?$/.test(name);
7
+ }
8
+ function buildVercelPackageJson(opts) {
9
+ return {
10
+ name: opts.projectName,
11
+ version: "0.0.0",
12
+ private: true,
13
+ description: "Takuhon profile deployment (Vercel).",
14
+ scripts: {
15
+ dev: "next dev",
16
+ build: "next build",
17
+ start: "next start"
18
+ },
19
+ dependencies: {
20
+ "@takuhon/vercel": TAKUHON_DEP_RANGE,
21
+ hono: "^4.0.0",
22
+ next: "^15.0.0",
23
+ react: "^19.0.0",
24
+ "react-dom": "^19.0.0"
25
+ },
26
+ devDependencies: {
27
+ "@types/node": "^22.0.0",
28
+ typescript: "^5.7.0"
29
+ },
30
+ engines: {
31
+ node: ">=22.0.0"
32
+ }
33
+ };
34
+ }
35
+ function renderVercelPackageJson(opts) {
36
+ return `${JSON.stringify(buildVercelPackageJson(opts), null, 2)}
37
+ `;
38
+ }
39
+ function renderVercelRouteTs() {
40
+ return `import { BundledTakuhonStorage, createTakuhonVercelApp } from '@takuhon/vercel';
41
+ import { handle } from 'hono/vercel';
42
+
43
+ // The project's own profile, bundled into the deployment. Edit takuhon.json and
44
+ // redeploy (push to your Vercel-connected Git repo, or run \`vercel\`) to publish
45
+ // changes \u2014 this adapter is read-only at runtime.
46
+ import takuhonJson from '../../takuhon.json';
47
+
48
+ const app = createTakuhonVercelApp({
49
+ storage: new BundledTakuhonStorage(takuhonJson),
50
+ });
51
+
52
+ export const GET = handle(app);
53
+ `;
54
+ }
55
+ function renderNextConfig() {
56
+ return `/** @type {import('next').NextConfig} */
57
+ const nextConfig = {};
58
+
59
+ export default nextConfig;
60
+ `;
61
+ }
62
+ function renderVercelTsconfigJson() {
63
+ const config = {
64
+ compilerOptions: {
65
+ target: "ES2022",
66
+ lib: ["dom", "dom.iterable", "esnext"],
67
+ allowJs: true,
68
+ skipLibCheck: true,
69
+ strict: true,
70
+ noEmit: true,
71
+ esModuleInterop: true,
72
+ module: "esnext",
73
+ moduleResolution: "bundler",
74
+ resolveJsonModule: true,
75
+ isolatedModules: true,
76
+ jsx: "preserve",
77
+ incremental: true,
78
+ plugins: [{ name: "next" }]
79
+ },
80
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
81
+ exclude: ["node_modules"]
82
+ };
83
+ return `${JSON.stringify(config, null, 2)}
84
+ `;
85
+ }
86
+ function renderVercelGitignore() {
87
+ return `# Dependencies
88
+ node_modules/
89
+
90
+ # Next.js build output
91
+ .next/
92
+ next-env.d.ts
93
+
94
+ # Vercel
95
+ .vercel/
96
+
97
+ # Environment variables (commit \`.env.example\` instead)
98
+ .env
99
+ .env.local
100
+ .env.*.local
101
+
102
+ # Logs
103
+ *.log
104
+ npm-debug.log*
105
+ pnpm-debug.log*
106
+ yarn-debug.log*
107
+ yarn-error.log*
108
+
109
+ # macOS
110
+ .DS_Store
111
+ `;
112
+ }
113
+ function renderVercelEnvExample() {
114
+ return `# ----------------------------------------------------------------
115
+ # Local development variables for this Takuhon deployment (Vercel).
116
+ #
117
+ # This adapter is read-only: the profile is the bundled takuhon.json, edited in
118
+ # Git. There are no admin tokens or KV/R2 bindings.
119
+ # ----------------------------------------------------------------
120
+
121
+ # Optional: fetch the profile from a URL at cold start instead of bundling it.
122
+ # Leave unset to use the committed takuhon.json (the default). When set, switch
123
+ # the route to UrlTakuhonStorage (see app/[[...route]]/route.ts).
124
+ # TAKUHON_DATA_URL=https://example.com/takuhon.json
125
+ `;
126
+ }
127
+ function formatLicenseLine(license) {
128
+ if (license.spdxId === "Proprietary") {
129
+ return "**Proprietary** \u2014 all rights reserved.";
130
+ }
131
+ if (license.url !== void 0) {
132
+ return `[\`${license.spdxId}\`](${license.url})`;
133
+ }
134
+ return `\`${license.spdxId}\``;
135
+ }
136
+ function renderVercelReadme(opts) {
137
+ const { projectName, license } = opts;
138
+ const licenseLine = formatLicenseLine(license);
139
+ return `# ${projectName}
140
+
141
+ A [Takuhon](https://github.com/takuhon-dev/takuhon) profile deployment, running on [Vercel](https://vercel.com) via the read-only \`@takuhon/vercel\` adapter.
142
+
143
+ > **Status**: pre-deploy. Edit \`takuhon.json\`, then deploy to Vercel.
144
+
145
+ ## What is Takuhon?
146
+
147
+ Takuhon lets you own your profile as a portable JSON document and publish it as a mobile-first profile page plus a public API (JSON-LD for AI agents and search engines included).
148
+
149
+ ## How it works
150
+
151
+ This project is a minimal Next.js App Router app. The catch-all route at
152
+ \`app/[[...route]]/route.ts\` mounts \`@takuhon/vercel\`, which serves:
153
+
154
+ - \`/\` and \`/<locale>/\` \u2014 the server-rendered profile page with embedded Schema.org JSON-LD
155
+ - \`/api/profile\`, \`/api/jsonld\`, \`/api/schema\`
156
+ - \`/takuhon.json\`, \`/.well-known/takuhon.json\`
157
+
158
+ It is **read-only**: there is no admin UI, auth, or database. Your profile is the
159
+ committed \`takuhon.json\` \u2014 edit it and redeploy to publish changes. (Image
160
+ uploads, the MCP endpoint, and the activity badge are Cloudflare-adapter
161
+ features; use \`@takuhon/cloudflare\` if you need them.)
162
+
163
+ ## Setup
164
+
165
+ 1. **Install dependencies.**
166
+
167
+ \`\`\`sh
168
+ pnpm install
169
+ \`\`\`
170
+
171
+ 2. **Edit your profile.** Open \`takuhon.json\` and replace the sample fields (\`profile.displayName\`, \`links\`, \`careers\`, \`projects\`, \`skills\`) with your own.
172
+
173
+ 3. **Run it locally.**
174
+
175
+ \`\`\`sh
176
+ pnpm dev
177
+ \`\`\`
178
+
179
+ Open the printed URL: \`/\` serves your profile page and \`/api/*\` serves the JSON API.
180
+
181
+ ## Deploy
182
+
183
+ Push this project to a Git repository connected to Vercel, or deploy from the CLI:
184
+
185
+ \`\`\`sh
186
+ npx vercel
187
+ \`\`\`
188
+
189
+ To publish changes later, edit \`takuhon.json\`, commit, and push \u2014 Vercel redeploys automatically.
190
+
191
+ ## License
192
+
193
+ Profile content (\`takuhon.json\`) is licensed under ${licenseLine}
194
+
195
+ The deployment code is your own; pick a license appropriate for it.
196
+ `;
197
+ }
198
+
1
199
  // src/scaffold/wrangler-toml.ts
2
200
  function isValidWorkerName(name) {
3
201
  return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(name);
@@ -157,9 +355,9 @@ function buildPackageJson(opts) {
157
355
  deploy: "wrangler deploy"
158
356
  },
159
357
  dependencies: {
160
- "@takuhon/api": "^0.19.0",
161
- "@takuhon/cloudflare": "^0.19.0",
162
- "@takuhon/core": "^0.19.0",
358
+ "@takuhon/api": TAKUHON_DEP_RANGE,
359
+ "@takuhon/cloudflare": TAKUHON_DEP_RANGE,
360
+ "@takuhon/core": TAKUHON_DEP_RANGE,
163
361
  hono: "^4.0.0"
164
362
  },
165
363
  devDependencies: {
@@ -176,7 +374,7 @@ function renderPackageJson(opts) {
176
374
  }
177
375
 
178
376
  // src/scaffold/readme.ts
179
- function formatLicenseLine(license) {
377
+ function formatLicenseLine2(license) {
180
378
  if (license.spdxId === "Proprietary") {
181
379
  return "**Proprietary** \u2014 all rights reserved.";
182
380
  }
@@ -187,7 +385,7 @@ function formatLicenseLine(license) {
187
385
  }
188
386
  function renderReadme(opts) {
189
387
  const { projectName, license } = opts;
190
- const licenseLine = formatLicenseLine(license);
388
+ const licenseLine = formatLicenseLine2(license);
191
389
  return `# ${projectName}
192
390
 
193
391
  A [Takuhon](https://github.com/takuhon-dev/takuhon) profile deployment, running on Cloudflare Workers.
@@ -468,7 +666,7 @@ var TargetDirectoryExistsError = class extends Error {
468
666
  code = "TARGET_EXISTS";
469
667
  };
470
668
  async function writeProject(opts) {
471
- const { targetDir, projectName, license } = opts;
669
+ const { targetDir, projectName, license, platform = "cloudflare" } = opts;
472
670
  try {
473
671
  await mkdir(targetDir, { recursive: false });
474
672
  } catch (err) {
@@ -477,7 +675,16 @@ async function writeProject(opts) {
477
675
  }
478
676
  throw err;
479
677
  }
480
- const files = [
678
+ const files = platform === "vercel" ? [
679
+ { path: "takuhon.json", content: renderTakuhonJson(license) },
680
+ { path: "package.json", content: renderVercelPackageJson({ projectName }) },
681
+ { path: "README.md", content: renderVercelReadme({ projectName, license }) },
682
+ { path: ".gitignore", content: renderVercelGitignore() },
683
+ { path: ".env.example", content: renderVercelEnvExample() },
684
+ { path: "tsconfig.json", content: renderVercelTsconfigJson() },
685
+ { path: "next.config.mjs", content: renderNextConfig() },
686
+ { path: "app/[[...route]]/route.ts", content: renderVercelRouteTs() }
687
+ ] : [
481
688
  { path: "takuhon.json", content: renderTakuhonJson(license) },
482
689
  { path: "wrangler.toml", content: renderWranglerToml(projectName) },
483
690
  { path: "package.json", content: renderPackageJson({ projectName }) },
@@ -519,6 +726,7 @@ async function copyAdminBundle(opts) {
519
726
  }
520
727
 
521
728
  export {
729
+ isValidVercelProjectName,
522
730
  isValidWorkerName,
523
731
  ADMIN_DIST_DIRNAME,
524
732
  TargetDirectoryExistsError,
@@ -526,4 +734,4 @@ export {
526
734
  resolveAdminBundleDir,
527
735
  copyAdminBundle
528
736
  };
529
- //# sourceMappingURL=chunk-H2RCCOPA.js.map
737
+ //# sourceMappingURL=chunk-WOSXRHGM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scaffold/deps.ts","../src/scaffold/vercel.ts","../src/scaffold/wrangler-toml.ts","../src/scaffold/index.ts","../src/scaffold/env-example.ts","../src/scaffold/gitignore.ts","../src/scaffold/package-json.ts","../src/scaffold/readme.ts","../src/scaffold/takuhon-json.ts","../src/scaffold/tsconfig-json.ts","../src/scaffold/worker-index-ts.ts"],"sourcesContent":["/**\n * Single source of truth for the `@takuhon/*` dependency range pinned into\n * every scaffolded project (Cloudflare and Vercel alike).\n *\n * Under 0.x semver a caret does not span minors, so a scaffolded project must\n * pin the same minor the CLI itself ships at — all `@takuhon/*` packages release\n * in lockstep. Advancing this one constant on each minor release keeps both\n * platform scaffolds in step; the `scaffold.test.ts` pin-guard derives the\n * expected range from the CLI's own version, so a missed bump fails CI.\n */\nexport const TAKUHON_DEP_RANGE = '^0.21.0';\n","/**\n * Generators for the `--platform vercel` scaffold.\n *\n * The Vercel scaffold is a minimal Next.js App Router project whose single\n * catch-all Route Handler mounts `@takuhon/vercel`'s `createTakuhonVercelApp`\n * via `hono/vercel`'s `handle`. It is read-only — no database, admin UI, or\n * auth, unlike the Cloudflare scaffold: the profile is the bundled\n * `takuhon.json`, edited in Git and redeployed. The Cloudflare-only files\n * (`wrangler.toml`, the Worker entry, the admin bundle) are intentionally\n * absent.\n */\n\nimport type { ContentLicenseFragment } from '../licenses.js';\n\nimport { TAKUHON_DEP_RANGE } from './deps.js';\n\n/**\n * Validate a Vercel project name (also used as the npm `name`).\n *\n * Vercel project names are lowercase, up to 100 chars, and may contain letters,\n * digits, `.`, `_`, and `-`; they must start and end with an alphanumeric. This\n * is deliberately looser than the Cloudflare Worker-name rule (which maps to a\n * `workers.dev` DNS label) but still a safe npm `name`.\n */\nexport function isValidVercelProjectName(name: string): boolean {\n return /^[a-z0-9](?:[a-z0-9._-]{0,98}[a-z0-9])?$/.test(name);\n}\n\nexport interface VercelPackageJsonOptions {\n /** Used for the npm `name` field. */\n readonly projectName: string;\n}\n\nexport function buildVercelPackageJson(opts: VercelPackageJsonOptions): Record<string, unknown> {\n return {\n name: opts.projectName,\n version: '0.0.0',\n private: true,\n description: 'Takuhon profile deployment (Vercel).',\n scripts: {\n dev: 'next dev',\n build: 'next build',\n start: 'next start',\n },\n dependencies: {\n '@takuhon/vercel': TAKUHON_DEP_RANGE,\n hono: '^4.0.0',\n next: '^15.0.0',\n react: '^19.0.0',\n 'react-dom': '^19.0.0',\n },\n devDependencies: {\n '@types/node': '^22.0.0',\n typescript: '^5.7.0',\n },\n engines: {\n node: '>=22.0.0',\n },\n };\n}\n\nexport function renderVercelPackageJson(opts: VercelPackageJsonOptions): string {\n return `${JSON.stringify(buildVercelPackageJson(opts), null, 2)}\\n`;\n}\n\n/**\n * The catch-all Route Handler at `app/[[...route]]/route.ts`. The optional\n * catch-all matches `/` and every sub-path, so a single file serves the whole\n * public surface from `@takuhon/vercel`. `BundledTakuhonStorage` validates the\n * profile once at module load, so an invalid `takuhon.json` fails the build.\n */\nexport function renderVercelRouteTs(): string {\n return `import { BundledTakuhonStorage, createTakuhonVercelApp } from '@takuhon/vercel';\nimport { handle } from 'hono/vercel';\n\n// The project's own profile, bundled into the deployment. Edit takuhon.json and\n// redeploy (push to your Vercel-connected Git repo, or run \\`vercel\\`) to publish\n// changes — this adapter is read-only at runtime.\nimport takuhonJson from '../../takuhon.json';\n\nconst app = createTakuhonVercelApp({\n storage: new BundledTakuhonStorage(takuhonJson),\n});\n\nexport const GET = handle(app);\n`;\n}\n\n/** Minimal `next.config.mjs`. */\nexport function renderNextConfig(): string {\n return `/** @type {import('next').NextConfig} */\nconst nextConfig = {};\n\nexport default nextConfig;\n`;\n}\n\n/** `tsconfig.json` for the Next.js project (standard App Router settings). */\nexport function renderVercelTsconfigJson(): string {\n const config = {\n compilerOptions: {\n target: 'ES2022',\n lib: ['dom', 'dom.iterable', 'esnext'],\n allowJs: true,\n skipLibCheck: true,\n strict: true,\n noEmit: true,\n esModuleInterop: true,\n module: 'esnext',\n moduleResolution: 'bundler',\n resolveJsonModule: true,\n isolatedModules: true,\n jsx: 'preserve',\n incremental: true,\n plugins: [{ name: 'next' }],\n },\n include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],\n exclude: ['node_modules'],\n };\n return `${JSON.stringify(config, null, 2)}\\n`;\n}\n\n/** `.gitignore` for a Next.js / Vercel project. */\nexport function renderVercelGitignore(): string {\n return `# Dependencies\nnode_modules/\n\n# Next.js build output\n.next/\nnext-env.d.ts\n\n# Vercel\n.vercel/\n\n# Environment variables (commit \\`.env.example\\` instead)\n.env\n.env.local\n.env.*.local\n\n# Logs\n*.log\nnpm-debug.log*\npnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# macOS\n.DS_Store\n`;\n}\n\n/** `.env.example` — only the optional remote-profile URL applies to Vercel. */\nexport function renderVercelEnvExample(): string {\n return `# ----------------------------------------------------------------\n# Local development variables for this Takuhon deployment (Vercel).\n#\n# This adapter is read-only: the profile is the bundled takuhon.json, edited in\n# Git. There are no admin tokens or KV/R2 bindings.\n# ----------------------------------------------------------------\n\n# Optional: fetch the profile from a URL at cold start instead of bundling it.\n# Leave unset to use the committed takuhon.json (the default). When set, switch\n# the route to UrlTakuhonStorage (see app/[[...route]]/route.ts).\n# TAKUHON_DATA_URL=https://example.com/takuhon.json\n`;\n}\n\nexport interface VercelReadmeOptions {\n readonly projectName: string;\n readonly license: ContentLicenseFragment;\n}\n\nfunction formatLicenseLine(license: ContentLicenseFragment): string {\n if (license.spdxId === 'Proprietary') {\n return '**Proprietary** — all rights reserved.';\n }\n if (license.url !== undefined) {\n return `[\\`${license.spdxId}\\`](${license.url})`;\n }\n return `\\`${license.spdxId}\\``;\n}\n\nexport function renderVercelReadme(opts: VercelReadmeOptions): string {\n const { projectName, license } = opts;\n const licenseLine = formatLicenseLine(license);\n return `# ${projectName}\n\nA [Takuhon](https://github.com/takuhon-dev/takuhon) profile deployment, running on [Vercel](https://vercel.com) via the read-only \\`@takuhon/vercel\\` adapter.\n\n> **Status**: pre-deploy. Edit \\`takuhon.json\\`, then deploy to Vercel.\n\n## What is Takuhon?\n\nTakuhon lets you own your profile as a portable JSON document and publish it as a mobile-first profile page plus a public API (JSON-LD for AI agents and search engines included).\n\n## How it works\n\nThis project is a minimal Next.js App Router app. The catch-all route at\n\\`app/[[...route]]/route.ts\\` mounts \\`@takuhon/vercel\\`, which serves:\n\n- \\`/\\` and \\`/<locale>/\\` — the server-rendered profile page with embedded Schema.org JSON-LD\n- \\`/api/profile\\`, \\`/api/jsonld\\`, \\`/api/schema\\`\n- \\`/takuhon.json\\`, \\`/.well-known/takuhon.json\\`\n\nIt is **read-only**: there is no admin UI, auth, or database. Your profile is the\ncommitted \\`takuhon.json\\` — edit it and redeploy to publish changes. (Image\nuploads, the MCP endpoint, and the activity badge are Cloudflare-adapter\nfeatures; use \\`@takuhon/cloudflare\\` if you need them.)\n\n## Setup\n\n1. **Install dependencies.**\n\n \\`\\`\\`sh\n pnpm install\n \\`\\`\\`\n\n2. **Edit your profile.** Open \\`takuhon.json\\` and replace the sample fields (\\`profile.displayName\\`, \\`links\\`, \\`careers\\`, \\`projects\\`, \\`skills\\`) with your own.\n\n3. **Run it locally.**\n\n \\`\\`\\`sh\n pnpm dev\n \\`\\`\\`\n\n Open the printed URL: \\`/\\` serves your profile page and \\`/api/*\\` serves the JSON API.\n\n## Deploy\n\nPush this project to a Git repository connected to Vercel, or deploy from the CLI:\n\n\\`\\`\\`sh\nnpx vercel\n\\`\\`\\`\n\nTo publish changes later, edit \\`takuhon.json\\`, commit, and push — Vercel redeploys automatically.\n\n## License\n\nProfile content (\\`takuhon.json\\`) is licensed under ${licenseLine}\n\nThe deployment code is your own; pick a license appropriate for it.\n`;\n}\n","/**\n * Generator for `wrangler.toml` in a freshly scaffolded project.\n *\n * The layout mirrors the reference Cloudflare adapter shipped in this\n * monorepo: a single KV namespace `TAKUHON_KV`, placeholder ids the user\n * fills in after running `wrangler kv namespace create`, and the\n * `TAKUHON_ADMIN_ORIGIN` var defaulting to \"\" (disabled). The admin bearer\n * token is provisioned as a Wrangler secret and is therefore not in this file.\n */\n\n/**\n * Validate a Cloudflare Worker name.\n *\n * Cloudflare itself accepts mixed-case DNS labels (1–63 chars, must not\n * start or end with a hyphen). This validator additionally enforces a\n * lowercase convention chosen for the scaffolder so that the resulting\n * `workers.dev` subdomain is predictable and matches the npm `name` field.\n */\nexport function isValidWorkerName(name: string): boolean {\n return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(name);\n}\n\n/**\n * In-project directory the admin SPA bundle is copied into and the scaffolded\n * `wrangler.toml` binds as `ASSETS`. Shared so the copy destination and the\n * `[assets] directory` value cannot drift apart. Defined in this leaf module\n * (which `scaffold/index.ts` already imports) to avoid an import cycle.\n */\nexport const ADMIN_DIST_DIRNAME = 'admin-dist';\n\n/**\n * Render `wrangler.toml` for the given project name.\n *\n * Throws if `projectName` is not a valid Cloudflare Worker name. Callers are\n * responsible for sanitising/validating earlier in the prompt flow.\n */\nexport function renderWranglerToml(projectName: string): string {\n if (!isValidWorkerName(projectName)) {\n throw new Error(\n `Invalid Cloudflare Worker name: \"${projectName}\". Names must be lowercase, ` +\n `start with a letter or digit, and contain only letters, digits, and hyphens (max 63 chars).`,\n );\n }\n return `name = \"${projectName}\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2026-05-01\"\ncompatibility_flags = [\"nodejs_compat\"]\n\n# Admin SPA bundle. The React admin UI is committed in ./${ADMIN_DIST_DIRNAME}\n# (copied here when this project was created with create-takuhon) and served at\n# /admin.\n#\n# run_worker_first = true makes the Worker mediate every request: the bundle's\n# files sit at the assets root, so without this they would shadow the public \"/\"\n# route. The Worker serves the bundle only for /admin/* (attaching the strict\n# admin CSP) and handles everything else itself.\n#\n# SECURITY-CRITICAL: do not set run_worker_first to false or remove it. The\n# Worker is the only thing that attaches the strict admin CSP (plus HSTS /\n# no-store) to the admin assets; serving them directly would expose the admin UI\n# with no policy. To deploy without the form UI, remove this whole [assets]\n# block — the Worker then falls back to a minimal inline editor.\n[assets]\ndirectory = \"${ADMIN_DIST_DIRNAME}\"\nbinding = \"ASSETS\"\nrun_worker_first = true\nnot_found_handling = \"single-page-application\"\n\n# Replace the placeholder ids after running:\n# wrangler kv namespace create TAKUHON_KV\n# wrangler kv namespace create TAKUHON_KV --preview\n[[kv_namespaces]]\nbinding = \"TAKUHON_KV\"\nid = \"REPLACE_WITH_PRODUCTION_NAMESPACE_ID\"\npreview_id = \"REPLACE_WITH_PREVIEW_NAMESPACE_ID\"\n\n# Admin Origin allowlist (comma-separated). Empty value disables the check.\n# Example for production: TAKUHON_ADMIN_ORIGIN = \"https://admin.example.com\"\n#\n# The admin bearer token MUST be provisioned as a Wrangler secret, never in\n# this file:\n# wrangler secret put TAKUHON_ADMIN_TOKEN\n# Use 32+ bytes of entropy, e.g. \\`openssl rand -base64 32\\`.\n[vars]\nTAKUHON_ADMIN_ORIGIN = \"\"\n\n# Image uploads (optional). Bind an R2 bucket to enable \\`POST /api/admin/assets\\`\n# and \\`GET /assets/*\\` delivery; without it the upload endpoint stays\n# unregistered (404) and avatars remain URL-only. Create the bucket, then\n# uncomment:\n# wrangler r2 bucket create ${projectName}-assets\n# [[r2_buckets]]\n# binding = \"TAKUHON_R2\"\n# bucket_name = \"${projectName}-assets\"\n\n# Developer-activity dashboard (optional). Enable settings.activity in your\n# takuhon.json, then uncomment the cron below to refresh the snapshot daily:\n# the Worker's scheduled handler fetches GitHub / WakaTime and stores the\n# result in the TAKUHON_KV namespace (a failed run keeps the last-known\n# snapshot). Secrets MUST be Wrangler secrets, never this file:\n# wrangler secret put TAKUHON_GITHUB_TOKEN # optional (contribution calendar)\n# wrangler secret put TAKUHON_WAKATIME_KEY # required for WakaTime\n# [triggers]\n# crons = [\"17 3 * * *\"]\n`;\n}\n","/**\n * Top-level scaffolding orchestrator for `create-takuhon`.\n *\n * `writeProject` creates the target directory (must not already exist), then\n * writes the eight files that make up the scaffold: `takuhon.json`,\n * `wrangler.toml`, `package.json`, `README.md`, `.gitignore`, `.env.example`,\n * `tsconfig.json`, and `src/index.ts` (the Cloudflare Worker entry composed via\n * `createTakuhonWorker` from `@takuhon/cloudflare`).\n *\n * `copyAdminBundle` then copies the bundled admin SPA into the project's\n * `admin-dist/` so the Worker can serve the form UI at `/admin`. `init.ts`\n * calls both in sequence.\n */\n\nimport { cp, mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport type { ContentLicenseFragment } from '../licenses.js';\n\nimport { renderEnvExample } from './env-example.js';\nimport { renderGitignore } from './gitignore.js';\nimport { renderPackageJson } from './package-json.js';\nimport { renderReadme } from './readme.js';\nimport { renderTakuhonJson } from './takuhon-json.js';\nimport { renderTsconfigJson } from './tsconfig-json.js';\nimport {\n renderNextConfig,\n renderVercelEnvExample,\n renderVercelGitignore,\n renderVercelPackageJson,\n renderVercelReadme,\n renderVercelRouteTs,\n renderVercelTsconfigJson,\n} from './vercel.js';\nimport { renderWorkerIndexTs } from './worker-index-ts.js';\nimport { ADMIN_DIST_DIRNAME, renderWranglerToml } from './wrangler-toml.js';\n\n/** Target platform for a scaffolded project. */\nexport type ScaffoldPlatform = 'cloudflare' | 'vercel';\n\nexport interface WriteProjectOptions {\n /** Absolute path of the directory to create. Must not exist. */\n readonly targetDir: string;\n /** Used for the npm `name`, Worker name, and README header. */\n readonly projectName: string;\n /** Chosen content license (already mapped via `buildContentLicense`). */\n readonly license: ContentLicenseFragment;\n /** Target platform. Defaults to `'cloudflare'` (backwards-compatible). */\n readonly platform?: ScaffoldPlatform;\n}\n\nexport interface WriteProjectResult {\n /** Relative paths of files that were written (in the order written). */\n readonly files: readonly string[];\n}\n\n/**\n * Error thrown when the target directory already exists. The caller is\n * expected to render a friendly message that references the user-supplied\n * path; the `code` is stable for tests. The absolute `targetDir` is exposed\n * as a field rather than embedded in the message so that bubbling the error\n * up through generic logging does not leak filesystem layout.\n */\nexport class TargetDirectoryExistsError extends Error {\n override readonly name = 'TargetDirectoryExistsError';\n readonly code = 'TARGET_EXISTS' as const;\n constructor(readonly targetDir: string) {\n super('Target directory already exists.');\n }\n}\n\n/**\n * Create the project directory and write the scaffolded files. Order is\n * deterministic so callers (and tests) can rely on it.\n */\nexport async function writeProject(opts: WriteProjectOptions): Promise<WriteProjectResult> {\n const { targetDir, projectName, license, platform = 'cloudflare' } = opts;\n\n // mkdir with recursive: false fails if the directory exists; we surface a\n // typed error so the CLI entry can render a friendly message.\n try {\n await mkdir(targetDir, { recursive: false });\n } catch (err) {\n if (isNodeErrnoException(err) && err.code === 'EEXIST') {\n throw new TargetDirectoryExistsError(targetDir);\n }\n throw err;\n }\n\n const files: { readonly path: string; readonly content: string }[] =\n platform === 'vercel'\n ? [\n { path: 'takuhon.json', content: renderTakuhonJson(license) },\n { path: 'package.json', content: renderVercelPackageJson({ projectName }) },\n { path: 'README.md', content: renderVercelReadme({ projectName, license }) },\n { path: '.gitignore', content: renderVercelGitignore() },\n { path: '.env.example', content: renderVercelEnvExample() },\n { path: 'tsconfig.json', content: renderVercelTsconfigJson() },\n { path: 'next.config.mjs', content: renderNextConfig() },\n { path: 'app/[[...route]]/route.ts', content: renderVercelRouteTs() },\n ]\n : [\n { path: 'takuhon.json', content: renderTakuhonJson(license) },\n { path: 'wrangler.toml', content: renderWranglerToml(projectName) },\n { path: 'package.json', content: renderPackageJson({ projectName }) },\n { path: 'README.md', content: renderReadme({ projectName, license }) },\n { path: '.gitignore', content: renderGitignore() },\n { path: '.env.example', content: renderEnvExample() },\n { path: 'tsconfig.json', content: renderTsconfigJson() },\n { path: 'src/index.ts', content: renderWorkerIndexTs() },\n ];\n\n for (const { path, content } of files) {\n const fullPath = join(targetDir, path);\n const parent = dirname(fullPath);\n if (parent !== targetDir) {\n await mkdir(parent, { recursive: true });\n }\n await writeFile(fullPath, content, 'utf8');\n }\n\n return { files: files.map((f) => f.path) };\n}\n\nfunction isNodeErrnoException(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && typeof (err as NodeJS.ErrnoException).code === 'string';\n}\n\n/**\n * Resolve the admin SPA bundle that ships inside this package. The bundle is\n * copied here from `apps/admin/dist` at build time (see\n * `scripts/copy-admin-bundle.mjs`) and listed in the package's `files`, so it\n * is present in the published `@takuhon/cli` tarball. Resolved relative to the\n * compiled module (`dist/init.js` or `dist/index.js`), whose sibling is\n * `admin-bundle/`.\n */\nexport function resolveAdminBundleDir(): string {\n return fileURLToPath(new URL('../admin-bundle', import.meta.url));\n}\n\nexport interface CopyAdminBundleOptions {\n /** Project directory previously created by {@link writeProject}. */\n readonly targetDir: string;\n /**\n * Source bundle directory. Defaults to the bundle shipped in this package\n * ({@link resolveAdminBundleDir}); overridable for tests.\n */\n readonly bundleDir?: string;\n}\n\nexport interface CopyAdminBundleResult {\n /** Absolute path of the directory the bundle was copied into. */\n readonly dest: string;\n}\n\n/**\n * Copy the admin SPA bundle into the scaffolded project's\n * `{@link ADMIN_DIST_DIRNAME}` directory. The generated `wrangler.toml` binds\n * this directory as `ASSETS`, and the Cloudflare Worker serves it at `/admin`\n * under a strict CSP (falling back to an inline editor when the binding is\n * absent). Kept separate from {@link writeProject} so the static-asset copy and\n * the generated-file rendering are independently testable.\n */\nexport async function copyAdminBundle(\n opts: CopyAdminBundleOptions,\n): Promise<CopyAdminBundleResult> {\n const bundleDir = opts.bundleDir ?? resolveAdminBundleDir();\n const dest = join(opts.targetDir, ADMIN_DIST_DIRNAME);\n try {\n await cp(bundleDir, dest, { recursive: true });\n } catch (err) {\n // The bundle ships inside @takuhon/cli, so a failure here means a broken\n // or incomplete install. Surface a sanitized message — the raw error would\n // embed the absolute node_modules path of this package.\n const code = isNodeErrnoException(err) ? ` (${err.code})` : '';\n throw new Error(\n `Failed to copy the admin UI bundle from @takuhon/cli${code}. ` +\n `Reinstall or upgrade @takuhon/cli and try again.`,\n { cause: err },\n );\n }\n return { dest };\n}\n","/**\n * Generator for the `.env.example` file in a freshly scaffolded project.\n *\n * The Wrangler-managed secrets (admin bearer token) are *not* read from `.env`\n * at deploy time — `wrangler secret put` is the source of truth. The\n * `.env.example` is therefore documentation: it lists the variables a developer\n * may want locally (e.g. for scripting or for the wrangler CLI itself) and\n * links each to its production provisioning path.\n */\n\nexport function renderEnvExample(): string {\n return `# ----------------------------------------------------------------\n# Local development variables for this Takuhon deployment.\n#\n# Production secrets (admin token) are provisioned via:\n# wrangler secret put TAKUHON_ADMIN_TOKEN\n# This file is a hint for local tooling, not a runtime source.\n# ----------------------------------------------------------------\n\n# Admin bearer token for /api/admin/* endpoints.\n# Generate with: openssl rand -base64 32\n# Then provision with:\n# echo \"$TAKUHON_ADMIN_TOKEN\" | wrangler secret put TAKUHON_ADMIN_TOKEN\nTAKUHON_ADMIN_TOKEN=\n\n# Comma-separated Origin allowlist for browser-originating admin requests.\n# Set in wrangler.toml [vars] for production. Local example:\n# TAKUHON_ADMIN_ORIGIN=https://admin.example.com,https://localhost:8787\nTAKUHON_ADMIN_ORIGIN=\n\n# Developer-activity sync secrets (only needed when settings.activity is\n# enabled in takuhon.json). Used by \\`takuhon activity sync\\` locally; for a\n# deployed Worker provision them as Wrangler secrets instead:\n# wrangler secret put TAKUHON_GITHUB_TOKEN\n# wrangler secret put TAKUHON_WAKATIME_KEY\n# Never commit the real values.\n#\n# Optional GitHub token: languages work without it; the contribution calendar\n# requires it (minimal read-only scope is enough).\nTAKUHON_GITHUB_TOKEN=\n# WakaTime API key (https://wakatime.com/settings/api-key); required to read\n# coding time.\nTAKUHON_WAKATIME_KEY=\n\n# Cloudflare account id (look up via \\`wrangler whoami\\`). Only needed if you\n# script Cloudflare API calls outside Wrangler itself.\nCLOUDFLARE_ACCOUNT_ID=\n`;\n}\n","/**\n * Generator for the scaffolded project's `.gitignore`.\n *\n * Covers Node, Wrangler local cache, and environment files. Kept conservative\n * — no opinions about lockfile preferences (the user picks their package\n * manager) or editor folders (per-editor patterns belong in a global ignore).\n */\n\nexport function renderGitignore(): string {\n return `# Dependencies\nnode_modules/\n\n# Wrangler local cache (do not commit; contains bundled output and tmp files)\n.wrangler/\n\n# Environment variables (commit \\`.env.example\\` instead)\n.env\n.env.local\n.env.*.local\n\n# Logs\n*.log\nnpm-debug.log*\npnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# macOS\n.DS_Store\n`;\n}\n","/**\n * Generator for the scaffolded project's `package.json`.\n *\n * Pinned dependency versions are caret ranges matched to the current\n * minor of the published `@takuhon/*` packages. Under 0.x semver, a caret\n * does not span minor versions, so the range must move forward with each\n * `@takuhon/core` minor release to keep scaffolded projects on the\n * matching schema generation. A guard test (`scaffold.test.ts`) asserts these\n * pins track the published minor, so a missed bump fails CI rather than\n * silently shipping a stale range. `hono` is included as a direct dependency for\n * projects that extend the Worker with their own Hono routes; the generated\n * `src/index.ts` reaches Takuhon's handlers through `@takuhon/cloudflare`\n * rather than importing `hono` directly. `wrangler` is a devDependency since\n * it's the deploy / dev-server tool.\n */\n\nimport { TAKUHON_DEP_RANGE } from './deps.js';\n\nexport interface PackageJsonOptions {\n /** Used for the npm `name` field. */\n readonly projectName: string;\n}\n\nexport function buildPackageJson(opts: PackageJsonOptions): Record<string, unknown> {\n return {\n name: opts.projectName,\n version: '0.0.0',\n private: true,\n type: 'module',\n description: 'Takuhon profile deployment.',\n scripts: {\n dev: 'wrangler dev',\n deploy: 'wrangler deploy',\n },\n dependencies: {\n '@takuhon/api': TAKUHON_DEP_RANGE,\n '@takuhon/cloudflare': TAKUHON_DEP_RANGE,\n '@takuhon/core': TAKUHON_DEP_RANGE,\n hono: '^4.0.0',\n },\n devDependencies: {\n wrangler: '^4.0.0',\n },\n engines: {\n node: '>=22.0.0',\n },\n };\n}\n\nexport function renderPackageJson(opts: PackageJsonOptions): string {\n return `${JSON.stringify(buildPackageJson(opts), null, 2)}\\n`;\n}\n","/**\n * Generator for the README.md placed in a freshly scaffolded project.\n *\n * Aims to give a first-time user enough to (1) install dependencies,\n * (2) fill in their profile data, (3) provision Cloudflare KV + admin\n * secret, and (4) `pnpm dev` / `pnpm deploy`. Cross-references the published\n * `@takuhon/cloudflare` adapter README as the next-step reference for the\n * route map.\n */\n\nimport type { ContentLicenseFragment } from '../licenses.js';\n\nexport interface ReadmeOptions {\n readonly projectName: string;\n readonly license: ContentLicenseFragment;\n}\n\n/**\n * Human-readable license attribution line for the README footer. Uses the\n * canonical URL when available; falls back to plain SPDX (or\n * \"All rights reserved\" for Proprietary).\n */\nfunction formatLicenseLine(license: ContentLicenseFragment): string {\n if (license.spdxId === 'Proprietary') {\n return '**Proprietary** — all rights reserved.';\n }\n if (license.url !== undefined) {\n return `[\\`${license.spdxId}\\`](${license.url})`;\n }\n return `\\`${license.spdxId}\\``;\n}\n\nexport function renderReadme(opts: ReadmeOptions): string {\n const { projectName, license } = opts;\n const licenseLine = formatLicenseLine(license);\n return `# ${projectName}\n\nA [Takuhon](https://github.com/takuhon-dev/takuhon) profile deployment, running on Cloudflare Workers.\n\n> **Status**: pre-deploy. Edit \\`takuhon.json\\`, provision Cloudflare KV, then \\`pnpm deploy\\`.\n\n## What is Takuhon?\n\nTakuhon lets you own your profile as a portable JSON document and publish it as a mobile-first profile page plus a public API (JSON-LD for AI agents and search engines included).\n\n## Setup\n\n1. **Install dependencies.**\n\n \\`\\`\\`sh\n pnpm install\n \\`\\`\\`\n\n2. **Edit your profile.** Open \\`takuhon.json\\` and replace the sample fields (\\`profile.displayName\\`, \\`links\\`, \\`careers\\`, \\`projects\\`, \\`skills\\`) with your own.\n\n3. **Create the Cloudflare KV namespaces** and copy the returned ids into \\`wrangler.toml\\`:\n\n \\`\\`\\`sh\n npx wrangler kv namespace create TAKUHON_KV\n npx wrangler kv namespace create TAKUHON_KV --preview\n \\`\\`\\`\n\n4. **Provision the admin token** as a Wrangler secret (used by \\`/api/admin/*\\`):\n\n \\`\\`\\`sh\n openssl rand -base64 32 | npx wrangler secret put TAKUHON_ADMIN_TOKEN\n \\`\\`\\`\n\n Leaving the secret unset disables admin writes entirely (every \\`PUT\\` / \\`DELETE\\` returns 401).\n\n## Develop\n\nThe Worker entry at \\`src/index.ts\\` composes the takuhon adapter via\n\\`createTakuhonWorker\\` and serves your \\`takuhon.json\\` as the fallback when\nKV has no stored profile yet.\n\n\\`\\`\\`sh\npnpm dev # runs \\`wrangler dev\\` locally\n\\`\\`\\`\n\nOpen the local URL it prints: the root serves your **profile page** — the same HTML a visitor and search-engine crawlers see in production, with Schema.org JSON-LD embedded — and the JSON API lives under \\`/api/*\\`. The full route map (public + admin) is documented in the [\\`@takuhon/cloudflare\\` README](https://github.com/takuhon-dev/takuhon/tree/main/adapters/cloudflare#readme).\n\n## Admin\n\nThe admin form UI is served at \\`/admin\\` under a strict Content-Security-Policy. Its compiled bundle is committed in \\`admin-dist/\\` and bound as Workers Assets in \\`wrangler.toml\\`. Sign in with the admin token you set as the \\`TAKUHON_ADMIN_TOKEN\\` Wrangler secret.\n\nThe bundle is a snapshot taken when this project was created. To pick up a newer admin UI, upgrade your \\`takuhon\\` CLI and run \\`takuhon admin update\\` in this project — it replaces \\`admin-dist/\\` with the bundle shipped in your installed \\`@takuhon/cli\\` (keep your \\`@takuhon/*\\` dependencies on a matching version). To deploy without the form UI, remove the \\`[assets]\\` block from \\`wrangler.toml\\`; the Worker then falls back to a minimal inline editor.\n\n### Image uploads (optional)\n\nImage uploads are off until you bind an R2 bucket. Create one and uncomment the \\`[[r2_buckets]]\\` block in \\`wrangler.toml\\`:\n\n\\`\\`\\`sh\nnpx wrangler r2 bucket create ${projectName}-assets\n\\`\\`\\`\n\nThe Worker then accepts uploads at \\`POST /api/admin/assets\\` (magic-byte check, 5 MB / 4096px limits, EXIF/metadata stripping) and serves them at \\`GET /assets/*\\` with \\`X-Content-Type-Options: nosniff\\`. Without the binding, avatars stay URL-only.\n\n### Developer-activity dashboard (optional)\n\nShow your GitHub language mix / contribution calendar and WakaTime coding time on the profile. The data is synced into a snapshot ahead of time — rendering never calls GitHub or WakaTime, and the API keys never leave the sync step.\n\n1. Opt in by adding \\`settings.activity\\` to \\`takuhon.json\\`:\n\n \\`\\`\\`jsonc\n \"settings\": {\n \"activity\": {\n \"enabled\": true,\n \"github\": { \"username\": \"your-github-login\" },\n \"wakatime\": { \"username\": \"your-wakatime-username\" }\n }\n }\n \\`\\`\\`\n\n2. Provision the sync secrets (see \\`.env.example\\`): \\`TAKUHON_WAKATIME_KEY\\` is required for coding time; \\`TAKUHON_GITHUB_TOKEN\\` is optional — languages work without it, the contribution calendar needs it.\n\n \\`\\`\\`sh\n npx wrangler secret put TAKUHON_GITHUB_TOKEN\n npx wrangler secret put TAKUHON_WAKATIME_KEY\n \\`\\`\\`\n\n3. Uncomment the \\`[triggers]\\` cron block in \\`wrangler.toml\\` so the Worker refreshes the snapshot daily, or run \\`takuhon activity sync\\` locally to write an \\`activity.json\\` next to your profile for \\`takuhon build\\` / \\`dev\\`.\n\nThe snapshot is served at \\`GET /api/activity\\` while enabled; a failed sync keeps the last-known data.\n\n### GitHub profile badge\n\nThe activity card is also available as a standalone SVG you can embed as an image — e.g. in your GitHub profile \\`README.md\\`:\n\n\\`\\`\\`md\n![My developer activity](https://your-worker.example/activity.svg)\n\\`\\`\\`\n\nTwo ways to host it (both require \\`settings.activity.enabled\\`):\n\n- **Dynamic (Worker):** \\`GET /activity.svg\\` renders the latest snapshot on every request. Add \\`?theme=dark\\` for the dark card.\n- **Static (\\`takuhon build\\`):** writes \\`dist/activity.svg\\` and \\`dist/activity-dark.svg\\` beside the pages, so a static deploy can serve the badge too.\n\nThe card carries an opaque background, so it stays readable on either GitHub theme. To follow the viewer's OS colour scheme automatically, use \\`<picture>\\`:\n\n\\`\\`\\`html\n<picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://your-worker.example/activity.svg?theme=dark\">\n <img alt=\"My developer activity\" src=\"https://your-worker.example/activity.svg\">\n</picture>\n\\`\\`\\`\n\nGitHub proxies the image through Camo and caches it for a few hours, so a fresh sync can take a little while to appear in your README.\n\n## Résumé / CV (optional)\n\nGenerate a print-ready CV from the same \\`takuhon.json\\` — no re-entry:\n\n\\`\\`\\`sh\nnpx takuhon build --cv\n\\`\\`\\`\n\nThis writes \\`dist/cv.html\\` (and \\`dist/<locale>/cv.html\\` for each locale) alongside the profile pages. Open it in a browser and use **Print → Save as PDF** to produce a résumé PDF. \\`npx takuhon dev\\` also serves it at \\`/cv\\` for previewing.\n\n## AI agents (MCP)\n\nYour profile is readable over the [Model Context Protocol](https://modelcontextprotocol.io) — read-only, the same data the public API exposes (with the privacy filter applied), no write access.\n\n- **Deployed:** the Worker serves a stateless MCP endpoint at \\`POST /mcp\\` (no extra binding or setup). Point an MCP client at \\`https://your-worker.example/mcp\\`. It is also advertised as \\`mcp\\` in \\`/.well-known/takuhon.json\\`.\n- **Local:** run an MCP server over stdio against your \\`takuhon.json\\`, e.g. for Claude Desktop:\n\n \\`\\`\\`jsonc\n // claude_desktop_config.json\n {\n \"mcpServers\": {\n \"my-profile\": { \"command\": \"npx\", \"args\": [\"takuhon\", \"mcp\", \"/path/to/takuhon.json\"] }\n }\n }\n \\`\\`\\`\n\nBoth expose the tools \\`get_profile\\`, \\`get_section\\`, \\`get_jsonld\\`, \\`list_locales\\` and the resources \\`takuhon://profile\\`, \\`takuhon://schema\\`.\n\n## Deploy\n\n\\`\\`\\`sh\npnpm deploy # runs \\`wrangler deploy\\`\n\\`\\`\\`\n\nOnce deployed, your Worker's root URL serves the **mobile profile page** with Schema.org JSON-LD embedded for search engines and AI agents; \\`/<locale>/\\` serves each translated page, and the JSON API stays under \\`/api/*\\`. Edits made in \\`/admin\\` appear on the page immediately.\n\n## License\n\nProfile content (\\`takuhon.json\\`) is licensed under ${licenseLine}\n\nThe deployment code is your own; pick a license appropriate for it.\n`;\n}\n","/**\n * Generator for the `takuhon.json` file written into a freshly scaffolded\n * project.\n *\n * The template is a copy of `examples/minimal-profile/takuhon.json` from the\n * monorepo. It is inlined here as a TypeScript constant rather than imported\n * from `examples/` so the published `@takuhon/cli` npm package does not need\n * to ship the examples directory.\n *\n * Only `meta.contentLicense` is rewritten per the user's choice; everything\n * else is the canonical minimal profile (one career, one project, three\n * skills, `en` locale only) that downstream `@takuhon/core` validation\n * already accepts (see `packages/core/src/__tests__/examples-fixtures.test.ts`).\n */\n\nimport type { ContentLicenseFragment } from '../licenses.js';\n\n/**\n * Build the `takuhon.json` payload as a deterministic, schema-valid object.\n * `meta.contentLicense` is filled from the supplied fragment so each\n * generated project carries the user's chosen SPDX identifier (and `url` /\n * `rights` where applicable).\n */\nexport function buildTakuhonJson(license: ContentLicenseFragment): unknown {\n return {\n schemaVersion: '0.6.0',\n profile: {\n displayName: {\n en: 'Sam Lee',\n },\n },\n links: [\n {\n id: 'github',\n type: 'github',\n url: 'https://example.com/github/sam-lee',\n featured: true,\n },\n ],\n careers: [\n {\n id: 'first-job',\n organization: {\n en: 'Example Co.',\n },\n role: {\n en: 'Junior Software Engineer',\n },\n startDate: '2026-04',\n endDate: null,\n isCurrent: true,\n },\n ],\n projects: [\n {\n id: 'personal-homepage',\n title: {\n en: 'Personal homepage',\n },\n },\n ],\n skills: [\n { id: 'html', label: 'HTML' },\n { id: 'css', label: 'CSS' },\n { id: 'javascript', label: 'JavaScript' },\n ],\n contact: {},\n settings: {\n defaultLocale: 'en',\n availableLocales: ['en'],\n },\n meta: {\n contentLicense: { ...license },\n },\n };\n}\n\n/** Serialise to a UTF-8 string with trailing newline (POSIX-friendly). */\nexport function renderTakuhonJson(license: ContentLicenseFragment): string {\n return `${JSON.stringify(buildTakuhonJson(license), null, 2)}\\n`;\n}\n","/**\n * Generator for the scaffolded project's `tsconfig.json`.\n *\n * Minimal configuration that makes the generated `src/index.ts` typecheck\n * with `tsc --noEmit` in a freshly scaffolded directory. The settings\n * mirror the upstream takuhon monorepo defaults (ES2022 target, ESNext\n * modules, strict mode, `noUncheckedIndexedAccess`) and explicitly enable\n * the two flags the Worker entry depends on:\n *\n * - `resolveJsonModule` — so `import takuhonJson from '../takuhon.json'`\n * resolves at type-check time.\n * - `moduleResolution: \"Bundler\"` — so the Wrangler / esbuild-style\n * resolution (no need for explicit file extensions) is the source of\n * truth that matches what `wrangler dev` actually runs.\n *\n * `noEmit: true` keeps `tsc` purely a type-checker; Wrangler's bundler\n * produces the deployable artifact.\n */\n\nexport function renderTsconfigJson(): string {\n const config = {\n compilerOptions: {\n target: 'ES2022',\n lib: ['ES2022'],\n module: 'ESNext',\n moduleResolution: 'Bundler',\n esModuleInterop: true,\n resolveJsonModule: true,\n isolatedModules: true,\n verbatimModuleSyntax: true,\n strict: true,\n noUncheckedIndexedAccess: true,\n noImplicitOverride: true,\n noFallthroughCasesInSwitch: true,\n skipLibCheck: true,\n noEmit: true,\n },\n include: ['src/**/*'],\n exclude: ['node_modules'],\n };\n return `${JSON.stringify(config, null, 2)}\\n`;\n}\n","/**\n * Generator for `src/index.ts` — the Cloudflare Worker entry file written\n * into a freshly scaffolded project.\n *\n * The generated file uses `createTakuhonWorker()` from `@takuhon/cloudflare`\n * so the scaffolded project does not need to know the internal wiring\n * (Hono router, KV-backed storage, edge cache purger, console audit logger).\n * The user's own `takuhon.json` is loaded via an ES module JSON import,\n * validated once at module load, and the resulting `Takuhon` value is\n * served as the fallback when KV has no stored profile yet.\n *\n * `wrangler.toml`'s `main` field already points at `src/index.ts`, and\n * `nodejs_compat` is enabled, so the file works under `wrangler dev` /\n * `wrangler deploy` without further configuration.\n */\n\nexport function renderWorkerIndexTs(): string {\n return `import { createTakuhonWorker } from '@takuhon/cloudflare';\nimport { validate } from '@takuhon/core';\n\n// Use the project's own takuhon.json as the fallback served when KV has no\n// stored profile yet. Validated once at module load so a malformed profile\n// fails fast rather than at first request.\nimport takuhonJson from '../takuhon.json' with { type: 'json' };\n\nconst fallback = validate(takuhonJson);\nif (!fallback.ok) {\n throw new Error(\n 'Project takuhon.json failed validation: ' +\n fallback.errors.map((e) => \\`\\${e.pointer}: \\${e.message}\\`).join('; '),\n );\n}\n\nconst fallbackTakuhon = fallback.data;\n\nexport default createTakuhonWorker({\n fallback: () => fallbackTakuhon,\n});\n`;\n}\n"],"mappings":"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","names":["formatLicenseLine"]}
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ADMIN_DIST_DIRNAME,
4
4
  resolveAdminBundleDir
5
- } from "./chunk-H2RCCOPA.js";
5
+ } from "./chunk-WOSXRHGM.js";
6
6
 
7
7
  // src/index.ts
8
8
  import { readFileSync as readFileSync16, realpathSync } from "fs";
package/dist/init.js CHANGED
@@ -2,9 +2,10 @@
2
2
  import {
3
3
  TargetDirectoryExistsError,
4
4
  copyAdminBundle,
5
+ isValidVercelProjectName,
5
6
  isValidWorkerName,
6
7
  writeProject
7
- } from "./chunk-H2RCCOPA.js";
8
+ } from "./chunk-WOSXRHGM.js";
8
9
 
9
10
  // src/init.ts
10
11
  import { realpathSync } from "fs";
@@ -116,6 +117,7 @@ function parseCliArgs(argv) {
116
117
  strict: true,
117
118
  options: {
118
119
  license: { type: "string" },
120
+ platform: { type: "string" },
119
121
  help: { type: "boolean", short: "h" }
120
122
  }
121
123
  });
@@ -123,16 +125,20 @@ function parseCliArgs(argv) {
123
125
  targetArg: positionals[0],
124
126
  extraPositionals: positionals.slice(1),
125
127
  license: values.license,
128
+ platform: values.platform,
126
129
  help: values.help === true
127
130
  };
128
131
  }
129
132
  function printHelp() {
130
133
  process.stdout.write(
131
- `Usage: create-takuhon <target-dir> [--license <spdxId>]
134
+ `Usage: create-takuhon <target-dir> [--platform <cloudflare|vercel>] [--license <spdxId>]
132
135
 
133
136
  Scaffolds a Takuhon profile deployment in <target-dir>.
134
137
 
135
138
  Options:
139
+ --platform <name> Target platform: "cloudflare" (default) or "vercel".
140
+ Cloudflare is the full deployment (admin UI, KV);
141
+ Vercel is a read-only profile published from Git.
136
142
  --license <spdxId> Skip the interactive license prompt and use the
137
143
  given SPDX identifier (e.g. CC-BY-4.0, MIT,
138
144
  Proprietary). Useful for CI / automation.
@@ -171,12 +177,32 @@ async function main(argv) {
171
177
  printHelp();
172
178
  return 2;
173
179
  }
180
+ let platform;
181
+ if (parsed.platform === void 0 || parsed.platform === "cloudflare") {
182
+ platform = "cloudflare";
183
+ } else if (parsed.platform === "vercel") {
184
+ platform = "vercel";
185
+ } else {
186
+ process.stderr.write(
187
+ `Error: invalid --platform value "${parsed.platform}". Use "cloudflare" or "vercel".
188
+ `
189
+ );
190
+ return 2;
191
+ }
174
192
  const targetDir = resolve(process.cwd(), parsed.targetArg);
175
193
  const projectName = basename(targetDir);
176
- if (!isValidWorkerName(projectName)) {
194
+ if (platform === "cloudflare" && !isValidWorkerName(projectName)) {
177
195
  process.stderr.write(
178
196
  `Error: target directory basename "${projectName}" is not a valid Cloudflare Worker name.
179
197
  Names must be lowercase, start and end with a letter or digit, and contain only letters, digits, and hyphens (max 63 chars).
198
+ `
199
+ );
200
+ return 2;
201
+ }
202
+ if (platform === "vercel" && !isValidVercelProjectName(projectName)) {
203
+ process.stderr.write(
204
+ `Error: target directory basename "${projectName}" is not a valid Vercel project name.
205
+ Names must be lowercase, start and end with a letter or digit, and contain only letters, digits, dots, underscores, and hyphens (max 100 chars).
180
206
  `
181
207
  );
182
208
  return 2;
@@ -199,7 +225,7 @@ Names must be lowercase, start and end with a letter or digit, and contain only
199
225
  }
200
226
  const license = buildContentLicense(spdxId);
201
227
  try {
202
- await writeProject({ targetDir, projectName, license });
228
+ await writeProject({ targetDir, projectName, license, platform });
203
229
  } catch (err) {
204
230
  if (err instanceof TargetDirectoryExistsError) {
205
231
  cancel2(`Target directory already exists: ${parsed.targetArg}`);
@@ -207,12 +233,29 @@ Names must be lowercase, start and end with a letter or digit, and contain only
207
233
  }
208
234
  throw err;
209
235
  }
210
- try {
211
- await copyAdminBundle({ targetDir });
212
- } catch (err) {
213
- await rm(targetDir, { recursive: true, force: true });
214
- cancel2(err instanceof Error ? err.message : "Failed to copy the admin UI bundle.");
215
- return 1;
236
+ if (platform === "cloudflare") {
237
+ try {
238
+ await copyAdminBundle({ targetDir });
239
+ } catch (err) {
240
+ await rm(targetDir, { recursive: true, force: true });
241
+ cancel2(err instanceof Error ? err.message : "Failed to copy the admin UI bundle.");
242
+ return 1;
243
+ }
244
+ }
245
+ if (platform === "vercel") {
246
+ outro(
247
+ `Created ${projectName} (license: ${spdxId}) for Vercel.
248
+
249
+ Next steps:
250
+ cd ${parsed.targetArg}
251
+ pnpm install
252
+ # 1. Edit takuhon.json with your profile data
253
+ pnpm dev # local Next.js dev server
254
+ # 2. Deploy: push to a Git repo connected to Vercel, or run \`npx vercel\`
255
+
256
+ This adapter is read-only: edit takuhon.json and redeploy to publish changes.`
257
+ );
258
+ return 0;
216
259
  }
217
260
  outro(
218
261
  `Created ${projectName} (license: ${spdxId}).
package/dist/init.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/init.ts","../src/licenses.ts","../src/prompts.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * Entry point for `npx create-takuhon` (and `pnpm create takuhon`).\n *\n * Usage:\n * create-takuhon <target-dir> [--license <spdxId>]\n *\n * The positional target directory is required; it must not already exist (we\n * refuse to overwrite). `--license` skips the interactive picker for CI /\n * automation use.\n */\n\nimport { realpathSync } from 'node:fs';\nimport { rm } from 'node:fs/promises';\nimport { basename, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { parseArgs } from 'node:util';\n\nimport { cancel, intro, outro } from '@clack/prompts';\n\nimport { buildContentLicense, isValidSpdxInput } from './licenses.js';\nimport { promptLicense } from './prompts.js';\nimport { copyAdminBundle, TargetDirectoryExistsError, writeProject } from './scaffold/index.js';\nimport { isValidWorkerName } from './scaffold/wrangler-toml.js';\n\ninterface CliArgs {\n readonly targetArg: string | undefined;\n readonly extraPositionals: readonly string[];\n readonly license: string | undefined;\n readonly help: boolean;\n}\n\nfunction parseCliArgs(argv: readonly string[]): CliArgs {\n const { values, positionals } = parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n options: {\n license: { type: 'string' },\n help: { type: 'boolean', short: 'h' },\n },\n });\n return {\n targetArg: positionals[0],\n extraPositionals: positionals.slice(1),\n license: values.license,\n help: values.help === true,\n };\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n `Usage: create-takuhon <target-dir> [--license <spdxId>]\\n` +\n `\\n` +\n `Scaffolds a Takuhon profile deployment in <target-dir>.\\n` +\n `\\n` +\n `Options:\\n` +\n ` --license <spdxId> Skip the interactive license prompt and use the\\n` +\n ` given SPDX identifier (e.g. CC-BY-4.0, MIT,\\n` +\n ` Proprietary). Useful for CI / automation.\\n` +\n ` -h, --help Show this help.\\n`,\n );\n}\n\nasync function main(argv: readonly string[]): Promise<number> {\n let parsed: CliArgs;\n try {\n parsed = parseCliArgs(argv);\n } catch (err) {\n process.stderr.write(`${(err as Error).message}\\n\\n`);\n printHelp();\n return 2;\n }\n\n if (parsed.help) {\n printHelp();\n return 0;\n }\n\n if (parsed.targetArg === undefined) {\n process.stderr.write(`Error: missing target directory argument.\\n\\n`);\n printHelp();\n return 2;\n }\n\n if (parsed.extraPositionals.length > 0) {\n process.stderr.write(\n `Error: unexpected extra arguments: ${parsed.extraPositionals.join(' ')}\\n\\n`,\n );\n printHelp();\n return 2;\n }\n\n const targetDir = resolve(process.cwd(), parsed.targetArg);\n const projectName = basename(targetDir);\n\n if (!isValidWorkerName(projectName)) {\n process.stderr.write(\n `Error: target directory basename \"${projectName}\" is not a valid Cloudflare Worker name.\\n` +\n `Names must be lowercase, start and end with a letter or digit, and contain only ` +\n `letters, digits, and hyphens (max 63 chars).\\n`,\n );\n return 2;\n }\n\n intro('create-takuhon');\n\n let spdxId: string;\n if (parsed.license !== undefined) {\n const raw = parsed.license.trim();\n if (!isValidSpdxInput(raw)) {\n cancel(`Invalid --license value: \"${parsed.license}\"`);\n return 2;\n }\n spdxId = raw;\n } else {\n const result = await promptLicense();\n if ('cancelled' in result) {\n return 130;\n }\n spdxId = result.spdxId;\n }\n\n const license = buildContentLicense(spdxId);\n\n try {\n await writeProject({ targetDir, projectName, license });\n } catch (err) {\n if (err instanceof TargetDirectoryExistsError) {\n cancel(`Target directory already exists: ${parsed.targetArg}`);\n return 1;\n }\n throw err;\n }\n\n // Copy the bundled admin SPA into the project so the Worker can serve the\n // form UI at /admin. On failure, remove the just-created project directory so\n // the user can retry into the same path (writeProject refuses an existing one).\n try {\n await copyAdminBundle({ targetDir });\n } catch (err) {\n await rm(targetDir, { recursive: true, force: true });\n cancel(err instanceof Error ? err.message : 'Failed to copy the admin UI bundle.');\n return 1;\n }\n\n outro(\n `Created ${projectName} (license: ${spdxId}).\\n` +\n `\\n` +\n `Next steps:\\n` +\n ` cd ${parsed.targetArg}\\n` +\n ` pnpm install\\n` +\n ` # 1. Edit takuhon.json with your profile data\\n` +\n ` # 2. Provision Cloudflare KV: npx wrangler kv namespace create TAKUHON_KV\\n` +\n ` # 3. Set admin token: openssl rand -base64 32 | npx wrangler secret put TAKUHON_ADMIN_TOKEN\\n` +\n ` pnpm dev\\n` +\n `\\n` +\n `The admin form UI is served at /admin (bundled in admin-dist/); sign in\\n` +\n `with the admin token from step 3.`,\n );\n\n return 0;\n}\n\n/**\n * Process entry point: run {@link main} and exit with its code. This is the\n * only function that calls `process.exit`, mirroring the `takuhon` entry in\n * `index.ts`. Exported so the `create-takuhon` redirect package can invoke it\n * after importing this module (`import { run } from '@takuhon/cli/init'`).\n */\nexport async function run(argv: readonly string[] = process.argv.slice(2)): Promise<void> {\n try {\n process.exit(await main(argv));\n } catch (err) {\n // Render only the message — full stack traces can leak host-machine\n // absolute paths and monorepo-internal layout into a published\n // binary's stderr output.\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(1);\n }\n}\n\n/** True when this module was started directly (`node …/init.js`). */\nfunction isEntrypoint(): boolean {\n const entry = process.argv[1];\n if (entry === undefined) return false;\n try {\n return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));\n } catch {\n return false;\n }\n}\n\nif (isEntrypoint()) {\n void run();\n}\n","/**\n * Content-license metadata and helpers for the `create-takuhon` scaffolding\n * flow.\n *\n * The four entries in `LICENSE_OPTIONS` are the curated choices the\n * interactive picker offers. \"Custom\" is handled separately by the prompt\n * flow as a free-form SPDX text input. `buildContentLicense` shapes the\n * chosen identifier into the `meta.contentLicense` fragment that lands in\n * the generated `takuhon.json`. The Takuhon profile JSON Schema accepts any\n * SPDX expression in `meta.contentLicense.spdxId`; unknown identifiers fall\n * through with `spdxId` only, and downstream UI rendering is best-effort.\n */\n\n/** A single row in the interactive license selector. */\nexport interface LicenseOption {\n /** Short label shown as the selectable label in the prompt. */\n readonly label: string;\n /** Longer hint shown alongside the label in the prompt UI. */\n readonly hint: string;\n /** SPDX identifier written to `takuhon.json` `meta.contentLicense.spdxId`. */\n readonly spdxId: string;\n /** Canonical license URL. Omitted for `Proprietary`. */\n readonly url?: string;\n}\n\n/**\n * The four curated license choices shown by the interactive picker. \"Custom\"\n * is not in this list because it triggers a free-form text prompt rather than\n * mapping to a fixed SPDX identifier here.\n */\nexport const LICENSE_OPTIONS: readonly LicenseOption[] = [\n {\n label: 'CC BY 4.0',\n hint: 'Allow reuse with attribution',\n spdxId: 'CC-BY-4.0',\n url: 'https://creativecommons.org/licenses/by/4.0/',\n },\n {\n label: 'CC BY-NC 4.0',\n hint: 'Non-commercial reuse with attribution',\n spdxId: 'CC-BY-NC-4.0',\n url: 'https://creativecommons.org/licenses/by-nc/4.0/',\n },\n {\n label: 'CC0',\n hint: 'Public domain',\n spdxId: 'CC0-1.0',\n url: 'https://creativecommons.org/publicdomain/zero/1.0/',\n },\n {\n label: 'Proprietary',\n hint: 'All rights reserved',\n spdxId: 'Proprietary',\n },\n];\n\n/**\n * Canonical URL lookup for SPDX identifiers we recognise but don't list in\n * the interactive picker. Used by `buildContentLicense` so that a user who\n * passes `--license CC-BY-SA-4.0` (or selects it via `Custom`) still gets a\n * usable `url` field in the generated `takuhon.json`.\n *\n * URLs for non-CC SPDX identifiers point at the canonical SPDX license page\n * (`spdx.org/licenses/<id>.html`). Creative Commons identifiers use the\n * `creativecommons.org` deed URL because that is the page humans expect to\n * land on (the SPDX page is metadata-only).\n */\nconst KNOWN_URL_BY_SPDX: Readonly<Record<string, string>> = {\n 'CC0-1.0': 'https://creativecommons.org/publicdomain/zero/1.0/',\n 'CC-BY-4.0': 'https://creativecommons.org/licenses/by/4.0/',\n 'CC-BY-SA-4.0': 'https://creativecommons.org/licenses/by-sa/4.0/',\n 'CC-BY-ND-4.0': 'https://creativecommons.org/licenses/by-nd/4.0/',\n 'CC-BY-NC-4.0': 'https://creativecommons.org/licenses/by-nc/4.0/',\n 'CC-BY-NC-SA-4.0': 'https://creativecommons.org/licenses/by-nc-sa/4.0/',\n 'CC-BY-NC-ND-4.0': 'https://creativecommons.org/licenses/by-nc-nd/4.0/',\n MIT: 'https://spdx.org/licenses/MIT.html',\n};\n\n/** Shape written to `takuhon.json` under `meta.contentLicense`. */\nexport interface ContentLicenseFragment {\n readonly spdxId: string;\n readonly url?: string;\n readonly rights?: string;\n}\n\n/**\n * Translate a chosen SPDX identifier into the `meta.contentLicense` fragment\n * for `takuhon.json`.\n *\n * - `Proprietary` is given a `rights` sentinel and no `url`.\n * - Known SPDX identifiers get a canonical `url`.\n * - Anything else is written as `{ spdxId }` only — the schema accepts\n * arbitrary SPDX expressions, and UI rendering is best-effort.\n */\nexport function buildContentLicense(spdxId: string): ContentLicenseFragment {\n if (spdxId === 'Proprietary') {\n return {\n spdxId: 'Proprietary',\n rights: 'All rights reserved. Contact owner for usage permission.',\n };\n }\n const url = KNOWN_URL_BY_SPDX[spdxId];\n if (url !== undefined) {\n return { spdxId, url };\n }\n return { spdxId };\n}\n\n/**\n * Light syntactic validation for a user-entered SPDX expression (the\n * `Custom` prompt branch and the `--license` flag).\n *\n * We accept any non-empty string composed of the characters that appear in\n * canonical SPDX identifiers and boolean expressions (alphanumerics, `.`,\n * `-`, `+`, parentheses, and literal spaces). Tabs and newlines are rejected\n * because real SPDX expressions never contain them. Full SPDX expression\n * parsing (`MIT OR (Apache-2.0 AND CC-BY-4.0)`) is intentionally out of\n * scope — we trust the user and let the JSON Schema's downstream validation\n * flag obvious problems.\n */\nexport function isValidSpdxInput(input: string): boolean {\n const trimmed = input.trim();\n if (trimmed === '') return false;\n return /^[A-Za-z0-9.\\-+() ]+$/.test(trimmed);\n}\n","/**\n * Interactive prompt flow for `create-takuhon`, built on `@clack/prompts`.\n *\n * A single-choice picker offers the curated `LICENSE_OPTIONS` plus a\n * `Custom` row that opens a free-form SPDX text input. Cancellation\n * (Ctrl+C or pressing Esc on a clack prompt) returns a sentinel result\n * instead of throwing so the caller can exit cleanly with a specific status\n * code.\n */\n\nimport { cancel, isCancel, select, text } from '@clack/prompts';\n\nimport { LICENSE_OPTIONS, isValidSpdxInput } from './licenses.js';\n\n/** Returned by {@link promptLicense} on successful completion. */\nexport interface PromptLicenseResult {\n /** SPDX identifier to write into `meta.contentLicense.spdxId`. */\n readonly spdxId: string;\n}\n\n/** Returned when the user cancels (Ctrl+C / Esc) instead of completing. */\nexport interface PromptCancelled {\n readonly cancelled: true;\n}\n\nconst CUSTOM_SENTINEL = '__custom__';\n\n/**\n * Run the license picker. Resolves to either the selected SPDX identifier or\n * a cancellation sentinel; never throws on user cancel.\n */\nexport async function promptLicense(): Promise<PromptLicenseResult | PromptCancelled> {\n const choice = await select<string>({\n message: 'Choose a license for your profile content:',\n options: [\n ...LICENSE_OPTIONS.map((opt) => ({\n value: opt.spdxId,\n label: opt.label,\n hint: opt.hint,\n })),\n { value: CUSTOM_SENTINEL, label: 'Custom', hint: 'enter SPDX identifier' },\n ],\n });\n\n if (isCancel(choice)) {\n cancel('Aborted.');\n return { cancelled: true };\n }\n\n if (choice !== CUSTOM_SENTINEL) {\n return { spdxId: choice };\n }\n\n const customRaw = await text({\n message: 'Enter the SPDX identifier:',\n placeholder: 'e.g. MIT, Apache-2.0, CC-BY-SA-4.0',\n validate(value): string | undefined {\n if (!isValidSpdxInput(value)) {\n return 'Must be a non-empty SPDX-like expression (alphanumerics, ., -, +, parentheses, spaces).';\n }\n return undefined;\n },\n });\n\n if (isCancel(customRaw)) {\n cancel('Aborted.');\n return { cancelled: true };\n }\n\n return { spdxId: customRaw.trim() };\n}\n"],"mappings":"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","names":["cancel","cancel"]}
1
+ {"version":3,"sources":["../src/init.ts","../src/licenses.ts","../src/prompts.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * Entry point for `npx create-takuhon` (and `pnpm create takuhon`).\n *\n * Usage:\n * create-takuhon <target-dir> [--license <spdxId>]\n *\n * The positional target directory is required; it must not already exist (we\n * refuse to overwrite). `--license` skips the interactive picker for CI /\n * automation use.\n */\n\nimport { realpathSync } from 'node:fs';\nimport { rm } from 'node:fs/promises';\nimport { basename, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { parseArgs } from 'node:util';\n\nimport { cancel, intro, outro } from '@clack/prompts';\n\nimport { buildContentLicense, isValidSpdxInput } from './licenses.js';\nimport { promptLicense } from './prompts.js';\nimport {\n copyAdminBundle,\n TargetDirectoryExistsError,\n writeProject,\n type ScaffoldPlatform,\n} from './scaffold/index.js';\nimport { isValidVercelProjectName } from './scaffold/vercel.js';\nimport { isValidWorkerName } from './scaffold/wrangler-toml.js';\n\ninterface CliArgs {\n readonly targetArg: string | undefined;\n readonly extraPositionals: readonly string[];\n readonly license: string | undefined;\n readonly platform: string | undefined;\n readonly help: boolean;\n}\n\nfunction parseCliArgs(argv: readonly string[]): CliArgs {\n const { values, positionals } = parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n options: {\n license: { type: 'string' },\n platform: { type: 'string' },\n help: { type: 'boolean', short: 'h' },\n },\n });\n return {\n targetArg: positionals[0],\n extraPositionals: positionals.slice(1),\n license: values.license,\n platform: values.platform,\n help: values.help === true,\n };\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n `Usage: create-takuhon <target-dir> [--platform <cloudflare|vercel>] [--license <spdxId>]\\n` +\n `\\n` +\n `Scaffolds a Takuhon profile deployment in <target-dir>.\\n` +\n `\\n` +\n `Options:\\n` +\n ` --platform <name> Target platform: \"cloudflare\" (default) or \"vercel\".\\n` +\n ` Cloudflare is the full deployment (admin UI, KV);\\n` +\n ` Vercel is a read-only profile published from Git.\\n` +\n ` --license <spdxId> Skip the interactive license prompt and use the\\n` +\n ` given SPDX identifier (e.g. CC-BY-4.0, MIT,\\n` +\n ` Proprietary). Useful for CI / automation.\\n` +\n ` -h, --help Show this help.\\n`,\n );\n}\n\nasync function main(argv: readonly string[]): Promise<number> {\n let parsed: CliArgs;\n try {\n parsed = parseCliArgs(argv);\n } catch (err) {\n process.stderr.write(`${(err as Error).message}\\n\\n`);\n printHelp();\n return 2;\n }\n\n if (parsed.help) {\n printHelp();\n return 0;\n }\n\n if (parsed.targetArg === undefined) {\n process.stderr.write(`Error: missing target directory argument.\\n\\n`);\n printHelp();\n return 2;\n }\n\n if (parsed.extraPositionals.length > 0) {\n process.stderr.write(\n `Error: unexpected extra arguments: ${parsed.extraPositionals.join(' ')}\\n\\n`,\n );\n printHelp();\n return 2;\n }\n\n let platform: ScaffoldPlatform;\n if (parsed.platform === undefined || parsed.platform === 'cloudflare') {\n platform = 'cloudflare';\n } else if (parsed.platform === 'vercel') {\n platform = 'vercel';\n } else {\n process.stderr.write(\n `Error: invalid --platform value \"${parsed.platform}\". Use \"cloudflare\" or \"vercel\".\\n`,\n );\n return 2;\n }\n\n const targetDir = resolve(process.cwd(), parsed.targetArg);\n const projectName = basename(targetDir);\n\n if (platform === 'cloudflare' && !isValidWorkerName(projectName)) {\n process.stderr.write(\n `Error: target directory basename \"${projectName}\" is not a valid Cloudflare Worker name.\\n` +\n `Names must be lowercase, start and end with a letter or digit, and contain only ` +\n `letters, digits, and hyphens (max 63 chars).\\n`,\n );\n return 2;\n }\n if (platform === 'vercel' && !isValidVercelProjectName(projectName)) {\n process.stderr.write(\n `Error: target directory basename \"${projectName}\" is not a valid Vercel project name.\\n` +\n `Names must be lowercase, start and end with a letter or digit, and contain only ` +\n `letters, digits, dots, underscores, and hyphens (max 100 chars).\\n`,\n );\n return 2;\n }\n\n intro('create-takuhon');\n\n let spdxId: string;\n if (parsed.license !== undefined) {\n const raw = parsed.license.trim();\n if (!isValidSpdxInput(raw)) {\n cancel(`Invalid --license value: \"${parsed.license}\"`);\n return 2;\n }\n spdxId = raw;\n } else {\n const result = await promptLicense();\n if ('cancelled' in result) {\n return 130;\n }\n spdxId = result.spdxId;\n }\n\n const license = buildContentLicense(spdxId);\n\n try {\n await writeProject({ targetDir, projectName, license, platform });\n } catch (err) {\n if (err instanceof TargetDirectoryExistsError) {\n cancel(`Target directory already exists: ${parsed.targetArg}`);\n return 1;\n }\n throw err;\n }\n\n // The Cloudflare scaffold ships the admin SPA so the Worker can serve the form\n // UI at /admin. Copy it into the project; on failure, remove the just-created\n // directory so the user can retry into the same path (writeProject refuses an\n // existing one). The read-only Vercel scaffold has no admin UI, so it skips\n // this step entirely.\n if (platform === 'cloudflare') {\n try {\n await copyAdminBundle({ targetDir });\n } catch (err) {\n await rm(targetDir, { recursive: true, force: true });\n cancel(err instanceof Error ? err.message : 'Failed to copy the admin UI bundle.');\n return 1;\n }\n }\n\n if (platform === 'vercel') {\n outro(\n `Created ${projectName} (license: ${spdxId}) for Vercel.\\n` +\n `\\n` +\n `Next steps:\\n` +\n ` cd ${parsed.targetArg}\\n` +\n ` pnpm install\\n` +\n ` # 1. Edit takuhon.json with your profile data\\n` +\n ` pnpm dev # local Next.js dev server\\n` +\n ` # 2. Deploy: push to a Git repo connected to Vercel, or run \\`npx vercel\\`\\n` +\n `\\n` +\n `This adapter is read-only: edit takuhon.json and redeploy to publish changes.`,\n );\n return 0;\n }\n\n outro(\n `Created ${projectName} (license: ${spdxId}).\\n` +\n `\\n` +\n `Next steps:\\n` +\n ` cd ${parsed.targetArg}\\n` +\n ` pnpm install\\n` +\n ` # 1. Edit takuhon.json with your profile data\\n` +\n ` # 2. Provision Cloudflare KV: npx wrangler kv namespace create TAKUHON_KV\\n` +\n ` # 3. Set admin token: openssl rand -base64 32 | npx wrangler secret put TAKUHON_ADMIN_TOKEN\\n` +\n ` pnpm dev\\n` +\n `\\n` +\n `The admin form UI is served at /admin (bundled in admin-dist/); sign in\\n` +\n `with the admin token from step 3.`,\n );\n\n return 0;\n}\n\n/**\n * Process entry point: run {@link main} and exit with its code. This is the\n * only function that calls `process.exit`, mirroring the `takuhon` entry in\n * `index.ts`. Exported so the `create-takuhon` redirect package can invoke it\n * after importing this module (`import { run } from '@takuhon/cli/init'`).\n */\nexport async function run(argv: readonly string[] = process.argv.slice(2)): Promise<void> {\n try {\n process.exit(await main(argv));\n } catch (err) {\n // Render only the message — full stack traces can leak host-machine\n // absolute paths and monorepo-internal layout into a published\n // binary's stderr output.\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(1);\n }\n}\n\n/** True when this module was started directly (`node …/init.js`). */\nfunction isEntrypoint(): boolean {\n const entry = process.argv[1];\n if (entry === undefined) return false;\n try {\n return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));\n } catch {\n return false;\n }\n}\n\nif (isEntrypoint()) {\n void run();\n}\n","/**\n * Content-license metadata and helpers for the `create-takuhon` scaffolding\n * flow.\n *\n * The four entries in `LICENSE_OPTIONS` are the curated choices the\n * interactive picker offers. \"Custom\" is handled separately by the prompt\n * flow as a free-form SPDX text input. `buildContentLicense` shapes the\n * chosen identifier into the `meta.contentLicense` fragment that lands in\n * the generated `takuhon.json`. The Takuhon profile JSON Schema accepts any\n * SPDX expression in `meta.contentLicense.spdxId`; unknown identifiers fall\n * through with `spdxId` only, and downstream UI rendering is best-effort.\n */\n\n/** A single row in the interactive license selector. */\nexport interface LicenseOption {\n /** Short label shown as the selectable label in the prompt. */\n readonly label: string;\n /** Longer hint shown alongside the label in the prompt UI. */\n readonly hint: string;\n /** SPDX identifier written to `takuhon.json` `meta.contentLicense.spdxId`. */\n readonly spdxId: string;\n /** Canonical license URL. Omitted for `Proprietary`. */\n readonly url?: string;\n}\n\n/**\n * The four curated license choices shown by the interactive picker. \"Custom\"\n * is not in this list because it triggers a free-form text prompt rather than\n * mapping to a fixed SPDX identifier here.\n */\nexport const LICENSE_OPTIONS: readonly LicenseOption[] = [\n {\n label: 'CC BY 4.0',\n hint: 'Allow reuse with attribution',\n spdxId: 'CC-BY-4.0',\n url: 'https://creativecommons.org/licenses/by/4.0/',\n },\n {\n label: 'CC BY-NC 4.0',\n hint: 'Non-commercial reuse with attribution',\n spdxId: 'CC-BY-NC-4.0',\n url: 'https://creativecommons.org/licenses/by-nc/4.0/',\n },\n {\n label: 'CC0',\n hint: 'Public domain',\n spdxId: 'CC0-1.0',\n url: 'https://creativecommons.org/publicdomain/zero/1.0/',\n },\n {\n label: 'Proprietary',\n hint: 'All rights reserved',\n spdxId: 'Proprietary',\n },\n];\n\n/**\n * Canonical URL lookup for SPDX identifiers we recognise but don't list in\n * the interactive picker. Used by `buildContentLicense` so that a user who\n * passes `--license CC-BY-SA-4.0` (or selects it via `Custom`) still gets a\n * usable `url` field in the generated `takuhon.json`.\n *\n * URLs for non-CC SPDX identifiers point at the canonical SPDX license page\n * (`spdx.org/licenses/<id>.html`). Creative Commons identifiers use the\n * `creativecommons.org` deed URL because that is the page humans expect to\n * land on (the SPDX page is metadata-only).\n */\nconst KNOWN_URL_BY_SPDX: Readonly<Record<string, string>> = {\n 'CC0-1.0': 'https://creativecommons.org/publicdomain/zero/1.0/',\n 'CC-BY-4.0': 'https://creativecommons.org/licenses/by/4.0/',\n 'CC-BY-SA-4.0': 'https://creativecommons.org/licenses/by-sa/4.0/',\n 'CC-BY-ND-4.0': 'https://creativecommons.org/licenses/by-nd/4.0/',\n 'CC-BY-NC-4.0': 'https://creativecommons.org/licenses/by-nc/4.0/',\n 'CC-BY-NC-SA-4.0': 'https://creativecommons.org/licenses/by-nc-sa/4.0/',\n 'CC-BY-NC-ND-4.0': 'https://creativecommons.org/licenses/by-nc-nd/4.0/',\n MIT: 'https://spdx.org/licenses/MIT.html',\n};\n\n/** Shape written to `takuhon.json` under `meta.contentLicense`. */\nexport interface ContentLicenseFragment {\n readonly spdxId: string;\n readonly url?: string;\n readonly rights?: string;\n}\n\n/**\n * Translate a chosen SPDX identifier into the `meta.contentLicense` fragment\n * for `takuhon.json`.\n *\n * - `Proprietary` is given a `rights` sentinel and no `url`.\n * - Known SPDX identifiers get a canonical `url`.\n * - Anything else is written as `{ spdxId }` only — the schema accepts\n * arbitrary SPDX expressions, and UI rendering is best-effort.\n */\nexport function buildContentLicense(spdxId: string): ContentLicenseFragment {\n if (spdxId === 'Proprietary') {\n return {\n spdxId: 'Proprietary',\n rights: 'All rights reserved. Contact owner for usage permission.',\n };\n }\n const url = KNOWN_URL_BY_SPDX[spdxId];\n if (url !== undefined) {\n return { spdxId, url };\n }\n return { spdxId };\n}\n\n/**\n * Light syntactic validation for a user-entered SPDX expression (the\n * `Custom` prompt branch and the `--license` flag).\n *\n * We accept any non-empty string composed of the characters that appear in\n * canonical SPDX identifiers and boolean expressions (alphanumerics, `.`,\n * `-`, `+`, parentheses, and literal spaces). Tabs and newlines are rejected\n * because real SPDX expressions never contain them. Full SPDX expression\n * parsing (`MIT OR (Apache-2.0 AND CC-BY-4.0)`) is intentionally out of\n * scope — we trust the user and let the JSON Schema's downstream validation\n * flag obvious problems.\n */\nexport function isValidSpdxInput(input: string): boolean {\n const trimmed = input.trim();\n if (trimmed === '') return false;\n return /^[A-Za-z0-9.\\-+() ]+$/.test(trimmed);\n}\n","/**\n * Interactive prompt flow for `create-takuhon`, built on `@clack/prompts`.\n *\n * A single-choice picker offers the curated `LICENSE_OPTIONS` plus a\n * `Custom` row that opens a free-form SPDX text input. Cancellation\n * (Ctrl+C or pressing Esc on a clack prompt) returns a sentinel result\n * instead of throwing so the caller can exit cleanly with a specific status\n * code.\n */\n\nimport { cancel, isCancel, select, text } from '@clack/prompts';\n\nimport { LICENSE_OPTIONS, isValidSpdxInput } from './licenses.js';\n\n/** Returned by {@link promptLicense} on successful completion. */\nexport interface PromptLicenseResult {\n /** SPDX identifier to write into `meta.contentLicense.spdxId`. */\n readonly spdxId: string;\n}\n\n/** Returned when the user cancels (Ctrl+C / Esc) instead of completing. */\nexport interface PromptCancelled {\n readonly cancelled: true;\n}\n\nconst CUSTOM_SENTINEL = '__custom__';\n\n/**\n * Run the license picker. Resolves to either the selected SPDX identifier or\n * a cancellation sentinel; never throws on user cancel.\n */\nexport async function promptLicense(): Promise<PromptLicenseResult | PromptCancelled> {\n const choice = await select<string>({\n message: 'Choose a license for your profile content:',\n options: [\n ...LICENSE_OPTIONS.map((opt) => ({\n value: opt.spdxId,\n label: opt.label,\n hint: opt.hint,\n })),\n { value: CUSTOM_SENTINEL, label: 'Custom', hint: 'enter SPDX identifier' },\n ],\n });\n\n if (isCancel(choice)) {\n cancel('Aborted.');\n return { cancelled: true };\n }\n\n if (choice !== CUSTOM_SENTINEL) {\n return { spdxId: choice };\n }\n\n const customRaw = await text({\n message: 'Enter the SPDX identifier:',\n placeholder: 'e.g. MIT, Apache-2.0, CC-BY-SA-4.0',\n validate(value): string | undefined {\n if (!isValidSpdxInput(value)) {\n return 'Must be a non-empty SPDX-like expression (alphanumerics, ., -, +, parentheses, spaces).';\n }\n return undefined;\n },\n });\n\n if (isCancel(customRaw)) {\n cancel('Aborted.');\n return { cancelled: true };\n }\n\n return { spdxId: customRaw.trim() };\n}\n"],"mappings":"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","names":["cancel","cancel"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takuhon/cli",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "create-takuhon scaffolding plus dev/validate/sync/export/migrate/restore commands",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Takuhon contributors",
@@ -56,10 +56,10 @@
56
56
  "@hono/node-server": "^2.0.0",
57
57
  "@modelcontextprotocol/sdk": "^1.29.0",
58
58
  "hono": "^4.12.21",
59
- "@takuhon/activity": "0.19.0",
60
- "@takuhon/api": "0.19.0",
61
- "@takuhon/core": "0.19.0",
62
- "@takuhon/mcp": "0.19.0"
59
+ "@takuhon/activity": "0.21.0",
60
+ "@takuhon/core": "0.21.0",
61
+ "@takuhon/api": "0.21.0",
62
+ "@takuhon/mcp": "0.21.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@takuhon/admin": "0.1.0"
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/scaffold/wrangler-toml.ts","../src/scaffold/index.ts","../src/scaffold/env-example.ts","../src/scaffold/gitignore.ts","../src/scaffold/package-json.ts","../src/scaffold/readme.ts","../src/scaffold/takuhon-json.ts","../src/scaffold/tsconfig-json.ts","../src/scaffold/worker-index-ts.ts"],"sourcesContent":["/**\n * Generator for `wrangler.toml` in a freshly scaffolded project.\n *\n * The layout mirrors the reference Cloudflare adapter shipped in this\n * monorepo: a single KV namespace `TAKUHON_KV`, placeholder ids the user\n * fills in after running `wrangler kv namespace create`, and the\n * `TAKUHON_ADMIN_ORIGIN` var defaulting to \"\" (disabled). The admin bearer\n * token is provisioned as a Wrangler secret and is therefore not in this file.\n */\n\n/**\n * Validate a Cloudflare Worker name.\n *\n * Cloudflare itself accepts mixed-case DNS labels (1–63 chars, must not\n * start or end with a hyphen). This validator additionally enforces a\n * lowercase convention chosen for the scaffolder so that the resulting\n * `workers.dev` subdomain is predictable and matches the npm `name` field.\n */\nexport function isValidWorkerName(name: string): boolean {\n return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(name);\n}\n\n/**\n * In-project directory the admin SPA bundle is copied into and the scaffolded\n * `wrangler.toml` binds as `ASSETS`. Shared so the copy destination and the\n * `[assets] directory` value cannot drift apart. Defined in this leaf module\n * (which `scaffold/index.ts` already imports) to avoid an import cycle.\n */\nexport const ADMIN_DIST_DIRNAME = 'admin-dist';\n\n/**\n * Render `wrangler.toml` for the given project name.\n *\n * Throws if `projectName` is not a valid Cloudflare Worker name. Callers are\n * responsible for sanitising/validating earlier in the prompt flow.\n */\nexport function renderWranglerToml(projectName: string): string {\n if (!isValidWorkerName(projectName)) {\n throw new Error(\n `Invalid Cloudflare Worker name: \"${projectName}\". Names must be lowercase, ` +\n `start with a letter or digit, and contain only letters, digits, and hyphens (max 63 chars).`,\n );\n }\n return `name = \"${projectName}\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2026-05-01\"\ncompatibility_flags = [\"nodejs_compat\"]\n\n# Admin SPA bundle. The React admin UI is committed in ./${ADMIN_DIST_DIRNAME}\n# (copied here when this project was created with create-takuhon) and served at\n# /admin.\n#\n# run_worker_first = true makes the Worker mediate every request: the bundle's\n# files sit at the assets root, so without this they would shadow the public \"/\"\n# route. The Worker serves the bundle only for /admin/* (attaching the strict\n# admin CSP) and handles everything else itself.\n#\n# SECURITY-CRITICAL: do not set run_worker_first to false or remove it. The\n# Worker is the only thing that attaches the strict admin CSP (plus HSTS /\n# no-store) to the admin assets; serving them directly would expose the admin UI\n# with no policy. To deploy without the form UI, remove this whole [assets]\n# block — the Worker then falls back to a minimal inline editor.\n[assets]\ndirectory = \"${ADMIN_DIST_DIRNAME}\"\nbinding = \"ASSETS\"\nrun_worker_first = true\nnot_found_handling = \"single-page-application\"\n\n# Replace the placeholder ids after running:\n# wrangler kv namespace create TAKUHON_KV\n# wrangler kv namespace create TAKUHON_KV --preview\n[[kv_namespaces]]\nbinding = \"TAKUHON_KV\"\nid = \"REPLACE_WITH_PRODUCTION_NAMESPACE_ID\"\npreview_id = \"REPLACE_WITH_PREVIEW_NAMESPACE_ID\"\n\n# Admin Origin allowlist (comma-separated). Empty value disables the check.\n# Example for production: TAKUHON_ADMIN_ORIGIN = \"https://admin.example.com\"\n#\n# The admin bearer token MUST be provisioned as a Wrangler secret, never in\n# this file:\n# wrangler secret put TAKUHON_ADMIN_TOKEN\n# Use 32+ bytes of entropy, e.g. \\`openssl rand -base64 32\\`.\n[vars]\nTAKUHON_ADMIN_ORIGIN = \"\"\n\n# Image uploads (optional). Bind an R2 bucket to enable \\`POST /api/admin/assets\\`\n# and \\`GET /assets/*\\` delivery; without it the upload endpoint stays\n# unregistered (404) and avatars remain URL-only. Create the bucket, then\n# uncomment:\n# wrangler r2 bucket create ${projectName}-assets\n# [[r2_buckets]]\n# binding = \"TAKUHON_R2\"\n# bucket_name = \"${projectName}-assets\"\n\n# Developer-activity dashboard (optional). Enable settings.activity in your\n# takuhon.json, then uncomment the cron below to refresh the snapshot daily:\n# the Worker's scheduled handler fetches GitHub / WakaTime and stores the\n# result in the TAKUHON_KV namespace (a failed run keeps the last-known\n# snapshot). Secrets MUST be Wrangler secrets, never this file:\n# wrangler secret put TAKUHON_GITHUB_TOKEN # optional (contribution calendar)\n# wrangler secret put TAKUHON_WAKATIME_KEY # required for WakaTime\n# [triggers]\n# crons = [\"17 3 * * *\"]\n`;\n}\n","/**\n * Top-level scaffolding orchestrator for `create-takuhon`.\n *\n * `writeProject` creates the target directory (must not already exist), then\n * writes the eight files that make up the scaffold: `takuhon.json`,\n * `wrangler.toml`, `package.json`, `README.md`, `.gitignore`, `.env.example`,\n * `tsconfig.json`, and `src/index.ts` (the Cloudflare Worker entry composed via\n * `createTakuhonWorker` from `@takuhon/cloudflare`).\n *\n * `copyAdminBundle` then copies the bundled admin SPA into the project's\n * `admin-dist/` so the Worker can serve the form UI at `/admin`. `init.ts`\n * calls both in sequence.\n */\n\nimport { cp, mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport type { ContentLicenseFragment } from '../licenses.js';\n\nimport { renderEnvExample } from './env-example.js';\nimport { renderGitignore } from './gitignore.js';\nimport { renderPackageJson } from './package-json.js';\nimport { renderReadme } from './readme.js';\nimport { renderTakuhonJson } from './takuhon-json.js';\nimport { renderTsconfigJson } from './tsconfig-json.js';\nimport { renderWorkerIndexTs } from './worker-index-ts.js';\nimport { ADMIN_DIST_DIRNAME, renderWranglerToml } from './wrangler-toml.js';\n\nexport interface WriteProjectOptions {\n /** Absolute path of the directory to create. Must not exist. */\n readonly targetDir: string;\n /** Used for the npm `name`, Worker name, and README header. */\n readonly projectName: string;\n /** Chosen content license (already mapped via `buildContentLicense`). */\n readonly license: ContentLicenseFragment;\n}\n\nexport interface WriteProjectResult {\n /** Relative paths of files that were written (in the order written). */\n readonly files: readonly string[];\n}\n\n/**\n * Error thrown when the target directory already exists. The caller is\n * expected to render a friendly message that references the user-supplied\n * path; the `code` is stable for tests. The absolute `targetDir` is exposed\n * as a field rather than embedded in the message so that bubbling the error\n * up through generic logging does not leak filesystem layout.\n */\nexport class TargetDirectoryExistsError extends Error {\n override readonly name = 'TargetDirectoryExistsError';\n readonly code = 'TARGET_EXISTS' as const;\n constructor(readonly targetDir: string) {\n super('Target directory already exists.');\n }\n}\n\n/**\n * Create the project directory and write the scaffolded files. Order is\n * deterministic so callers (and tests) can rely on it.\n */\nexport async function writeProject(opts: WriteProjectOptions): Promise<WriteProjectResult> {\n const { targetDir, projectName, license } = opts;\n\n // mkdir with recursive: false fails if the directory exists; we surface a\n // typed error so the CLI entry can render a friendly message.\n try {\n await mkdir(targetDir, { recursive: false });\n } catch (err) {\n if (isNodeErrnoException(err) && err.code === 'EEXIST') {\n throw new TargetDirectoryExistsError(targetDir);\n }\n throw err;\n }\n\n const files: { readonly path: string; readonly content: string }[] = [\n { path: 'takuhon.json', content: renderTakuhonJson(license) },\n { path: 'wrangler.toml', content: renderWranglerToml(projectName) },\n { path: 'package.json', content: renderPackageJson({ projectName }) },\n { path: 'README.md', content: renderReadme({ projectName, license }) },\n { path: '.gitignore', content: renderGitignore() },\n { path: '.env.example', content: renderEnvExample() },\n { path: 'tsconfig.json', content: renderTsconfigJson() },\n { path: 'src/index.ts', content: renderWorkerIndexTs() },\n ];\n\n for (const { path, content } of files) {\n const fullPath = join(targetDir, path);\n const parent = dirname(fullPath);\n if (parent !== targetDir) {\n await mkdir(parent, { recursive: true });\n }\n await writeFile(fullPath, content, 'utf8');\n }\n\n return { files: files.map((f) => f.path) };\n}\n\nfunction isNodeErrnoException(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && typeof (err as NodeJS.ErrnoException).code === 'string';\n}\n\n/**\n * Resolve the admin SPA bundle that ships inside this package. The bundle is\n * copied here from `apps/admin/dist` at build time (see\n * `scripts/copy-admin-bundle.mjs`) and listed in the package's `files`, so it\n * is present in the published `@takuhon/cli` tarball. Resolved relative to the\n * compiled module (`dist/init.js` or `dist/index.js`), whose sibling is\n * `admin-bundle/`.\n */\nexport function resolveAdminBundleDir(): string {\n return fileURLToPath(new URL('../admin-bundle', import.meta.url));\n}\n\nexport interface CopyAdminBundleOptions {\n /** Project directory previously created by {@link writeProject}. */\n readonly targetDir: string;\n /**\n * Source bundle directory. Defaults to the bundle shipped in this package\n * ({@link resolveAdminBundleDir}); overridable for tests.\n */\n readonly bundleDir?: string;\n}\n\nexport interface CopyAdminBundleResult {\n /** Absolute path of the directory the bundle was copied into. */\n readonly dest: string;\n}\n\n/**\n * Copy the admin SPA bundle into the scaffolded project's\n * `{@link ADMIN_DIST_DIRNAME}` directory. The generated `wrangler.toml` binds\n * this directory as `ASSETS`, and the Cloudflare Worker serves it at `/admin`\n * under a strict CSP (falling back to an inline editor when the binding is\n * absent). Kept separate from {@link writeProject} so the static-asset copy and\n * the generated-file rendering are independently testable.\n */\nexport async function copyAdminBundle(\n opts: CopyAdminBundleOptions,\n): Promise<CopyAdminBundleResult> {\n const bundleDir = opts.bundleDir ?? resolveAdminBundleDir();\n const dest = join(opts.targetDir, ADMIN_DIST_DIRNAME);\n try {\n await cp(bundleDir, dest, { recursive: true });\n } catch (err) {\n // The bundle ships inside @takuhon/cli, so a failure here means a broken\n // or incomplete install. Surface a sanitized message — the raw error would\n // embed the absolute node_modules path of this package.\n const code = isNodeErrnoException(err) ? ` (${err.code})` : '';\n throw new Error(\n `Failed to copy the admin UI bundle from @takuhon/cli${code}. ` +\n `Reinstall or upgrade @takuhon/cli and try again.`,\n { cause: err },\n );\n }\n return { dest };\n}\n","/**\n * Generator for the `.env.example` file in a freshly scaffolded project.\n *\n * The Wrangler-managed secrets (admin bearer token) are *not* read from `.env`\n * at deploy time — `wrangler secret put` is the source of truth. The\n * `.env.example` is therefore documentation: it lists the variables a developer\n * may want locally (e.g. for scripting or for the wrangler CLI itself) and\n * links each to its production provisioning path.\n */\n\nexport function renderEnvExample(): string {\n return `# ----------------------------------------------------------------\n# Local development variables for this Takuhon deployment.\n#\n# Production secrets (admin token) are provisioned via:\n# wrangler secret put TAKUHON_ADMIN_TOKEN\n# This file is a hint for local tooling, not a runtime source.\n# ----------------------------------------------------------------\n\n# Admin bearer token for /api/admin/* endpoints.\n# Generate with: openssl rand -base64 32\n# Then provision with:\n# echo \"$TAKUHON_ADMIN_TOKEN\" | wrangler secret put TAKUHON_ADMIN_TOKEN\nTAKUHON_ADMIN_TOKEN=\n\n# Comma-separated Origin allowlist for browser-originating admin requests.\n# Set in wrangler.toml [vars] for production. Local example:\n# TAKUHON_ADMIN_ORIGIN=https://admin.example.com,https://localhost:8787\nTAKUHON_ADMIN_ORIGIN=\n\n# Developer-activity sync secrets (only needed when settings.activity is\n# enabled in takuhon.json). Used by \\`takuhon activity sync\\` locally; for a\n# deployed Worker provision them as Wrangler secrets instead:\n# wrangler secret put TAKUHON_GITHUB_TOKEN\n# wrangler secret put TAKUHON_WAKATIME_KEY\n# Never commit the real values.\n#\n# Optional GitHub token: languages work without it; the contribution calendar\n# requires it (minimal read-only scope is enough).\nTAKUHON_GITHUB_TOKEN=\n# WakaTime API key (https://wakatime.com/settings/api-key); required to read\n# coding time.\nTAKUHON_WAKATIME_KEY=\n\n# Cloudflare account id (look up via \\`wrangler whoami\\`). Only needed if you\n# script Cloudflare API calls outside Wrangler itself.\nCLOUDFLARE_ACCOUNT_ID=\n`;\n}\n","/**\n * Generator for the scaffolded project's `.gitignore`.\n *\n * Covers Node, Wrangler local cache, and environment files. Kept conservative\n * — no opinions about lockfile preferences (the user picks their package\n * manager) or editor folders (per-editor patterns belong in a global ignore).\n */\n\nexport function renderGitignore(): string {\n return `# Dependencies\nnode_modules/\n\n# Wrangler local cache (do not commit; contains bundled output and tmp files)\n.wrangler/\n\n# Environment variables (commit \\`.env.example\\` instead)\n.env\n.env.local\n.env.*.local\n\n# Logs\n*.log\nnpm-debug.log*\npnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# macOS\n.DS_Store\n`;\n}\n","/**\n * Generator for the scaffolded project's `package.json`.\n *\n * Pinned dependency versions are caret ranges matched to the current\n * minor of the published `@takuhon/*` packages. Under 0.x semver, a caret\n * does not span minor versions, so the range must move forward with each\n * `@takuhon/core` minor release to keep scaffolded projects on the\n * matching schema generation. A guard test (`scaffold.test.ts`) asserts these\n * pins track the published minor, so a missed bump fails CI rather than\n * silently shipping a stale range. `hono` is included as a direct dependency for\n * projects that extend the Worker with their own Hono routes; the generated\n * `src/index.ts` reaches Takuhon's handlers through `@takuhon/cloudflare`\n * rather than importing `hono` directly. `wrangler` is a devDependency since\n * it's the deploy / dev-server tool.\n */\n\nexport interface PackageJsonOptions {\n /** Used for the npm `name` field. */\n readonly projectName: string;\n}\n\nexport function buildPackageJson(opts: PackageJsonOptions): Record<string, unknown> {\n return {\n name: opts.projectName,\n version: '0.0.0',\n private: true,\n type: 'module',\n description: 'Takuhon profile deployment.',\n scripts: {\n dev: 'wrangler dev',\n deploy: 'wrangler deploy',\n },\n dependencies: {\n '@takuhon/api': '^0.19.0',\n '@takuhon/cloudflare': '^0.19.0',\n '@takuhon/core': '^0.19.0',\n hono: '^4.0.0',\n },\n devDependencies: {\n wrangler: '^4.0.0',\n },\n engines: {\n node: '>=22.0.0',\n },\n };\n}\n\nexport function renderPackageJson(opts: PackageJsonOptions): string {\n return `${JSON.stringify(buildPackageJson(opts), null, 2)}\\n`;\n}\n","/**\n * Generator for the README.md placed in a freshly scaffolded project.\n *\n * Aims to give a first-time user enough to (1) install dependencies,\n * (2) fill in their profile data, (3) provision Cloudflare KV + admin\n * secret, and (4) `pnpm dev` / `pnpm deploy`. Cross-references the published\n * `@takuhon/cloudflare` adapter README as the next-step reference for the\n * route map.\n */\n\nimport type { ContentLicenseFragment } from '../licenses.js';\n\nexport interface ReadmeOptions {\n readonly projectName: string;\n readonly license: ContentLicenseFragment;\n}\n\n/**\n * Human-readable license attribution line for the README footer. Uses the\n * canonical URL when available; falls back to plain SPDX (or\n * \"All rights reserved\" for Proprietary).\n */\nfunction formatLicenseLine(license: ContentLicenseFragment): string {\n if (license.spdxId === 'Proprietary') {\n return '**Proprietary** — all rights reserved.';\n }\n if (license.url !== undefined) {\n return `[\\`${license.spdxId}\\`](${license.url})`;\n }\n return `\\`${license.spdxId}\\``;\n}\n\nexport function renderReadme(opts: ReadmeOptions): string {\n const { projectName, license } = opts;\n const licenseLine = formatLicenseLine(license);\n return `# ${projectName}\n\nA [Takuhon](https://github.com/takuhon-dev/takuhon) profile deployment, running on Cloudflare Workers.\n\n> **Status**: pre-deploy. Edit \\`takuhon.json\\`, provision Cloudflare KV, then \\`pnpm deploy\\`.\n\n## What is Takuhon?\n\nTakuhon lets you own your profile as a portable JSON document and publish it as a mobile-first profile page plus a public API (JSON-LD for AI agents and search engines included).\n\n## Setup\n\n1. **Install dependencies.**\n\n \\`\\`\\`sh\n pnpm install\n \\`\\`\\`\n\n2. **Edit your profile.** Open \\`takuhon.json\\` and replace the sample fields (\\`profile.displayName\\`, \\`links\\`, \\`careers\\`, \\`projects\\`, \\`skills\\`) with your own.\n\n3. **Create the Cloudflare KV namespaces** and copy the returned ids into \\`wrangler.toml\\`:\n\n \\`\\`\\`sh\n npx wrangler kv namespace create TAKUHON_KV\n npx wrangler kv namespace create TAKUHON_KV --preview\n \\`\\`\\`\n\n4. **Provision the admin token** as a Wrangler secret (used by \\`/api/admin/*\\`):\n\n \\`\\`\\`sh\n openssl rand -base64 32 | npx wrangler secret put TAKUHON_ADMIN_TOKEN\n \\`\\`\\`\n\n Leaving the secret unset disables admin writes entirely (every \\`PUT\\` / \\`DELETE\\` returns 401).\n\n## Develop\n\nThe Worker entry at \\`src/index.ts\\` composes the takuhon adapter via\n\\`createTakuhonWorker\\` and serves your \\`takuhon.json\\` as the fallback when\nKV has no stored profile yet.\n\n\\`\\`\\`sh\npnpm dev # runs \\`wrangler dev\\` locally\n\\`\\`\\`\n\nOpen the local URL it prints: the root serves your **profile page** — the same HTML a visitor and search-engine crawlers see in production, with Schema.org JSON-LD embedded — and the JSON API lives under \\`/api/*\\`. The full route map (public + admin) is documented in the [\\`@takuhon/cloudflare\\` README](https://github.com/takuhon-dev/takuhon/tree/main/adapters/cloudflare#readme).\n\n## Admin\n\nThe admin form UI is served at \\`/admin\\` under a strict Content-Security-Policy. Its compiled bundle is committed in \\`admin-dist/\\` and bound as Workers Assets in \\`wrangler.toml\\`. Sign in with the admin token you set as the \\`TAKUHON_ADMIN_TOKEN\\` Wrangler secret.\n\nThe bundle is a snapshot taken when this project was created. To pick up a newer admin UI, upgrade your \\`takuhon\\` CLI and run \\`takuhon admin update\\` in this project — it replaces \\`admin-dist/\\` with the bundle shipped in your installed \\`@takuhon/cli\\` (keep your \\`@takuhon/*\\` dependencies on a matching version). To deploy without the form UI, remove the \\`[assets]\\` block from \\`wrangler.toml\\`; the Worker then falls back to a minimal inline editor.\n\n### Image uploads (optional)\n\nImage uploads are off until you bind an R2 bucket. Create one and uncomment the \\`[[r2_buckets]]\\` block in \\`wrangler.toml\\`:\n\n\\`\\`\\`sh\nnpx wrangler r2 bucket create ${projectName}-assets\n\\`\\`\\`\n\nThe Worker then accepts uploads at \\`POST /api/admin/assets\\` (magic-byte check, 5 MB / 4096px limits, EXIF/metadata stripping) and serves them at \\`GET /assets/*\\` with \\`X-Content-Type-Options: nosniff\\`. Without the binding, avatars stay URL-only.\n\n### Developer-activity dashboard (optional)\n\nShow your GitHub language mix / contribution calendar and WakaTime coding time on the profile. The data is synced into a snapshot ahead of time — rendering never calls GitHub or WakaTime, and the API keys never leave the sync step.\n\n1. Opt in by adding \\`settings.activity\\` to \\`takuhon.json\\`:\n\n \\`\\`\\`jsonc\n \"settings\": {\n \"activity\": {\n \"enabled\": true,\n \"github\": { \"username\": \"your-github-login\" },\n \"wakatime\": { \"username\": \"your-wakatime-username\" }\n }\n }\n \\`\\`\\`\n\n2. Provision the sync secrets (see \\`.env.example\\`): \\`TAKUHON_WAKATIME_KEY\\` is required for coding time; \\`TAKUHON_GITHUB_TOKEN\\` is optional — languages work without it, the contribution calendar needs it.\n\n \\`\\`\\`sh\n npx wrangler secret put TAKUHON_GITHUB_TOKEN\n npx wrangler secret put TAKUHON_WAKATIME_KEY\n \\`\\`\\`\n\n3. Uncomment the \\`[triggers]\\` cron block in \\`wrangler.toml\\` so the Worker refreshes the snapshot daily, or run \\`takuhon activity sync\\` locally to write an \\`activity.json\\` next to your profile for \\`takuhon build\\` / \\`dev\\`.\n\nThe snapshot is served at \\`GET /api/activity\\` while enabled; a failed sync keeps the last-known data.\n\n### GitHub profile badge\n\nThe activity card is also available as a standalone SVG you can embed as an image — e.g. in your GitHub profile \\`README.md\\`:\n\n\\`\\`\\`md\n![My developer activity](https://your-worker.example/activity.svg)\n\\`\\`\\`\n\nTwo ways to host it (both require \\`settings.activity.enabled\\`):\n\n- **Dynamic (Worker):** \\`GET /activity.svg\\` renders the latest snapshot on every request. Add \\`?theme=dark\\` for the dark card.\n- **Static (\\`takuhon build\\`):** writes \\`dist/activity.svg\\` and \\`dist/activity-dark.svg\\` beside the pages, so a static deploy can serve the badge too.\n\nThe card carries an opaque background, so it stays readable on either GitHub theme. To follow the viewer's OS colour scheme automatically, use \\`<picture>\\`:\n\n\\`\\`\\`html\n<picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://your-worker.example/activity.svg?theme=dark\">\n <img alt=\"My developer activity\" src=\"https://your-worker.example/activity.svg\">\n</picture>\n\\`\\`\\`\n\nGitHub proxies the image through Camo and caches it for a few hours, so a fresh sync can take a little while to appear in your README.\n\n## Résumé / CV (optional)\n\nGenerate a print-ready CV from the same \\`takuhon.json\\` — no re-entry:\n\n\\`\\`\\`sh\nnpx takuhon build --cv\n\\`\\`\\`\n\nThis writes \\`dist/cv.html\\` (and \\`dist/<locale>/cv.html\\` for each locale) alongside the profile pages. Open it in a browser and use **Print → Save as PDF** to produce a résumé PDF. \\`npx takuhon dev\\` also serves it at \\`/cv\\` for previewing.\n\n## AI agents (MCP)\n\nYour profile is readable over the [Model Context Protocol](https://modelcontextprotocol.io) — read-only, the same data the public API exposes (with the privacy filter applied), no write access.\n\n- **Deployed:** the Worker serves a stateless MCP endpoint at \\`POST /mcp\\` (no extra binding or setup). Point an MCP client at \\`https://your-worker.example/mcp\\`. It is also advertised as \\`mcp\\` in \\`/.well-known/takuhon.json\\`.\n- **Local:** run an MCP server over stdio against your \\`takuhon.json\\`, e.g. for Claude Desktop:\n\n \\`\\`\\`jsonc\n // claude_desktop_config.json\n {\n \"mcpServers\": {\n \"my-profile\": { \"command\": \"npx\", \"args\": [\"takuhon\", \"mcp\", \"/path/to/takuhon.json\"] }\n }\n }\n \\`\\`\\`\n\nBoth expose the tools \\`get_profile\\`, \\`get_section\\`, \\`get_jsonld\\`, \\`list_locales\\` and the resources \\`takuhon://profile\\`, \\`takuhon://schema\\`.\n\n## Deploy\n\n\\`\\`\\`sh\npnpm deploy # runs \\`wrangler deploy\\`\n\\`\\`\\`\n\nOnce deployed, your Worker's root URL serves the **mobile profile page** with Schema.org JSON-LD embedded for search engines and AI agents; \\`/<locale>/\\` serves each translated page, and the JSON API stays under \\`/api/*\\`. Edits made in \\`/admin\\` appear on the page immediately.\n\n## License\n\nProfile content (\\`takuhon.json\\`) is licensed under ${licenseLine}\n\nThe deployment code is your own; pick a license appropriate for it.\n`;\n}\n","/**\n * Generator for the `takuhon.json` file written into a freshly scaffolded\n * project.\n *\n * The template is a copy of `examples/minimal-profile/takuhon.json` from the\n * monorepo. It is inlined here as a TypeScript constant rather than imported\n * from `examples/` so the published `@takuhon/cli` npm package does not need\n * to ship the examples directory.\n *\n * Only `meta.contentLicense` is rewritten per the user's choice; everything\n * else is the canonical minimal profile (one career, one project, three\n * skills, `en` locale only) that downstream `@takuhon/core` validation\n * already accepts (see `packages/core/src/__tests__/examples-fixtures.test.ts`).\n */\n\nimport type { ContentLicenseFragment } from '../licenses.js';\n\n/**\n * Build the `takuhon.json` payload as a deterministic, schema-valid object.\n * `meta.contentLicense` is filled from the supplied fragment so each\n * generated project carries the user's chosen SPDX identifier (and `url` /\n * `rights` where applicable).\n */\nexport function buildTakuhonJson(license: ContentLicenseFragment): unknown {\n return {\n schemaVersion: '0.6.0',\n profile: {\n displayName: {\n en: 'Sam Lee',\n },\n },\n links: [\n {\n id: 'github',\n type: 'github',\n url: 'https://example.com/github/sam-lee',\n featured: true,\n },\n ],\n careers: [\n {\n id: 'first-job',\n organization: {\n en: 'Example Co.',\n },\n role: {\n en: 'Junior Software Engineer',\n },\n startDate: '2026-04',\n endDate: null,\n isCurrent: true,\n },\n ],\n projects: [\n {\n id: 'personal-homepage',\n title: {\n en: 'Personal homepage',\n },\n },\n ],\n skills: [\n { id: 'html', label: 'HTML' },\n { id: 'css', label: 'CSS' },\n { id: 'javascript', label: 'JavaScript' },\n ],\n contact: {},\n settings: {\n defaultLocale: 'en',\n availableLocales: ['en'],\n },\n meta: {\n contentLicense: { ...license },\n },\n };\n}\n\n/** Serialise to a UTF-8 string with trailing newline (POSIX-friendly). */\nexport function renderTakuhonJson(license: ContentLicenseFragment): string {\n return `${JSON.stringify(buildTakuhonJson(license), null, 2)}\\n`;\n}\n","/**\n * Generator for the scaffolded project's `tsconfig.json`.\n *\n * Minimal configuration that makes the generated `src/index.ts` typecheck\n * with `tsc --noEmit` in a freshly scaffolded directory. The settings\n * mirror the upstream takuhon monorepo defaults (ES2022 target, ESNext\n * modules, strict mode, `noUncheckedIndexedAccess`) and explicitly enable\n * the two flags the Worker entry depends on:\n *\n * - `resolveJsonModule` — so `import takuhonJson from '../takuhon.json'`\n * resolves at type-check time.\n * - `moduleResolution: \"Bundler\"` — so the Wrangler / esbuild-style\n * resolution (no need for explicit file extensions) is the source of\n * truth that matches what `wrangler dev` actually runs.\n *\n * `noEmit: true` keeps `tsc` purely a type-checker; Wrangler's bundler\n * produces the deployable artifact.\n */\n\nexport function renderTsconfigJson(): string {\n const config = {\n compilerOptions: {\n target: 'ES2022',\n lib: ['ES2022'],\n module: 'ESNext',\n moduleResolution: 'Bundler',\n esModuleInterop: true,\n resolveJsonModule: true,\n isolatedModules: true,\n verbatimModuleSyntax: true,\n strict: true,\n noUncheckedIndexedAccess: true,\n noImplicitOverride: true,\n noFallthroughCasesInSwitch: true,\n skipLibCheck: true,\n noEmit: true,\n },\n include: ['src/**/*'],\n exclude: ['node_modules'],\n };\n return `${JSON.stringify(config, null, 2)}\\n`;\n}\n","/**\n * Generator for `src/index.ts` — the Cloudflare Worker entry file written\n * into a freshly scaffolded project.\n *\n * The generated file uses `createTakuhonWorker()` from `@takuhon/cloudflare`\n * so the scaffolded project does not need to know the internal wiring\n * (Hono router, KV-backed storage, edge cache purger, console audit logger).\n * The user's own `takuhon.json` is loaded via an ES module JSON import,\n * validated once at module load, and the resulting `Takuhon` value is\n * served as the fallback when KV has no stored profile yet.\n *\n * `wrangler.toml`'s `main` field already points at `src/index.ts`, and\n * `nodejs_compat` is enabled, so the file works under `wrangler dev` /\n * `wrangler deploy` without further configuration.\n */\n\nexport function renderWorkerIndexTs(): string {\n return `import { createTakuhonWorker } from '@takuhon/cloudflare';\nimport { validate } from '@takuhon/core';\n\n// Use the project's own takuhon.json as the fallback served when KV has no\n// stored profile yet. Validated once at module load so a malformed profile\n// fails fast rather than at first request.\nimport takuhonJson from '../takuhon.json' with { type: 'json' };\n\nconst fallback = validate(takuhonJson);\nif (!fallback.ok) {\n throw new Error(\n 'Project takuhon.json failed validation: ' +\n fallback.errors.map((e) => \\`\\${e.pointer}: \\${e.message}\\`).join('; '),\n );\n}\n\nconst fallbackTakuhon = fallback.data;\n\nexport default createTakuhonWorker({\n fallback: () => fallbackTakuhon,\n});\n`;\n}\n"],"mappings":"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","names":[]}