From de141f1471c0dd97ebe8b7bf8522d270b677c5b4 Mon Sep 17 00:00:00 2001 From: raj-khan Date: Sat, 23 May 2026 00:04:26 +0800 Subject: [PATCH 1/7] Add mobile hamburger nav with slide-down menu --- apps/web/src/app/responsive.css | 9 +- apps/web/src/components/layout/Nav.tsx | 128 ++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/responsive.css b/apps/web/src/app/responsive.css index 24235a0..d0b608a 100644 --- a/apps/web/src/app/responsive.css +++ b/apps/web/src/app/responsive.css @@ -20,8 +20,8 @@ .lk-workbench-grid { grid-template-columns: 200px 1fr !important; } .lk-workbench-chat { display: none !important; } - /* Nav: hide some links */ - .lk-nav-links a:nth-child(n+5) { display: none !important; } + /* Nav: hide last two links on tablet */ + .lk-nav-links a:nth-child(n+4) { display: none !important; } /* Hero typography */ .lk-hero-title { font-size: 56px !important; } @@ -65,8 +65,9 @@ /* Mobile nav */ .lk-nav { padding: 14px 20px !important; } .lk-nav-links { display: none !important; } - .lk-nav-cta { gap: 6px !important; } - .lk-nav-cta .lk-nav-cta-secondary { display: none !important; } + .lk-nav-cta { display: none !important; } + .lk-nav-hamburger { display: flex !important; } + .lk-nav-mobile-menu { display: flex !important; } .lk-subnav { flex-wrap: wrap !important; gap: 12px !important; padding: 14px 20px !important; } .lk-subnav-links { display: none !important; } diff --git a/apps/web/src/components/layout/Nav.tsx b/apps/web/src/components/layout/Nav.tsx index 4fc0625..38aaf9c 100644 --- a/apps/web/src/components/layout/Nav.tsx +++ b/apps/web/src/components/layout/Nav.tsx @@ -1,3 +1,6 @@ +'use client'; + +import { useState, useEffect, useRef } from 'react'; import { Wordmark } from '@/components/ui/Wordmark'; import { Button, ArrowR } from '@/components/ui/Button'; @@ -15,30 +18,57 @@ const GithubIcon = () => ( ); +const NAV_LINKS = [ + { label: 'For teams', href: '/teams' }, + { label: 'Docs', href: '/docs' }, + { label: 'API', href: '/developers' }, + { label: 'Blog', href: '/blog' }, +]; + export function Nav() { + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + if (!open) return; + function handleClick(e: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setOpen(false); + } + } + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [open]); + + // Close on route navigation (hash or pathname change) + useEffect(() => { setOpen(false); }, []); + return ( ); } From 9cd03cdfeab14cf83101e8c32447f9f839df3467 Mon Sep 17 00:00:00 2001 From: raj-khan Date: Sat, 23 May 2026 00:04:30 +0800 Subject: [PATCH 2/7] Add /changelog page and update manifest description --- apps/web/src/app/changelog/page.tsx | 174 ++++++++++++++++++++++ apps/web/src/app/manifest.ts | 2 +- apps/web/src/app/sitemap.ts | 1 + apps/web/src/components/layout/Footer.tsx | 2 +- apps/web/src/lib/changelog-data.ts | 77 ++++++++++ 5 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/changelog/page.tsx create mode 100644 apps/web/src/lib/changelog-data.ts diff --git a/apps/web/src/app/changelog/page.tsx b/apps/web/src/app/changelog/page.tsx new file mode 100644 index 0000000..05d32ee --- /dev/null +++ b/apps/web/src/app/changelog/page.tsx @@ -0,0 +1,174 @@ +import type { Metadata } from 'next'; +import { Nav } from '@/components/layout/Nav'; +import { Footer } from '@/components/layout/Footer'; +import { Eyebrow } from '@/components/ui/primitives'; +import { CHANGELOG, KIND_COLORS } from '@/lib/changelog-data'; +import { SITE_URL } from '@/lib/seo-data'; + +export const metadata: Metadata = { + title: 'Changelog · LearnKit AI', + description: + 'What shipped and when. Every notable change to @learnkit-ai/core, @learnkit-ai/react, and the LearnKit AI web app.', + alternates: { canonical: '/changelog' }, + openGraph: { + type: 'website', + url: `${SITE_URL}/changelog`, + title: 'Changelog · LearnKit AI', + description: 'What shipped and when. Every notable change to the LearnKit AI packages.', + siteName: 'LearnKit AI', + }, +}; + +export default function ChangelogPage() { + return ( +
+
+ ); +} diff --git a/apps/web/src/app/manifest.ts b/apps/web/src/app/manifest.ts index edd69b3..aeef01c 100644 --- a/apps/web/src/app/manifest.ts +++ b/apps/web/src/app/manifest.ts @@ -5,7 +5,7 @@ export default function manifest(): MetadataRoute.Manifest { name: 'LearnKit AI', short_name: 'LearnKit', description: - 'The AI workbench for teams that ship. Learn Claude, Cursor, ChatGPT and 40+ tools by building real things at work.', + 'Open-source TypeScript engine for embedding personalized, role-aware AI learning paths in any product. Apache-2.0.', start_url: '/', display: 'standalone', background_color: '#FAF7F0', diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts index 136e2b4..deebf8d 100644 --- a/apps/web/src/app/sitemap.ts +++ b/apps/web/src/app/sitemap.ts @@ -14,6 +14,7 @@ export default function sitemap(): MetadataRoute.Sitemap { { url: `${SITE_URL}/tools`, lastModified: now, changeFrequency: 'weekly', priority: 0.8 }, { url: `${SITE_URL}/roles`, lastModified: now, changeFrequency: 'weekly', priority: 0.75 }, { url: `${SITE_URL}/blog`, lastModified: now, changeFrequency: 'weekly', priority: 0.7 }, + { url: `${SITE_URL}/changelog`, lastModified: now, changeFrequency: 'weekly', priority: 0.65 }, ]; const toolPages: MetadataRoute.Sitemap = TOOLS.map((t) => ({ diff --git a/apps/web/src/components/layout/Footer.tsx b/apps/web/src/components/layout/Footer.tsx index f9b6de1..378a91c 100644 --- a/apps/web/src/components/layout/Footer.tsx +++ b/apps/web/src/components/layout/Footer.tsx @@ -36,8 +36,8 @@ const LINKS: FooterCol[] = [ l: [ { label: 'Docs', href: '/docs' }, { label: 'API reference', href: '/developers' }, + { label: 'Changelog', href: '/changelog' }, { label: 'GitHub', href: 'https://github.com/learnkit-ai/learnkit' }, - { label: 'Blog', href: '/blog' }, ], }, ]; diff --git a/apps/web/src/lib/changelog-data.ts b/apps/web/src/lib/changelog-data.ts new file mode 100644 index 0000000..32e5daa --- /dev/null +++ b/apps/web/src/lib/changelog-data.ts @@ -0,0 +1,77 @@ +export type ChangeKind = 'Added' | 'Changed' | 'Fixed' | 'Removed'; + +export interface ChangeEntry { + kind: ChangeKind; + text: string; +} + +export interface ChangelogVersion { + version: string; + date: string | null; + entries: ChangeEntry[]; +} + +export const CHANGELOG: ChangelogVersion[] = [ + { + version: 'Unreleased', + date: null, + entries: [ + { kind: 'Added', text: 'Level picker (Beginner / Intermediate / Advanced) in the /demo flow — wired to generateLearningPath() to adjust lesson pacing' }, + { kind: 'Added', text: 'Optional company context textarea in /demo — passes companyContext to the engine to personalise project lesson summaries' }, + { kind: 'Added', text: 'Role-specific 4-week curricula for Marketer, Founder, Operations, and Researcher — all 8 supported roles now have custom content' }, + { kind: 'Added', text: 'ROADMAP.md — v0 done, v1 planned, v2 exploratory, explicit no-go list' }, + { kind: 'Added', text: '4 new blog posts: CSS theming deep-dive, role curriculum design decisions, companyContext field explainer, and SaaS embedding guide' }, + { kind: 'Added', text: 'Mobile hamburger nav — slide-down menu with all nav links, accessible and keyboard-friendly' }, + { kind: 'Added', text: '/changelog page — this page, derived from CHANGELOG.md' }, + { kind: 'Added', text: '@learnkit-ai/cli — npx @learnkit-ai/cli generate outputs a JSON or pretty-printed learning path with zero setup' }, + { kind: 'Changed', text: 'companyContext field now used in project lesson summaries — parsed for stack, pace, and team-size signals' }, + ], + }, + { + version: '0.1.0', + date: '2026-05-12', + entries: [ + { kind: 'Added', text: 'Custom 404 page — branded design with Nav, serif headline, and three CTA links' }, + { kind: 'Added', text: 'OG image — updated headline "Personalized AI paths for every role" and OSS-aligned description' }, + { kind: 'Added', text: 'Example page metadata via layout.tsx (page itself stays \'use client\')' }, + { kind: 'Fixed', text: 'Footer dead links — all href="#" replaced with real GitHub URLs (SECURITY.md, LICENSE, CONTRIBUTING.md)' }, + { kind: 'Fixed', text: '"Docs" footer link corrected to /docs (was pointing to /developers)' }, + { kind: 'Changed', text: 'Footer brand tagline updated to OSS messaging: "Open-source TypeScript engine for embedding personalized AI learning paths"' }, + { kind: 'Added', text: 'CodeBlock copy button — wired with navigator.clipboard.writeText; shows "copied!" with green colour for 1.8s' }, + { kind: 'Added', text: '/teams page — OSS-framed guide for embedding LearnKit AI across an org' }, + { kind: 'Added', text: '/docs page — full API reference with sticky sidebar and IntersectionObserver-based active-section highlighting' }, + { kind: 'Added', text: 'DocsSidebar client component with IntersectionObserver-based active section tracking' }, + { kind: 'Added', text: '6 blog posts across Pedagogy, Engineering, and Launches categories' }, + { kind: 'Added', text: 'tsup build for all packages — ESM + CJS + .d.ts output with "source" custom export condition' }, + { kind: 'Added', text: '21 React component tests — LearningPath, LessonCard, AIGuide, useLearnKit() using @testing-library/react + happy-dom' }, + { kind: 'Added', text: 'Per-role lesson curricula for Software Engineer, Product Manager, Designer, and Data Analyst' }, + { kind: 'Added', text: 'vitest.config.ts with resolve.alias in core and react packages — resolves workspace deps to TypeScript source without pre-build' }, + { kind: 'Changed', text: '/developers page — rewrote to show the real package API surface instead of fabricated REST endpoints' }, + { kind: 'Changed', text: 'UseItYourWay section — replaced pricing cards with OSS install/embed/self-host options' }, + { kind: 'Changed', text: 'Proof section — replaced fake testimonials with real OSS facts' }, + { kind: 'Fixed', text: 'getSupportedRoles() comment in docs now shows the actual return value' }, + { kind: 'Fixed', text: 'next-env.d.ts added to .gitignore' }, + ], + }, + { + version: '0.0.1', + date: '2026-05-01', + entries: [ + { kind: 'Added', text: 'packages/schemas — @learnkit-ai/schemas: Zod schemas and inferred TypeScript types' }, + { kind: 'Added', text: 'packages/core — @learnkit-ai/core: generateLearningPath(), getSupportedRoles(), getSupportedTools(), isRoleSupported()' }, + { kind: 'Added', text: 'packages/react — @learnkit-ai/react: , , , useLearnKit()' }, + { kind: 'Added', text: 'apps/web — Next.js 15 App Router landing page, /demo, /docs, /roles, /tools, /blog, /example, /developers' }, + { kind: 'Added', text: 'examples/nextjs-basic — standalone Next.js integration example' }, + { kind: 'Added', text: 'GitHub Actions CI — install, lint, typecheck, test, build pipeline' }, + { kind: 'Added', text: '.agent/ — Agent Anatomy universal config with rules, commands, agents, tasks' }, + { kind: 'Added', text: 'Apache-2.0 license, CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md' }, + ], + }, +]; + +export const KIND_COLORS: Record = { + Added: { bg: 'rgba(107,143,110,0.12)', color: '#3D7A41' }, + Changed: { bg: 'rgba(91,115,196,0.12)', color: '#3B5BBD' }, + Fixed: { bg: 'rgba(200,71,42,0.10)', color: '#C8472A' }, + Removed: { bg: 'rgba(120,80,80,0.12)', color: '#7A3535' }, +}; From 2ff2d69e30e36f5b5d1147a3c601c50015f8ba4d Mon Sep 17 00:00:00 2001 From: raj-khan Date: Sat, 23 May 2026 00:04:34 +0800 Subject: [PATCH 3/7] Wire companyContext into project lesson summaries --- packages/core/src/__tests__/generate.test.ts | 24 ++++++++ packages/core/src/generate.ts | 64 +++++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/packages/core/src/__tests__/generate.test.ts b/packages/core/src/__tests__/generate.test.ts index 2a6ce0d..8424da1 100644 --- a/packages/core/src/__tests__/generate.test.ts +++ b/packages/core/src/__tests__/generate.test.ts @@ -92,4 +92,28 @@ describe('generateLearningPath', () => { expect(adv.totalMinutes).toBeLessThan(beg.totalMinutes); } }); + + it('appends context suffix to project lessons when companyContext is provided', () => { + const path = generateLearningPath({ ...SAMPLE, companyContext: 'React/TypeScript team, ships weekly' }); + const projects = path.weeks.flatMap((w) => w.lessons.filter((l) => l.kind === 'project')); + for (const p of projects) { + expect(p.summary).toMatch(/react\/typescript|shipping weekly/i); + } + }); + + it('leaves non-project lessons unchanged when companyContext is provided', () => { + const base = generateLearningPath(SAMPLE); + const withCtx = generateLearningPath({ ...SAMPLE, companyContext: 'Python team, ships weekly' }); + const baseLessons = base.weeks.flatMap((w) => w.lessons.filter((l) => l.kind === 'lesson')); + const ctxLessons = withCtx.weeks.flatMap((w) => w.lessons.filter((l) => l.kind === 'lesson')); + for (let i = 0; i < baseLessons.length; i++) { + expect(ctxLessons[i]!.summary).toBe(baseLessons[i]!.summary); + } + }); + + it('produces different path id when companyContext differs', () => { + const a = generateLearningPath(SAMPLE); + const b = generateLearningPath({ ...SAMPLE, companyContext: 'B2B SaaS, 20-person team, Python stack' }); + expect(a.id).not.toBe(b.id); + }); }); diff --git a/packages/core/src/generate.ts b/packages/core/src/generate.ts index df5d017..907ba82 100644 --- a/packages/core/src/generate.ts +++ b/packages/core/src/generate.ts @@ -752,6 +752,62 @@ const LEVEL_BIAS: Record = { advanced: -4, }; +// --------------------------------------------------------------------------- +// Company context parsing +// --------------------------------------------------------------------------- + +const STACK_KEYWORDS = [ + 'react', 'vue', 'angular', 'svelte', 'next.js', 'nextjs', + 'typescript', 'javascript', 'python', 'go', 'rust', 'java', 'kotlin', 'swift', + 'node', 'django', 'rails', 'laravel', 'fastapi', +] as const; + +interface ParsedContext { + stackHint: string | null; + paceHint: string | null; + teamHint: string | null; +} + +function parseCompanyContext(ctx: string): ParsedContext { + const lower = ctx.toLowerCase(); + + // Stack: find first two recognisable tech keywords + const found = STACK_KEYWORDS.filter((k) => lower.includes(k)); + const stackHint = found.length > 0 + ? `your ${found.slice(0, 2).join('/')} stack` + : null; + + // Pace: keyword scan + const paceHint = lower.includes('daily') || lower.includes('ships daily') + ? 'shipping daily' + : lower.includes('weekly') || lower.includes('week') + ? 'shipping weekly' + : lower.includes('quarterly') + ? 'quarterly releases' + : null; + + // Team size: first number followed by "person", "people", "member", "engineer", "developer" + const sizeMatch = lower.match(/(\d+)[- ](?:person|people|member|engineer|developer)/); + const size = sizeMatch ? parseInt(sizeMatch[1], 10) : null; + const teamHint = size !== null + ? size <= 5 + ? 'a small team' + : size <= 20 + ? `a ${size}-person team` + : `a ${size}-person org` + : null; + + return { stackHint, paceHint, teamHint }; +} + +function buildContextSuffix(parsed: ParsedContext): string { + const parts: string[] = []; + if (parsed.stackHint) parts.push(`Adapt this for ${parsed.stackHint}.`); + if (parsed.paceHint) parts.push(`Your cadence (${parsed.paceHint}) means you can validate in one sprint.`); + if (parsed.teamHint) parts.push(`Scope the deliverable for ${parsed.teamHint}.`); + return parts.length > 0 ? ' ' + parts.join(' ') : ''; +} + // --------------------------------------------------------------------------- // Main function // --------------------------------------------------------------------------- @@ -765,18 +821,22 @@ export function generateLearningPath(rawInput: LearningPathInput): LearningPath const primaryTool = normalizedTools[0] ?? 'Claude'; const secondaryTool = normalizedTools[1] ?? primaryTool; - const stableSeed = `${role}|${normalizedTools.join(',')}|${input.goal}|${input.level}`; + const stableSeed = `${role}|${normalizedTools.join(',')}|${input.goal}|${input.level}|${input.companyContext ?? ''}`; + + const parsedCtx = input.companyContext ? parseCompanyContext(input.companyContext) : null; + const ctxSuffix = parsedCtx ? buildContextSuffix(parsedCtx) : ''; const weeks = (ROLE_WEEKS[role] ?? GENERIC_WEEKS).map((w, wi) => { const tool = wi % 2 === 0 ? primaryTool : secondaryTool; const lessons: Lesson[] = w.templates.map((template, li) => { const dayBase = wi * 7 + li * 2 + 1; const minutesAdjusted = Math.max(8, template.minutes + LEVEL_BIAS[input.level]); + const baseSummary = template.summary({ tool, role, goal: input.goal }); return { id: hashId('l', stableSeed, wi, li), day: dayBase, title: template.title({ tool, role }), - summary: template.summary({ tool, role, goal: input.goal }), + summary: template.kind === 'project' ? baseSummary + ctxSuffix : baseSummary, tool, minutes: minutesAdjusted, kind: template.kind, From 1c88ace601eb9b09b975962cb601e06fc35140e8 Mon Sep 17 00:00:00 2001 From: raj-khan Date: Sat, 23 May 2026 00:04:37 +0800 Subject: [PATCH 4/7] Add @learnkit-ai/cli package with generate, roles, and tools commands --- packages/cli/package.json | 42 +++++ packages/cli/src/__tests__/generate.test.ts | 45 ++++++ packages/cli/src/index.ts | 165 ++++++++++++++++++++ packages/cli/tsconfig.json | 19 +++ packages/cli/tsup.config.ts | 13 ++ packages/cli/vitest.config.ts | 11 ++ pnpm-lock.yaml | 19 +++ 7 files changed, 314 insertions(+) create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/__tests__/generate.test.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/cli/tsup.config.ts create mode 100644 packages/cli/vitest.config.ts diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..42ff231 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,42 @@ +{ + "name": "@learnkit-ai/cli", + "version": "0.1.0", + "description": "CLI for @learnkit-ai/core — generate AI learning paths from the terminal", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/learnkit-ai/learnkit.git", + "directory": "packages/cli" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "learnkit-ai": "./dist/index.js" + }, + "files": ["dist"], + "exports": { + ".": { + "source": "./src/index.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts", + "default": "./src/index.ts" + } + }, + "scripts": { + "build": "tsup", + "lint": "tsc -p tsconfig.json --noEmit", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@learnkit-ai/core": "workspace:*", + "@learnkit-ai/schemas": "workspace:*" + }, + "devDependencies": { + "tsup": "^8.0.0", + "typescript": "^5.7.3", + "vitest": "^2.1.9" + } +} diff --git a/packages/cli/src/__tests__/generate.test.ts b/packages/cli/src/__tests__/generate.test.ts new file mode 100644 index 0000000..b4a9052 --- /dev/null +++ b/packages/cli/src/__tests__/generate.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; +import { generateLearningPath, getSupportedRoles, getSupportedTools } from '@learnkit-ai/core'; + +describe('CLI integration — generateLearningPath', () => { + it('generates a valid path for all supported roles', () => { + const roles = getSupportedRoles(); + const tools = getSupportedTools(); + for (const role of roles) { + const path = generateLearningPath({ + role, + tools: [tools[0]!], + goal: 'ship something useful', + level: 'beginner', + }); + expect(path.weeks).toHaveLength(4); + expect(path.totalMinutes).toBeGreaterThan(0); + } + }); + + it('outputs valid JSON-serialisable structure', () => { + const path = generateLearningPath({ + role: 'Software Engineer', + tools: ['Claude', 'Cursor'], + goal: 'ship a research agent', + level: 'intermediate', + companyContext: 'TypeScript/React team, ships weekly', + }); + expect(() => JSON.stringify(path)).not.toThrow(); + const parsed = JSON.parse(JSON.stringify(path)) as typeof path; + expect(parsed.id).toBe(path.id); + }); + + it('company context appears in project lesson summaries', () => { + const path = generateLearningPath({ + role: 'Product Manager', + tools: ['Claude'], + goal: 'improve discovery process', + level: 'beginner', + companyContext: 'Python stack, 15-person team', + }); + const projects = path.weeks.flatMap((w) => w.lessons.filter((l) => l.kind === 'project')); + const combined = projects.map((p) => p.summary).join(' '); + expect(combined).toMatch(/python|15-person/i); + }); +}); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000..f1503c8 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,165 @@ +import { parseArgs } from 'node:util'; +import { generateLearningPath, getSupportedRoles, getSupportedTools } from '@learnkit-ai/core'; + +const HELP = ` +@learnkit-ai/cli — generate AI learning paths from the terminal + +Usage: + npx @learnkit-ai/cli generate [options] + npx @learnkit-ai/cli roles + npx @learnkit-ai/cli tools + +Commands: + generate Generate a learning path and print it + roles List all supported roles + tools List all supported tools + +Options for generate: + --role Learner role (required) e.g. "Product Manager" + --tools Comma-separated tools e.g. "Claude,Cursor" + --goal Learning goal (required) e.g. "ship a research agent" + --level beginner | intermediate | advanced (default: beginner) + --company-context Optional context string e.g. "React team, ships weekly" + --output pretty | json (default: pretty) + -h, --help Show this help + +Examples: + npx @learnkit-ai/cli generate --role "Product Manager" --tools "Claude,Cursor" --goal "ship an internal research agent" + npx @learnkit-ai/cli generate --role "Software Engineer" --tools "Cursor" --goal "automate code review" --level advanced --output json + npx @learnkit-ai/cli roles +`; + +function fatal(msg: string): never { + process.stderr.write(`error: ${msg}\n`); + process.exit(1); +} + +function prettyPrint(json: object): void { + process.stdout.write(JSON.stringify(json, null, 2) + '\n'); +} + +function cmdGenerate(argv: string[]): void { + const { values } = parseArgs({ + args: argv, + options: { + role: { type: 'string' }, + tools: { type: 'string' }, + goal: { type: 'string' }, + level: { type: 'string', default: 'beginner' }, + 'company-context': { type: 'string' }, + output: { type: 'string', default: 'pretty' }, + help: { type: 'boolean', short: 'h', default: false }, + }, + strict: false, + }); + + if (values.help) { + process.stdout.write(HELP); + process.exit(0); + } + + const role = typeof values.role === 'string' ? values.role : null; + const goal = typeof values.goal === 'string' ? values.goal : null; + const rawLevel = typeof values.level === 'string' ? values.level : 'beginner'; + const rawTools = typeof values.tools === 'string' ? values.tools : null; + const rawCtx = typeof values['company-context'] === 'string' ? values['company-context'] : null; + + if (!role) fatal('--role is required'); + if (!goal) fatal('--goal is required'); + + if (!['beginner', 'intermediate', 'advanced'].includes(rawLevel)) { + fatal(`--level must be beginner, intermediate, or advanced (got: "${rawLevel}")`); + } + + const toolList = rawTools + ? rawTools.split(',').map((t) => t.trim()).filter(Boolean) + : ['Claude']; + + const input = { + role, + tools: toolList, + goal, + level: rawLevel as 'beginner' | 'intermediate' | 'advanced', + ...(rawCtx ? { companyContext: rawCtx } : {}), + }; + + let path; + try { + path = generateLearningPath(input); + } catch (err) { + fatal(err instanceof Error ? err.message : String(err)); + } + + const outputMode = values.output ?? 'pretty'; + + if (outputMode === 'json') { + process.stdout.write(JSON.stringify(path) + '\n'); + return; + } + + // Pretty-print + const hr = '─'.repeat(60); + process.stdout.write(`\n LearnKit AI — 30-day learning path\n`); + process.stdout.write(` Role: ${path.input.role} · Level: ${path.input.level} · ${path.totalMinutes} min total\n`); + process.stdout.write(` Goal: ${path.input.goal}\n`); + if (path.input.companyContext) { + process.stdout.write(` Context: ${path.input.companyContext}\n`); + } + process.stdout.write(`\n`); + + for (const week of path.weeks) { + process.stdout.write(` ${hr}\n`); + process.stdout.write(` Week ${week.index} — ${week.title}\n`); + process.stdout.write(` ${hr}\n`); + for (const lesson of week.lessons) { + const kindTag = lesson.kind === 'project' ? '[PROJECT]' : lesson.kind === 'practicum' ? '[PRACTICUM]' : '[LESSON]'; + process.stdout.write(`\n Day ${lesson.day} ${kindTag} ${lesson.minutes}m (${lesson.tool})\n`); + process.stdout.write(` ${lesson.title}\n`); + process.stdout.write(` ${lesson.summary}\n`); + } + process.stdout.write(`\n`); + } +} + +function cmdRoles(): void { + const roles = getSupportedRoles(); + process.stdout.write('\nSupported roles:\n'); + for (const r of roles) { + process.stdout.write(` • ${r}\n`); + } + process.stdout.write('\n'); +} + +function cmdTools(): void { + const tools = getSupportedTools(); + process.stdout.write('\nSupported tools:\n'); + for (const t of tools) { + process.stdout.write(` • ${t}\n`); + } + process.stdout.write('\n'); +} + +function main(): void { + const [, , cmd, ...rest] = process.argv; + + if (!cmd || cmd === '--help' || cmd === '-h') { + process.stdout.write(HELP); + process.exit(0); + } + + switch (cmd) { + case 'generate': + cmdGenerate(rest); + break; + case 'roles': + cmdRoles(); + break; + case 'tools': + cmdTools(); + break; + default: + fatal(`unknown command "${cmd}". Run --help for usage.`); + } +} + +main(); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000..e94dc5e --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts new file mode 100644 index 0000000..66fb8d1 --- /dev/null +++ b/packages/cli/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + dts: true, + clean: true, + sourcemap: false, + outDir: 'dist', + external: ['@learnkit-ai/core', '@learnkit-ai/schemas'], + // Inject the Node.js shebang into the ESM entry so `npx` can execute it + banner: { js: '#!/usr/bin/env node' }, +}); diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts new file mode 100644 index 0000000..d3462c3 --- /dev/null +++ b/packages/cli/vitest.config.ts @@ -0,0 +1,11 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + alias: { + '@learnkit-ai/schemas': resolve(__dirname, '../schemas/src/index.ts'), + '@learnkit-ai/core': resolve(__dirname, '../core/src/index.ts'), + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9067d0..9baa718 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,25 @@ importers: specifier: ^5.7.3 version: 5.9.3 + packages/cli: + dependencies: + '@learnkit-ai/core': + specifier: workspace:* + version: link:../core + '@learnkit-ai/schemas': + specifier: workspace:* + version: link:../schemas + devDependencies: + tsup: + specifier: ^8.0.0 + version: 8.5.1(postcss@8.5.14)(typescript@5.9.3) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.19)(happy-dom@20.9.0)(jsdom@29.1.1) + packages/core: dependencies: '@learnkit-ai/schemas': From c201f891cc29c91a673f48443e927b9496bf2764 Mon Sep 17 00:00:00 2001 From: raj-khan Date: Sat, 23 May 2026 00:07:56 +0800 Subject: [PATCH 5/7] Fix DTS build: guard sizeMatch[1] for noUncheckedIndexedAccess; reorder export conditions --- packages/cli/package.json | 5 ++--- packages/core/package.json | 5 ++--- packages/core/src/generate.ts | 3 ++- packages/react/package.json | 5 ++--- packages/schemas/package.json | 5 ++--- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 42ff231..d5620d7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -17,11 +17,10 @@ "files": ["dist"], "exports": { ".": { + "types": "./dist/index.d.ts", "source": "./src/index.ts", "import": "./dist/index.js", - "require": "./dist/index.cjs", - "types": "./dist/index.d.ts", - "default": "./src/index.ts" + "require": "./dist/index.cjs" } }, "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index e02f126..11b9bd0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -17,11 +17,10 @@ "types": "./dist/index.d.ts", "exports": { ".": { + "types": "./dist/index.d.ts", "source": "./src/index.ts", "import": "./dist/index.js", - "require": "./dist/index.cjs", - "types": "./dist/index.d.ts", - "default": "./src/index.ts" + "require": "./dist/index.cjs" } }, "files": ["dist", "src"], diff --git a/packages/core/src/generate.ts b/packages/core/src/generate.ts index 907ba82..c297c47 100644 --- a/packages/core/src/generate.ts +++ b/packages/core/src/generate.ts @@ -788,7 +788,8 @@ function parseCompanyContext(ctx: string): ParsedContext { // Team size: first number followed by "person", "people", "member", "engineer", "developer" const sizeMatch = lower.match(/(\d+)[- ](?:person|people|member|engineer|developer)/); - const size = sizeMatch ? parseInt(sizeMatch[1], 10) : null; + const sizeStr = sizeMatch?.[1]; + const size = sizeStr != null ? parseInt(sizeStr, 10) : null; const teamHint = size !== null ? size <= 5 ? 'a small team' diff --git a/packages/react/package.json b/packages/react/package.json index fdca3df..6282ba8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -24,11 +24,10 @@ "types": "./dist/index.d.ts", "exports": { ".": { + "types": "./dist/index.d.ts", "source": "./src/index.ts", "import": "./dist/index.js", - "require": "./dist/index.cjs", - "types": "./dist/index.d.ts", - "default": "./src/index.ts" + "require": "./dist/index.cjs" } }, "files": [ diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 570791d..564fca6 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -17,11 +17,10 @@ "types": "./dist/index.d.ts", "exports": { ".": { + "types": "./dist/index.d.ts", "source": "./src/index.ts", "import": "./dist/index.js", - "require": "./dist/index.cjs", - "types": "./dist/index.d.ts", - "default": "./src/index.ts" + "require": "./dist/index.cjs" } }, "files": ["dist", "src"], From 7986c84dace8cb1dae123792fd8b3a7f22d9c190 Mon Sep 17 00:00:00 2001 From: raj-khan Date: Sat, 23 May 2026 00:11:50 +0800 Subject: [PATCH 6/7] Fix CI typecheck: add tsconfig paths for workspace deps; restore default export condition; add cli to CI --- .github/workflows/ci.yml | 3 +++ packages/cli/package.json | 3 ++- packages/cli/tsconfig.json | 4 ++++ packages/core/package.json | 3 ++- packages/core/tsconfig.json | 3 +++ packages/react/package.json | 3 ++- packages/react/tsconfig.json | 4 ++++ packages/schemas/package.json | 3 ++- 8 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 850a1db..aedae2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,7 @@ jobs: pnpm --filter @learnkit-ai/schemas typecheck pnpm --filter @learnkit-ai/core typecheck pnpm --filter @learnkit-ai/react typecheck + pnpm --filter @learnkit-ai/cli typecheck - name: Typecheck apps/web run: pnpm --filter @learnkit-ai/web typecheck @@ -48,6 +49,7 @@ jobs: pnpm --filter @learnkit-ai/schemas test pnpm --filter @learnkit-ai/core test pnpm --filter @learnkit-ai/react test + pnpm --filter @learnkit-ai/cli test build: name: Build packages + apps/web @@ -69,6 +71,7 @@ jobs: pnpm --filter @learnkit-ai/schemas build pnpm --filter @learnkit-ai/core build pnpm --filter @learnkit-ai/react build + pnpm --filter @learnkit-ai/cli build - name: Build apps/web run: pnpm --filter @learnkit-ai/web build diff --git a/packages/cli/package.json b/packages/cli/package.json index d5620d7..cff9328 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -20,7 +20,8 @@ "types": "./dist/index.d.ts", "source": "./src/index.ts", "import": "./dist/index.js", - "require": "./dist/index.cjs" + "require": "./dist/index.cjs", + "default": "./src/index.ts" } }, "scripts": { diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index e94dc5e..6551d2e 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -15,5 +15,9 @@ "noUncheckedIndexedAccess": true, "noImplicitOverride": true }, + "paths": { + "@learnkit-ai/schemas": ["../schemas/src/index.ts"], + "@learnkit-ai/core": ["../core/src/index.ts"] + }, "include": ["src/**/*.ts"] } diff --git a/packages/core/package.json b/packages/core/package.json index 11b9bd0..0e3ad20 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -20,7 +20,8 @@ "types": "./dist/index.d.ts", "source": "./src/index.ts", "import": "./dist/index.js", - "require": "./dist/index.cjs" + "require": "./dist/index.cjs", + "default": "./src/index.ts" } }, "files": ["dist", "src"], diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index e194197..d7c8a1c 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -15,5 +15,8 @@ "noUncheckedIndexedAccess": true, "noImplicitOverride": true }, + "paths": { + "@learnkit-ai/schemas": ["../schemas/src/index.ts"] + }, "include": ["src/**/*.ts"] } diff --git a/packages/react/package.json b/packages/react/package.json index 6282ba8..f04feba 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -27,7 +27,8 @@ "types": "./dist/index.d.ts", "source": "./src/index.ts", "import": "./dist/index.js", - "require": "./dist/index.cjs" + "require": "./dist/index.cjs", + "default": "./src/index.ts" } }, "files": [ diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 337e65b..77927dd 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -16,5 +16,9 @@ "noUncheckedIndexedAccess": true, "noImplicitOverride": true }, + "paths": { + "@learnkit-ai/schemas": ["../schemas/src/index.ts"], + "@learnkit-ai/core": ["../core/src/index.ts"] + }, "include": ["src/**/*.ts", "src/**/*.tsx"] } diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 564fca6..d57b175 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -20,7 +20,8 @@ "types": "./dist/index.d.ts", "source": "./src/index.ts", "import": "./dist/index.js", - "require": "./dist/index.cjs" + "require": "./dist/index.cjs", + "default": "./src/index.ts" } }, "files": ["dist", "src"], From da43e63a5f94d0f4be143b15773c60cb3b52fca5 Mon Sep 17 00:00:00 2001 From: raj-khan Date: Sat, 23 May 2026 00:14:47 +0800 Subject: [PATCH 7/7] Fix tsconfig paths placement and add @types/node to CLI --- packages/cli/package.json | 5 ++++- packages/cli/tsconfig.json | 11 ++++++----- packages/core/tsconfig.json | 8 ++++---- packages/react/tsconfig.json | 10 +++++----- pnpm-lock.yaml | 3 +++ 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index cff9328..6446c5a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -14,7 +14,9 @@ "bin": { "learnkit-ai": "./dist/index.js" }, - "files": ["dist"], + "files": [ + "dist" + ], "exports": { ".": { "types": "./dist/index.d.ts", @@ -35,6 +37,7 @@ "@learnkit-ai/schemas": "workspace:*" }, "devDependencies": { + "@types/node": "^22.19.19", "tsup": "^8.0.0", "typescript": "^5.7.3", "vitest": "^2.1.9" diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 6551d2e..f36e70a 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -4,6 +4,7 @@ "module": "ESNext", "moduleResolution": "Bundler", "lib": ["ES2022"], + "types": ["node"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -13,11 +14,11 @@ "declaration": true, "forceConsistentCasingInFileNames": true, "noUncheckedIndexedAccess": true, - "noImplicitOverride": true - }, - "paths": { - "@learnkit-ai/schemas": ["../schemas/src/index.ts"], - "@learnkit-ai/core": ["../core/src/index.ts"] + "noImplicitOverride": true, + "paths": { + "@learnkit-ai/schemas": ["../schemas/src/index.ts"], + "@learnkit-ai/core": ["../core/src/index.ts"] + } }, "include": ["src/**/*.ts"] } diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index d7c8a1c..2049e68 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -13,10 +13,10 @@ "declaration": true, "forceConsistentCasingInFileNames": true, "noUncheckedIndexedAccess": true, - "noImplicitOverride": true - }, - "paths": { - "@learnkit-ai/schemas": ["../schemas/src/index.ts"] + "noImplicitOverride": true, + "paths": { + "@learnkit-ai/schemas": ["../schemas/src/index.ts"] + } }, "include": ["src/**/*.ts"] } diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 77927dd..89a5454 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -14,11 +14,11 @@ "declaration": true, "forceConsistentCasingInFileNames": true, "noUncheckedIndexedAccess": true, - "noImplicitOverride": true - }, - "paths": { - "@learnkit-ai/schemas": ["../schemas/src/index.ts"], - "@learnkit-ai/core": ["../core/src/index.ts"] + "noImplicitOverride": true, + "paths": { + "@learnkit-ai/schemas": ["../schemas/src/index.ts"], + "@learnkit-ai/core": ["../core/src/index.ts"] + } }, "include": ["src/**/*.ts", "src/**/*.tsx"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9baa718..828c442 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: specifier: workspace:* version: link:../schemas devDependencies: + '@types/node': + specifier: ^22.19.19 + version: 22.19.19 tsup: specifier: ^8.0.0 version: 8.5.1(postcss@8.5.14)(typescript@5.9.3)