@kb-labs/site-tools 2.96.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/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @kb-labs/site-tools
2
+
3
+ > Part of [KB Labs](https://github.com/KirillBaranov/kb-labs) ecosystem.
4
+
5
+ Developer tools for the KB Labs marketing site — scaffold pages, SEO checks, i18n validation.
@@ -0,0 +1,10 @@
1
+ import * as _kb_labs_shared_command_kit from '@kb-labs/shared-command-kit';
2
+ import { CLIInput } from '@kb-labs/sdk';
3
+
4
+ interface PageFlags {
5
+ badge?: string;
6
+ 'dry-run'?: boolean;
7
+ }
8
+ declare const _default: _kb_labs_shared_command_kit.CommandHandlerV3<unknown, CLIInput<PageFlags>, unknown>;
9
+
10
+ export { _default as default };
@@ -0,0 +1,171 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
2
+ import { normalize, join, resolve } from 'path';
3
+ import { defineCommand } from '@kb-labs/sdk';
4
+
5
+ // src/commands/page.ts
6
+ function dotSet(obj, path, value) {
7
+ const parts = path.split(".");
8
+ let cur = obj;
9
+ for (let i = 0; i < parts.length - 1; i++) {
10
+ const key = parts[i];
11
+ if (cur[key] == null || typeof cur[key] !== "object" || Array.isArray(cur[key])) {
12
+ cur[key] = {};
13
+ }
14
+ cur = cur[key];
15
+ }
16
+ const last = parts[parts.length - 1];
17
+ cur[last] = value;
18
+ }
19
+ function pageTemplate(segment, namespace) {
20
+ const routePath = `/${segment}`;
21
+ const words = segment.split(/[-/]/);
22
+ const componentName = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") + "Page";
23
+ return `import type { Metadata } from 'next';
24
+ import { getTranslations, setRequestLocale } from 'next-intl/server';
25
+ import { routing } from '@/i18n/routing';
26
+ import { buildPageMetadata } from '@/lib/page-metadata';
27
+
28
+ type Props = { params: Promise<{ locale: string }> };
29
+
30
+ export function generateStaticParams() {
31
+ return routing.locales.map((locale) => ({ locale }));
32
+ }
33
+
34
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
35
+ const { locale } = await params;
36
+ const t = await getTranslations({ locale, namespace: '${namespace}' });
37
+ return buildPageMetadata({
38
+ locale,
39
+ title: t('meta.title'),
40
+ description: t('meta.description'),
41
+ path: '${routePath}',
42
+ imageSegment: '${segment}',
43
+ });
44
+ }
45
+
46
+ export default async function ${componentName}({ params }: Props) {
47
+ const { locale } = await params;
48
+ setRequestLocale(locale);
49
+
50
+ return (
51
+ <main>
52
+ {/* TODO: add page content */}
53
+ </main>
54
+ );
55
+ }
56
+ `;
57
+ }
58
+ function ogTemplate(segment, namespace, badge) {
59
+ const displayName = segment.split("/").pop() ?? segment;
60
+ const altText = "KB Labs \u2014 " + displayName.charAt(0).toUpperCase() + displayName.slice(1);
61
+ const badgeLine = badge ? `
62
+ badge: '${badge}',` : "";
63
+ return `import { getTranslations } from 'next-intl/server';
64
+ import { renderOgImage, OG_SIZE, OG_CONTENT_TYPE } from '@kb-labs/web-og';
65
+
66
+ export const alt = '${altText}';
67
+ export const size = OG_SIZE;
68
+ export const contentType = OG_CONTENT_TYPE;
69
+
70
+ export default async function OpengraphImage({
71
+ params,
72
+ }: {
73
+ params: Promise<{ locale: string }>;
74
+ }) {
75
+ const { locale } = await params;
76
+ const t = await getTranslations({ locale, namespace: '${namespace}' });
77
+ return renderOgImage({
78
+ title: t('meta.title'),
79
+ description: t('meta.description'),${badgeLine}
80
+ });
81
+ }
82
+ `;
83
+ }
84
+ async function runPage(ctx, input) {
85
+ const argv = input.argv ?? [];
86
+ const segment = argv.find((a) => !a.startsWith("-"));
87
+ if (!segment) {
88
+ ctx.ui.error("Usage: kb site page <segment> [--badge <text>] [--dry-run]");
89
+ ctx.ui.write(
90
+ "Examples:\n kb site page pricing\n kb site page solutions/my-solution --badge Solutions\n"
91
+ );
92
+ return { exitCode: 1 };
93
+ }
94
+ if (!/^[a-z0-9][a-z0-9/-]*$/.test(segment) || segment.includes("..") || normalize(segment) !== segment) {
95
+ ctx.ui.error(`Invalid segment '${segment}' \u2014 use only lowercase letters, digits, hyphens, and forward slashes`);
96
+ return { exitCode: 1 };
97
+ }
98
+ const badge = input.flags?.badge ?? "";
99
+ const dryRun = input.flags?.["dry-run"] ?? false;
100
+ const workspaceRoot = ctx.cwd ?? process.cwd();
101
+ const siteRoot = join(workspaceRoot, "sites", "web", "apps", "web");
102
+ const localeDir = join(siteRoot, "app", "[locale]");
103
+ const segmentDir = resolve(join(localeDir, segment));
104
+ if (!segmentDir.startsWith(resolve(localeDir) + "/") && segmentDir !== resolve(localeDir)) {
105
+ ctx.ui.error(`Segment '${segment}' resolves outside app/[locale]/ \u2014 aborting`);
106
+ return { exitCode: 1 };
107
+ }
108
+ const namespace = segment.split(/[-/]/).map(
109
+ (part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
110
+ ).join("");
111
+ const pagePath = join(segmentDir, "page.tsx");
112
+ const ogPath = join(segmentDir, "opengraph-image.tsx");
113
+ const enJsonPath = join(siteRoot, "messages", "en.json");
114
+ const ruJsonPath = join(siteRoot, "messages", "ru.json");
115
+ if (existsSync(pagePath)) {
116
+ ctx.ui.error(`page.tsx already exists at app/[locale]/${segment}/`);
117
+ return { exitCode: 1 };
118
+ }
119
+ if (existsSync(ogPath)) {
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 };
122
+ }
123
+ const pageContent = pageTemplate(segment, namespace);
124
+ const ogContent = ogTemplate(segment, namespace, badge);
125
+ const titleStub = "TODO: 15\u201365 chars \u2014 Page Title \u2014 KB Labs";
126
+ const descStub = "TODO: 70\u2013165 chars \u2014 Describe what this page is about and why it matters.";
127
+ if (!dryRun) {
128
+ mkdirSync(segmentDir, { recursive: true });
129
+ writeFileSync(pagePath, pageContent, "utf8");
130
+ writeFileSync(ogPath, ogContent, "utf8");
131
+ for (const jsonPath of [enJsonPath, ruJsonPath]) {
132
+ const messages = JSON.parse(readFileSync(jsonPath, "utf8"));
133
+ dotSet(messages, `${namespace}.meta.title`, titleStub);
134
+ dotSet(messages, `${namespace}.meta.description`, descStub);
135
+ writeFileSync(jsonPath, JSON.stringify(messages, null, 2) + "\n", "utf8");
136
+ }
137
+ }
138
+ const prefix = dryRun ? "[dry-run] would create" : "Created";
139
+ ctx.ui.success(`${prefix} page '${segment}'`, {
140
+ title: "site page",
141
+ sections: [
142
+ {
143
+ header: "Files",
144
+ items: [
145
+ `app/[locale]/${segment}/page.tsx`,
146
+ `app/[locale]/${segment}/opengraph-image.tsx`,
147
+ `messages/en.json (key: ${namespace}.meta.{title,description})`,
148
+ `messages/ru.json (key: ${namespace}.meta.{title,description})`
149
+ ]
150
+ },
151
+ {
152
+ header: "Next steps",
153
+ items: [
154
+ `Fill in ${namespace}.meta.title and .description in messages/en.json + ru.json`,
155
+ `Add page content to app/[locale]/${segment}/page.tsx`,
156
+ `Add to sitemap.ts: { path: '/${segment}', priority: 0.7, changeFrequency: 'monthly' }`
157
+ ]
158
+ }
159
+ ]
160
+ });
161
+ return { exitCode: 0 };
162
+ }
163
+ var page_default = defineCommand({
164
+ id: "site-tools:page",
165
+ description: "Scaffold a new marketing site page with SEO defaults",
166
+ handler: { execute: runPage }
167
+ });
168
+
169
+ export { page_default as default };
170
+ //# sourceMappingURL=page.js.map
171
+ //# sourceMappingURL=page.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,2 @@
1
+ export { default as manifest } from './manifest.js';
2
+ import '@kb-labs/plugin-contracts';
package/dist/index.js ADDED
@@ -0,0 +1,55 @@
1
+ import { combinePermissions, kbPlatformPreset } from '@kb-labs/sdk';
2
+
3
+ // src/manifest.ts
4
+ var permissions = combinePermissions().with(kbPlatformPreset).withFs({
5
+ mode: "readWrite",
6
+ allow: [
7
+ "sites/web/apps/web/app/[locale]/**",
8
+ "sites/web/apps/web/messages/*.json"
9
+ ]
10
+ }).withQuotas({ timeoutMs: 3e4, memoryMb: 128 }).build();
11
+ var manifest = {
12
+ schema: "kb.plugin/3",
13
+ id: "@kb-labs/site-tools",
14
+ version: "0.1.0",
15
+ display: {
16
+ name: "Site Tools",
17
+ description: "Developer tools for the KB Labs marketing site",
18
+ tags: ["site", "scaffold", "seo", "dx"]
19
+ },
20
+ cli: {
21
+ groupMeta: [
22
+ { path: "site", describe: "Marketing site developer tools" }
23
+ ],
24
+ commands: [
25
+ {
26
+ path: "site page",
27
+ describe: "Scaffold a new site page with SEO defaults (page.tsx + opengraph-image.tsx + i18n stubs)",
28
+ handler: "./commands/page.js#default",
29
+ flags: [
30
+ {
31
+ name: "badge",
32
+ type: "string",
33
+ description: 'Badge text shown on the OG image (e.g. "Product", "Solutions")'
34
+ },
35
+ {
36
+ name: "dry-run",
37
+ type: "boolean",
38
+ description: "Preview what would be created without writing files"
39
+ }
40
+ ],
41
+ examples: [
42
+ { command: "kb site page pricing", description: "Create /pricing page" },
43
+ { command: "kb site page solutions/my-solution --badge Solutions", description: "Create a solutions sub-page" },
44
+ { command: "kb site page new-feature --dry-run", description: "Preview without writing" }
45
+ ]
46
+ }
47
+ ]
48
+ },
49
+ permissions
50
+ };
51
+ var manifest_default = manifest;
52
+
53
+ export { manifest_default as manifest };
54
+ //# sourceMappingURL=index.js.map
55
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/manifest.ts"],"names":[],"mappings":";;;AAEA,IAAM,cAAc,kBAAA,EAAmB,CACpC,IAAA,CAAK,gBAAgB,EACrB,MAAA,CAAO;AAAA,EACN,IAAA,EAAM,WAAA;AAAA,EACN,KAAA,EAAO;AAAA,IACL,oCAAA;AAAA,IACA;AAAA;AAEJ,CAAC,CAAA,CACA,WAAW,EAAE,SAAA,EAAW,KAAO,QAAA,EAAU,GAAA,EAAK,CAAA,CAC9C,KAAA,EAAM;AAET,IAAM,QAAA,GAAW;AAAA,EACf,MAAA,EAAQ,aAAA;AAAA,EACR,EAAA,EAAI,qBAAA;AAAA,EACJ,OAAA,EAAS,OAAA;AAAA,EACT,OAAA,EAAS;AAAA,IACP,IAAA,EAAM,YAAA;AAAA,IACN,WAAA,EAAa,gDAAA;AAAA,IACb,IAAA,EAAM,CAAC,MAAA,EAAQ,UAAA,EAAY,OAAO,IAAI;AAAA,GACxC;AAAA,EACA,GAAA,EAAK;AAAA,IACH,SAAA,EAAW;AAAA,MACT,EAAE,IAAA,EAAM,MAAA,EAAQ,QAAA,EAAU,gCAAA;AAAiC,KAC7D;AAAA,IACA,QAAA,EAAU;AAAA,MACR;AAAA,QACE,IAAA,EAAM,WAAA;AAAA,QACN,QAAA,EAAU,0FAAA;AAAA,QACV,OAAA,EAAS,4BAAA;AAAA,QACT,KAAA,EAAO;AAAA,UACL;AAAA,YACE,IAAA,EAAM,OAAA;AAAA,YACN,IAAA,EAAM,QAAA;AAAA,YACN,WAAA,EAAa;AAAA,WACf;AAAA,UACA;AAAA,YACE,IAAA,EAAM,SAAA;AAAA,YACN,IAAA,EAAM,SAAA;AAAA,YACN,WAAA,EAAa;AAAA;AACf,SACF;AAAA,QACA,QAAA,EAAU;AAAA,UACR,EAAE,OAAA,EAAS,sBAAA,EAAwB,WAAA,EAAa,sBAAA,EAAuB;AAAA,UACvE,EAAE,OAAA,EAAS,sDAAA,EAAwD,WAAA,EAAa,6BAAA,EAA8B;AAAA,UAC9G,EAAE,OAAA,EAAS,oCAAA,EAAsC,WAAA,EAAa,yBAAA;AAA0B;AAC1F;AACF;AACF,GACF;AAAA,EACA;AACF,CAAA;AAEA,IAAO,gBAAA,GAAQ","file":"index.js","sourcesContent":["import { combinePermissions, kbPlatformPreset } from '@kb-labs/sdk';\n\nconst permissions = combinePermissions()\n .with(kbPlatformPreset)\n .withFs({\n mode: 'readWrite',\n allow: [\n 'sites/web/apps/web/app/[locale]/**',\n 'sites/web/apps/web/messages/*.json',\n ],\n })\n .withQuotas({ timeoutMs: 30000, memoryMb: 128 })\n .build();\n\nconst manifest = {\n schema: 'kb.plugin/3',\n id: '@kb-labs/site-tools',\n version: '0.1.0',\n display: {\n name: 'Site Tools',\n description: 'Developer tools for the KB Labs marketing site',\n tags: ['site', 'scaffold', 'seo', 'dx'],\n },\n cli: {\n groupMeta: [\n { path: 'site', describe: 'Marketing site developer tools' },\n ],\n commands: [\n {\n path: 'site page',\n describe: 'Scaffold a new site page with SEO defaults (page.tsx + opengraph-image.tsx + i18n stubs)',\n handler: './commands/page.js#default',\n flags: [\n {\n name: 'badge',\n type: 'string',\n description: 'Badge text shown on the OG image (e.g. \"Product\", \"Solutions\")',\n },\n {\n name: 'dry-run',\n type: 'boolean',\n description: 'Preview what would be created without writing files',\n },\n ],\n examples: [\n { command: 'kb site page pricing', description: 'Create /pricing page' },\n { command: 'kb site page solutions/my-solution --badge Solutions', description: 'Create a solutions sub-page' },\n { command: 'kb site page new-feature --dry-run', description: 'Preview without writing' },\n ],\n },\n ],\n },\n permissions,\n};\n\nexport default manifest;\n"]}
@@ -0,0 +1,35 @@
1
+ import * as _kb_labs_plugin_contracts from '@kb-labs/plugin-contracts';
2
+
3
+ declare const manifest: {
4
+ schema: string;
5
+ id: string;
6
+ version: string;
7
+ display: {
8
+ name: string;
9
+ description: string;
10
+ tags: string[];
11
+ };
12
+ cli: {
13
+ groupMeta: {
14
+ path: string;
15
+ describe: string;
16
+ }[];
17
+ commands: {
18
+ path: string;
19
+ describe: string;
20
+ handler: string;
21
+ flags: {
22
+ name: string;
23
+ type: string;
24
+ description: string;
25
+ }[];
26
+ examples: {
27
+ command: string;
28
+ description: string;
29
+ }[];
30
+ }[];
31
+ };
32
+ permissions: _kb_labs_plugin_contracts.PermissionSpec;
33
+ };
34
+
35
+ export { manifest as default };
@@ -0,0 +1,55 @@
1
+ import { combinePermissions, kbPlatformPreset } from '@kb-labs/sdk';
2
+
3
+ // src/manifest.ts
4
+ var permissions = combinePermissions().with(kbPlatformPreset).withFs({
5
+ mode: "readWrite",
6
+ allow: [
7
+ "sites/web/apps/web/app/[locale]/**",
8
+ "sites/web/apps/web/messages/*.json"
9
+ ]
10
+ }).withQuotas({ timeoutMs: 3e4, memoryMb: 128 }).build();
11
+ var manifest = {
12
+ schema: "kb.plugin/3",
13
+ id: "@kb-labs/site-tools",
14
+ version: "0.1.0",
15
+ display: {
16
+ name: "Site Tools",
17
+ description: "Developer tools for the KB Labs marketing site",
18
+ tags: ["site", "scaffold", "seo", "dx"]
19
+ },
20
+ cli: {
21
+ groupMeta: [
22
+ { path: "site", describe: "Marketing site developer tools" }
23
+ ],
24
+ commands: [
25
+ {
26
+ path: "site page",
27
+ describe: "Scaffold a new site page with SEO defaults (page.tsx + opengraph-image.tsx + i18n stubs)",
28
+ handler: "./commands/page.js#default",
29
+ flags: [
30
+ {
31
+ name: "badge",
32
+ type: "string",
33
+ description: 'Badge text shown on the OG image (e.g. "Product", "Solutions")'
34
+ },
35
+ {
36
+ name: "dry-run",
37
+ type: "boolean",
38
+ description: "Preview what would be created without writing files"
39
+ }
40
+ ],
41
+ examples: [
42
+ { command: "kb site page pricing", description: "Create /pricing page" },
43
+ { command: "kb site page solutions/my-solution --badge Solutions", description: "Create a solutions sub-page" },
44
+ { command: "kb site page new-feature --dry-run", description: "Preview without writing" }
45
+ ]
46
+ }
47
+ ]
48
+ },
49
+ permissions
50
+ };
51
+ var manifest_default = manifest;
52
+
53
+ export { manifest_default as default };
54
+ //# sourceMappingURL=manifest.js.map
55
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/manifest.ts"],"names":[],"mappings":";;;AAEA,IAAM,cAAc,kBAAA,EAAmB,CACpC,IAAA,CAAK,gBAAgB,EACrB,MAAA,CAAO;AAAA,EACN,IAAA,EAAM,WAAA;AAAA,EACN,KAAA,EAAO;AAAA,IACL,oCAAA;AAAA,IACA;AAAA;AAEJ,CAAC,CAAA,CACA,WAAW,EAAE,SAAA,EAAW,KAAO,QAAA,EAAU,GAAA,EAAK,CAAA,CAC9C,KAAA,EAAM;AAET,IAAM,QAAA,GAAW;AAAA,EACf,MAAA,EAAQ,aAAA;AAAA,EACR,EAAA,EAAI,qBAAA;AAAA,EACJ,OAAA,EAAS,OAAA;AAAA,EACT,OAAA,EAAS;AAAA,IACP,IAAA,EAAM,YAAA;AAAA,IACN,WAAA,EAAa,gDAAA;AAAA,IACb,IAAA,EAAM,CAAC,MAAA,EAAQ,UAAA,EAAY,OAAO,IAAI;AAAA,GACxC;AAAA,EACA,GAAA,EAAK;AAAA,IACH,SAAA,EAAW;AAAA,MACT,EAAE,IAAA,EAAM,MAAA,EAAQ,QAAA,EAAU,gCAAA;AAAiC,KAC7D;AAAA,IACA,QAAA,EAAU;AAAA,MACR;AAAA,QACE,IAAA,EAAM,WAAA;AAAA,QACN,QAAA,EAAU,0FAAA;AAAA,QACV,OAAA,EAAS,4BAAA;AAAA,QACT,KAAA,EAAO;AAAA,UACL;AAAA,YACE,IAAA,EAAM,OAAA;AAAA,YACN,IAAA,EAAM,QAAA;AAAA,YACN,WAAA,EAAa;AAAA,WACf;AAAA,UACA;AAAA,YACE,IAAA,EAAM,SAAA;AAAA,YACN,IAAA,EAAM,SAAA;AAAA,YACN,WAAA,EAAa;AAAA;AACf,SACF;AAAA,QACA,QAAA,EAAU;AAAA,UACR,EAAE,OAAA,EAAS,sBAAA,EAAwB,WAAA,EAAa,sBAAA,EAAuB;AAAA,UACvE,EAAE,OAAA,EAAS,sDAAA,EAAwD,WAAA,EAAa,6BAAA,EAA8B;AAAA,UAC9G,EAAE,OAAA,EAAS,oCAAA,EAAsC,WAAA,EAAa,yBAAA;AAA0B;AAC1F;AACF;AACF,GACF;AAAA,EACA;AACF,CAAA;AAEA,IAAO,gBAAA,GAAQ","file":"manifest.js","sourcesContent":["import { combinePermissions, kbPlatformPreset } from '@kb-labs/sdk';\n\nconst permissions = combinePermissions()\n .with(kbPlatformPreset)\n .withFs({\n mode: 'readWrite',\n allow: [\n 'sites/web/apps/web/app/[locale]/**',\n 'sites/web/apps/web/messages/*.json',\n ],\n })\n .withQuotas({ timeoutMs: 30000, memoryMb: 128 })\n .build();\n\nconst manifest = {\n schema: 'kb.plugin/3',\n id: '@kb-labs/site-tools',\n version: '0.1.0',\n display: {\n name: 'Site Tools',\n description: 'Developer tools for the KB Labs marketing site',\n tags: ['site', 'scaffold', 'seo', 'dx'],\n },\n cli: {\n groupMeta: [\n { path: 'site', describe: 'Marketing site developer tools' },\n ],\n commands: [\n {\n path: 'site page',\n describe: 'Scaffold a new site page with SEO defaults (page.tsx + opengraph-image.tsx + i18n stubs)',\n handler: './commands/page.js#default',\n flags: [\n {\n name: 'badge',\n type: 'string',\n description: 'Badge text shown on the OG image (e.g. \"Product\", \"Solutions\")',\n },\n {\n name: 'dry-run',\n type: 'boolean',\n description: 'Preview what would be created without writing files',\n },\n ],\n examples: [\n { command: 'kb site page pricing', description: 'Create /pricing page' },\n { command: 'kb site page solutions/my-solution --badge Solutions', description: 'Create a solutions sub-page' },\n { command: 'kb site page new-feature --dry-run', description: 'Preview without writing' },\n ],\n },\n ],\n },\n permissions,\n};\n\nexport default manifest;\n"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@kb-labs/site-tools",
3
+ "description": "Developer tools for the KB Labs marketing site (scaffold pages, SEO checks).",
4
+ "version": "2.96.0",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./plugin-manifest": {
14
+ "types": "./dist/manifest.d.ts",
15
+ "import": "./dist/manifest.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "dependencies": {
22
+ "@kb-labs/sdk": "2.21.2"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^24.3.3",
26
+ "rimraf": "^6.0.1",
27
+ "tsup": "^8.5.0",
28
+ "typescript": "^5.6.3",
29
+ "vitest": "^3.2.6",
30
+ "@kb-labs/devkit": "2.96.0",
31
+ "@kb-labs/plugin-contracts": "2.96.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=20.0.0",
35
+ "pnpm": ">=9.0.0"
36
+ },
37
+ "kb": {
38
+ "manifest": "./dist/manifest.js"
39
+ },
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
+ }