@kb-labs/site-tools 2.107.0 → 2.110.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.
@@ -5,6 +5,7 @@ interface PageFlags {
5
5
  badge?: string;
6
6
  'dry-run'?: boolean;
7
7
  }
8
- declare const _default: _kb_labs_shared_command_kit.CommandHandlerV3<unknown, CLIInput<PageFlags>, unknown>;
8
+ type PageResult = Record<string, never>;
9
+ declare const _default: _kb_labs_shared_command_kit.CommandHandlerV3<unknown, CLIInput<PageFlags>, PageResult>;
9
10
 
10
11
  export { _default as default };
@@ -89,11 +89,11 @@ async function runPage(ctx, input) {
89
89
  ctx.ui.write(
90
90
  "Examples:\n kb site page pricing\n kb site page solutions/my-solution --badge Solutions\n"
91
91
  );
92
- return { exitCode: 1 };
92
+ return { ok: false, error: "A page segment is required" };
93
93
  }
94
94
  if (!/^[a-z0-9][a-z0-9/-]*$/.test(segment) || segment.includes("..") || normalize(segment) !== segment) {
95
95
  ctx.ui.error(`Invalid segment '${segment}' \u2014 use only lowercase letters, digits, hyphens, and forward slashes`);
96
- return { exitCode: 1 };
96
+ return { ok: false, error: `Invalid segment '${segment}'` };
97
97
  }
98
98
  const badge = input.flags?.badge ?? "";
99
99
  const dryRun = input.flags?.["dry-run"] ?? false;
@@ -103,7 +103,7 @@ async function runPage(ctx, input) {
103
103
  const segmentDir = resolve(join(localeDir, segment));
104
104
  if (!segmentDir.startsWith(resolve(localeDir) + "/") && segmentDir !== resolve(localeDir)) {
105
105
  ctx.ui.error(`Segment '${segment}' resolves outside app/[locale]/ \u2014 aborting`);
106
- return { exitCode: 1 };
106
+ return { ok: false, error: `Segment '${segment}' resolves outside app/[locale]/` };
107
107
  }
108
108
  const namespace = segment.split(/[-/]/).map(
109
109
  (part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
@@ -114,11 +114,11 @@ async function runPage(ctx, input) {
114
114
  const ruJsonPath = join(siteRoot, "messages", "ru.json");
115
115
  if (existsSync(pagePath)) {
116
116
  ctx.ui.error(`page.tsx already exists at app/[locale]/${segment}/`);
117
- return { exitCode: 1 };
117
+ return { ok: false, error: `page.tsx already exists at app/[locale]/${segment}/` };
118
118
  }
119
119
  if (existsSync(ogPath)) {
120
120
  ctx.ui.error(`opengraph-image.tsx already exists at app/[locale]/${segment}/ \u2014 remove it first or use a different segment`);
121
- return { exitCode: 1 };
121
+ return { ok: false, error: `opengraph-image.tsx already exists at app/[locale]/${segment}/` };
122
122
  }
123
123
  const pageContent = pageTemplate(segment, namespace);
124
124
  const ogContent = ogTemplate(segment, namespace, badge);
@@ -158,7 +158,7 @@ async function runPage(ctx, input) {
158
158
  }
159
159
  ]
160
160
  });
161
- return { exitCode: 0 };
161
+ return { ok: true };
162
162
  }
