@kb-labs/site-tools 2.109.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.
package/dist/commands/page.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ interface PageFlags {
|
|
|
5
5
|
badge?: string;
|
|
6
6
|
'dry-run'?: boolean;
|
|
7
7
|
}
|
|
8
|
-
|
|
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 };
|
package/dist/commands/page.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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