163
163
  var page_default = defineCommand({
164
164
  id: "site-tools:page",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/commands/page.ts"],"names":[],"mappings":"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","file":"page.js","sourcesContent":["import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { join, resolve, normalize } from 'node:path';\nimport { defineCommand, type PluginContextV3, type CLIInput } from '@kb-labs/sdk';\n\ninterface PageFlags {\n badge?: string;\n 'dry-run'?: boolean;\n}\n\ntype PageResult = { exitCode: number };\n\ntype JsonObj = { [key: string]: JsonVal };\ntype JsonVal = string | number | boolean | null | JsonVal[] | JsonObj;\n\nfunction dotSet(obj: JsonObj, path: string, value: string): void {\n const parts = path.split('.');\n let cur: JsonObj = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n const key = parts[i] as string;\n if (cur[key] == null || typeof cur[key] !== 'object' || Array.isArray(cur[key])) {\n cur[key] = {};\n }\n cur = cur[key] as JsonObj;\n }\n const last = parts[parts.length - 1] as string;\n cur[last] = value;\n}\n\nfunction pageTemplate(segment: string, namespace: string): string {\n const routePath = `/${segment}`;\n const words = segment.split(/[-/]/);\n const componentName =\n words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('') + 'Page';\n\n return `import type { Metadata } from 'next';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\nimport { routing } from '@/i18n/routing';\nimport { buildPageMetadata } from '@/lib/page-metadata';\n\ntype Props = { params: Promise<{ locale: string }> };\n\nexport function generateStaticParams() {\n return routing.locales.map((locale) => ({ locale }));\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n const { locale } = await params;\n const t = await getTranslations({ locale, namespace: '${namespace}' });\n return buildPageMetadata({\n locale,\n title: t('meta.title'),\n description: t('meta.description'),\n path: '${routePath}',\n imageSegment: '${segment}',\n });\n}\n\nexport default async function ${componentName}({ params }: Props) {\n const { locale } = await params;\n setRequestLocale(locale);\n\n return (\n <main>\n {/* TODO: add page content */}\n </main>\n );\n}\n`;\n}\n\nfunction ogTemplate(segment: string, namespace: string, badge: string): string {\n const displayName = segment.split('/').pop() ?? segment;\n const altText =\n 'KB Labs — ' + displayName.charAt(0).toUpperCase() + displayName.slice(1);\n const badgeLine = badge ? `\\n badge: '${badge}',` : '';\n\n return `import { getTranslations } from 'next-intl/server';\nimport { renderOgImage, OG_SIZE, OG_CONTENT_TYPE } from '@kb-labs/web-og';\n\nexport const alt = '${altText}';\nexport const size = OG_SIZE;\nexport const contentType = OG_CONTENT_TYPE;\n\nexport default async function OpengraphImage({\n params,\n}: {\n params: Promise<{ locale: string }>;\n}) {\n const { locale } = await params;\n const t = await getTranslations({ locale, namespace: '${namespace}' });\n return renderOgImage({\n title: t('meta.title'),\n description: t('meta.description'),${badgeLine}\n });\n}\n`;\n}\n\nasync function runPage(\n ctx: PluginContextV3,\n input: CLIInput<PageFlags>,\n): Promise<PageResult> {\n const argv = input.argv ?? [];\n const segment = argv.find((a) => !a.startsWith('-'));\n\n if (!segment) {\n ctx.ui.error('Usage: kb site page <segment> [--badge <text>] [--dry-run]');\n ctx.ui.write(\n 'Examples:\\n kb site page pricing\\n kb site page solutions/my-solution --badge Solutions\\n',\n );\n return { exitCode: 1 };\n }\n\n // Reject path traversal — only forward slashes and alphanumeric/hyphen segments allowed\n if (!/^[a-z0-9][a-z0-9/-]*$/.test(segment) || segment.includes('..') || normalize(segment) !== segment) {\n ctx.ui.error(`Invalid segment '${segment}' — use only lowercase letters, digits, hyphens, and forward slashes`);\n return { exitCode: 1 };\n }\n\n const badge = input.flags?.badge ?? '';\n const dryRun = input.flags?.['dry-run'] ?? false;\n\n const workspaceRoot = ctx.cwd ?? process.cwd();\n const siteRoot = join(workspaceRoot, 'sites', 'web', 'apps', 'web');\n const localeDir = join(siteRoot, 'app', '[locale]');\n const segmentDir = resolve(join(localeDir, segment));\n\n // Double-check the resolved path is still inside localeDir (defense in depth)\n if (!segmentDir.startsWith(resolve(localeDir) + '/') && segmentDir !== resolve(localeDir)) {\n ctx.ui.error(`Segment '${segment}' resolves outside app/[locale]/ — aborting`);\n return { exitCode: 1 };\n }\n\n const namespace = segment\n .split(/[-/]/)\n .map((part, i) =>\n i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1),\n )\n .join('');\n\n const pagePath = join(segmentDir, 'page.tsx');\n const ogPath = join(segmentDir, 'opengraph-image.tsx');\n const enJsonPath = join(siteRoot, 'messages', 'en.json');\n const ruJsonPath = join(siteRoot, 'messages', 'ru.json');\n\n if (existsSync(pagePath)) {\n ctx.ui.error(`page.tsx already exists at app/[locale]/${segment}/`);\n return { exitCode: 1 };\n }\n\n if (existsSync(ogPath)) {\n ctx.ui.error(`opengraph-image.tsx already exists at app/[locale]/${segment}/ — remove it first or use a different segment`);\n return { exitCode: 1 };\n }\n\n const pageContent = pageTemplate(segment, namespace);\n const ogContent = ogTemplate(segment, namespace, badge);\n const titleStub = 'TODO: 15–65 chars — Page Title — KB Labs';\n const descStub = 'TODO: 70–165 chars — Describe what this page is about and why it matters.';\n\n if (!dryRun) {\n mkdirSync(segmentDir, { recursive: true });\n writeFileSync(pagePath, pageContent, 'utf8');\n writeFileSync(ogPath, ogContent, 'utf8');\n\n for (const jsonPath of [enJsonPath, ruJsonPath]) {\n const messages = JSON.parse(readFileSync(jsonPath, 'utf8')) as JsonObj;\n dotSet(messages, `${namespace}.meta.title`, titleStub);\n dotSet(messages, `${namespace}.meta.description`, descStub);\n writeFileSync(jsonPath, JSON.stringify(messages, null, 2) + '\\n', 'utf8');\n }\n }\n\n const prefix = dryRun ? '[dry-run] would create' : 'Created';\n\n ctx.ui.success(`${prefix} page '${segment}'`, {\n title: 'site page',\n sections: [\n {\n header: 'Files',\n items: [\n `app/[locale]/${segment}/page.tsx`,\n `app/[locale]/${segment}/opengraph-image.tsx`,\n `messages/en.json (key: ${namespace}.meta.{title,description})`,\n `messages/ru.json (key: ${namespace}.meta.{title,description})`,\n ],\n },\n {\n header: 'Next steps',\n items: [\n `Fill in ${namespace}.meta.title and .description in messages/en.json + ru.json`,\n `Add page content to app/[locale]/${segment}/page.tsx`,\n `Add to sitemap.ts: { path: '/${segment}', priority: 0.7, changeFrequency: 'monthly' }`,\n ],\n },\n ],\n });\n\n return { exitCode: 0 };\n}\n\nexport default defineCommand({\n id: 'site-tools:page',\n description: 'Scaffold a new marketing site page with SEO defaults',\n handler: { execute: runPage },\n});\n"]}
1
+ {"version":3,"sources":["../../src/commands/page.ts"],"names":[],"mappings":"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","file":"page.js","sourcesContent":["import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { join, resolve, normalize } from 'node:path';\nimport { defineCommand, type PluginContextV3, type CLIInput, type CommandResult } from '@kb-labs/sdk';\n\ninterface PageFlags {\n badge?: string;\n 'dry-run'?: boolean;\n}\n\ntype PageResult = Record<string, never>;\n\ntype JsonObj = { [key: string]: JsonVal };\ntype JsonVal = string | number | boolean | null | JsonVal[] | JsonObj;\n\nfunction dotSet(obj: JsonObj, path: string, value: string): void {\n const parts = path.split('.');\n let cur: JsonObj = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n const key = parts[i] as string;\n if (cur[key] == null || typeof cur[key] !== 'object' || Array.isArray(cur[key])) {\n cur[key] = {};\n }\n cur = cur[key] as JsonObj;\n }\n const last = parts[parts.length - 1] as string;\n cur[last] = value;\n}\n\nfunction pageTemplate(segment: string, namespace: string): string {\n const routePath = `/${segment}`;\n const words = segment.split(/[-/]/);\n const componentName =\n words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('') + 'Page';\n\n return `import type { Metadata } from 'next';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\nimport { routing } from '@/i18n/routing';\nimport { buildPageMetadata } from '@/lib/page-metadata';\n\ntype Props = { params: Promise<{ locale: string }> };\n\nexport function generateStaticParams() {\n return routing.locales.map((locale) => ({ locale }));\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n const { locale } = await params;\n const t = await getTranslations({ locale, namespace: '${namespace}' });\n return buildPageMetadata({\n locale,\n title: t('meta.title'),\n description: t('meta.description'),\n path: '${routePath}',\n imageSegment: '${segment}',\n });\n}\n\nexport default async function ${componentName}({ params }: Props) {\n const { locale } = await params;\n setRequestLocale(locale);\n\n return (\n <main>\n {/* TODO: add page content */}\n </main>\n );\n}\n`;\n}\n\nfunction ogTemplate(segment: string, namespace: string, badge: string): string {\n const displayName = segment.split('/').pop() ?? segment;\n const altText =\n 'KB Labs — ' + displayName.charAt(0).toUpperCase() + displayName.slice(1);\n const badgeLine = badge ? `\\n badge: '${badge}',` : '';\n\n return `import { getTranslations } from 'next-intl/server';\nimport { renderOgImage, OG_SIZE, OG_CONTENT_TYPE } from '@kb-labs/web-og';\n\nexport const alt = '${altText}';\nexport const size = OG_SIZE;\nexport const contentType = OG_CONTENT_TYPE;\n\nexport default async function OpengraphImage({\n params,\n}: {\n params: Promise<{ locale: string }>;\n}) {\n const { locale } = await params;\n const t = await getTranslations({ locale, namespace: '${namespace}' });\n return renderOgImage({\n title: t('meta.title'),\n description: t('meta.description'),${badgeLine}\n });\n}\n`;\n}\n\nasync function runPage(\n ctx: PluginContextV3,\n input: CLIInput<PageFlags>,\n): Promise<CommandResult<PageResult>> {\n const argv = input.argv ?? [];\n const segment = argv.find((a) => !a.startsWith('-'));\n\n if (!segment) {\n ctx.ui.error('Usage: kb site page <segment> [--badge <text>] [--dry-run]');\n ctx.ui.write(\n 'Examples:\\n kb site page pricing\\n kb site page solutions/my-solution --badge Solutions\\n',\n );\n return { ok: false, error: 'A page segment is required' };\n }\n\n // Reject path traversal — only forward slashes and alphanumeric/hyphen segments allowed\n if (!/^[a-z0-9][a-z0-9/-]*$/.test(segment) || segment.includes('..') || normalize(segment) !== segment) {\n ctx.ui.error(`Invalid segment '${segment}' — use only lowercase letters, digits, hyphens, and forward slashes`);\n return { ok: false, error: `Invalid segment '${segment}'` };\n }\n\n const badge = input.flags?.badge ?? '';\n const dryRun = input.flags?.['dry-run'] ?? false;\n\n const workspaceRoot = ctx.cwd ?? process.cwd();\n const siteRoot = join(workspaceRoot, 'sites', 'web', 'apps', 'web');\n const localeDir = join(siteRoot, 'app', '[locale]');\n const segmentDir = resolve(join(localeDir, segment));\n\n // Double-check the resolved path is still inside localeDir (defense in depth)\n if (!segmentDir.startsWith(resolve(localeDir) + '/') && segmentDir !== resolve(localeDir)) {\n ctx.ui.error(`Segment '${segment}' resolves outside app/[locale]/ — aborting`);\n return { ok: false, error: `Segment '${segment}' resolves outside app/[locale]/` };\n }\n\n const namespace = segment\n .split(/[-/]/)\n .map((part, i) =>\n i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1),\n )\n .join('');\n\n const pagePath = join(segmentDir, 'page.tsx');\n const ogPath = join(segmentDir, 'opengraph-image.tsx');\n const enJsonPath = join(siteRoot, 'messages', 'en.json');\n const ruJsonPath = join(siteRoot, 'messages', 'ru.json');\n\n if (existsSync(pagePath)) {\n ctx.ui.error(`page.tsx already exists at app/[locale]/${segment}/`);\n return { ok: false, error: `page.tsx already exists at app/[locale]/${segment}/` };\n }\n\n if (existsSync(ogPath)) {\n ctx.ui.error(`opengraph-image.tsx already exists at app/[locale]/${segment}/ — remove it first or use a different segment`);\n return { ok: false, error: `opengraph-image.tsx already exists at app/[locale]/${segment}/` };\n }\n\n const pageContent = pageTemplate(segment, namespace);\n const ogContent = ogTemplate(segment, namespace, badge);\n const titleStub = 'TODO: 15–65 chars — Page Title — KB Labs';\n const descStub = 'TODO: 70–165 chars — Describe what this page is about and why it matters.';\n\n if (!dryRun) {\n mkdirSync(segmentDir, { recursive: true });\n writeFileSync(pagePath, pageContent, 'utf8');\n writeFileSync(ogPath, ogContent, 'utf8');\n\n for (const jsonPath of [enJsonPath, ruJsonPath]) {\n const messages = JSON.parse(readFileSync(jsonPath, 'utf8')) as JsonObj;\n dotSet(messages, `${namespace}.meta.title`, titleStub);\n dotSet(messages, `${namespace}.meta.description`, descStub);\n writeFileSync(jsonPath, JSON.stringify(messages, null, 2) + '\\n', 'utf8');\n }\n }\n\n const prefix = dryRun ? '[dry-run] would create' : 'Created';\n\n ctx.ui.success(`${prefix} page '${segment}'`, {\n title: 'site page',\n sections: [\n {\n header: 'Files',\n items: [\n `app/[locale]/${segment}/page.tsx`,\n `app/[locale]/${segment}/opengraph-image.tsx`,\n `messages/en.json (key: ${namespace}.meta.{title,description})`,\n `messages/ru.json (key: ${namespace}.meta.{title,description})`,\n ],\n },\n {\n header: 'Next steps',\n items: [\n `Fill in ${namespace}.meta.title and .description in messages/en.json + ru.json`,\n `Add page content to app/[locale]/${segment}/page.tsx`,\n `Add to sitemap.ts: { path: '/${segment}', priority: 0.7, changeFrequency: 'monthly' }`,\n ],\n },\n ],\n });\n\n return { ok: true };\n}\n\nexport default defineCommand({\n id: 'site-tools:page',\n description: 'Scaffold a new marketing site page with SEO defaults',\n handler: { execute: runPage },\n});\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kb-labs/site-tools",
3
3
  "description": "Developer tools for the KB Labs marketing site (scaffold pages, SEO checks).",
4
- "version": "2.107.0",
4
+ "version": "2.110.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -18,17 +18,27 @@
18
18
  "files": [
19
19
  "dist"
20
20
  ],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "clean": "rimraf dist",
24
+ "dev": "tsup --watch",
25
+ "lint": "eslint src --ext .ts",
26
+ "lint:fix": "eslint . --fix",
27
+ "type-check": "tsc --noEmit",
28
+ "test": "vitest run --config vitest.cli.config.ts --passWithNoTests",
29
+ "test:cli": "vitest run --config vitest.cli.config.ts --passWithNoTests"
30
+ },
21
31
  "dependencies": {
22
- "@kb-labs/sdk": "2.22.2"
32
+ "@kb-labs/sdk": "workspace:*"
23
33
  },
24
34
  "devDependencies": {
35
+ "@kb-labs/devkit": "workspace:*",
36
+ "@kb-labs/plugin-contracts": "workspace:*",
25
37
  "@types/node": "^24.3.3",
26
38
  "rimraf": "^6.0.1",
27
39
  "tsup": "^8.5.0",
28
40
  "typescript": "^5.6.3",
29
- "vitest": "^3.2.6",
30
- "@kb-labs/devkit": "2.107.0",
31
- "@kb-labs/plugin-contracts": "2.107.0"
41
+ "vitest": "^3.2.6"
32
42
  },
33
43
  "engines": {
34
44
  "node": ">=22.0.0",
@@ -37,15 +47,5 @@
37
47
  "kb": {
38
48
  "manifest": "./dist/manifest.js"
39
49
  },
40
- "sideEffects": false,
41
- "scripts": {
42
- "build": "tsup",
43
- "clean": "rimraf dist",
44
- "dev": "tsup --watch",
45
- "lint": "eslint src --ext .ts",
46
- "lint:fix": "eslint . --fix",
47
- "type-check": "tsc --noEmit",
48
- "test": "vitest run --config vitest.cli.config.ts --passWithNoTests",
49
- "test:cli": "vitest run --config vitest.cli.config.ts --passWithNoTests"
50
- }
51
- }
50
+ "sideEffects": false
51
+ }