learn-anything-cli 0.2.1 → 0.4.0-beta.1

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.
Files changed (37) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/index.js +1 -1
  3. package/dist/core/command-generation/registry.js +1 -1
  4. package/dist/core/config.js +148 -20
  5. package/dist/core/init.d.ts +2 -0
  6. package/dist/core/init.js +36 -6
  7. package/dist/core/learn-protocol/index.d.ts +8 -0
  8. package/dist/core/learn-protocol/index.js +5 -0
  9. package/dist/core/learn-protocol/migrate.d.ts +52 -0
  10. package/dist/core/learn-protocol/migrate.js +259 -0
  11. package/dist/core/learn-protocol/parser.d.ts +33 -0
  12. package/dist/core/learn-protocol/parser.js +150 -0
  13. package/dist/core/learn-protocol/schema.d.ts +38 -0
  14. package/dist/core/learn-protocol/schema.js +43 -0
  15. package/dist/core/learn-protocol/slug.d.ts +13 -0
  16. package/dist/core/learn-protocol/slug.js +28 -0
  17. package/dist/core/learn-protocol/types.d.ts +63 -0
  18. package/dist/core/learn-protocol/types.js +2 -0
  19. package/dist/core/shared/skill-generation.js +25 -5
  20. package/dist/core/templates/skill-templates.d.ts +5 -5
  21. package/dist/core/templates/skill-templates.js +5 -5
  22. package/dist/core/templates/workflows/learn-explain.js +65 -142
  23. package/dist/core/templates/workflows/learn-practice.js +88 -282
  24. package/dist/core/templates/workflows/learn-review.js +35 -93
  25. package/dist/core/templates/workflows/learn-status.js +26 -69
  26. package/dist/core/templates/workflows/learn-topic.js +73 -82
  27. package/dist/i18n/index.js +1 -4
  28. package/dist/i18n/locales/en.js +1 -0
  29. package/dist/i18n/locales/zh-CN.js +1 -0
  30. package/dist/i18n/types.d.ts +1 -0
  31. package/dist/scripts/render.d.mts +13 -0
  32. package/dist/scripts/render.mjs +112 -0
  33. package/dist/scripts/status.d.mts +31 -0
  34. package/dist/scripts/status.mjs +418 -0
  35. package/dist/scripts/utils.d.mts +43 -0
  36. package/dist/scripts/utils.mjs +124 -0
  37. package/package.json +24 -2
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Learn Anything
2
2
 
3
+ [English](./README.md) | [中文](./README.zh-CN.md)
4
+
3
5
  AI-powered recursive learning system — turns your AI coding assistant into an interactive tutor using the Socratic method and TDD-style exercises.
4
6
 
5
7
  Generate skill and command files for **30+ AI tools** (Claude Code, Cursor, Gemini CLI, Codex, Copilot, Windsurf, etc.), then use slash commands to systematically master any technical topic.
package/dist/cli/index.js CHANGED
@@ -43,7 +43,7 @@ program
43
43
  throw error;
44
44
  }
45
45
  else {
46
- throw new Error(mc.cannotAccess(targetPath, error.message));
46
+ throw new Error(mc.cannotAccess(targetPath, error.message), { cause: error });
47
47
  }
48
48
  }
49
49
  const { InitCommand } = await import('../core/init.js');
@@ -1,4 +1,4 @@
1
- import { claudeAdapter, cursorAdapter, codexAdapter, geminiAdapter, } from './adapters/index.js';
1
+ import { claudeAdapter, cursorAdapter, codexAdapter, geminiAdapter } from './adapters/index.js';
2
2
  export class CommandAdapterRegistry {
3
3
  static adapters = new Map();
4
4
  static {
@@ -1,33 +1,161 @@
1
1
  export const LEARN_DIR = '.learn';
2
2
  export const AI_TOOLS = [
3
- { name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq' },
4
- { name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agent' },
5
- { name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie', skillsDir: '.augment' },
6
- { name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' },
7
- { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
3
+ {
4
+ name: 'Amazon Q Developer',
5
+ value: 'amazon-q',
6
+ available: true,
7
+ successLabel: 'Amazon Q Developer',
8
+ skillsDir: '.amazonq',
9
+ },
10
+ {
11
+ name: 'Antigravity',
12
+ value: 'antigravity',
13
+ available: true,
14
+ successLabel: 'Antigravity',
15
+ skillsDir: '.agent',
16
+ },
17
+ {
18
+ name: 'Auggie (Augment CLI)',
19
+ value: 'auggie',
20
+ available: true,
21
+ successLabel: 'Auggie',
22
+ skillsDir: '.augment',
23
+ },
24
+ {
25
+ name: 'Bob Shell',
26
+ value: 'bob',
27
+ available: true,
28
+ successLabel: 'Bob Shell',
29
+ skillsDir: '.bob',
30
+ },
31
+ {
32
+ name: 'Claude Code',
33
+ value: 'claude',
34
+ available: true,
35
+ successLabel: 'Claude Code',
36
+ skillsDir: '.claude',
37
+ },
8
38
  { name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' },
9
39
  { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.codex' },
10
- { name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' },
11
- { name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' },
12
- { name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue' },
13
- { name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict', skillsDir: '.cospec' },
40
+ {
41
+ name: 'ForgeCode',
42
+ value: 'forgecode',
43
+ available: true,
44
+ successLabel: 'ForgeCode',
45
+ skillsDir: '.forge',
46
+ },
47
+ {
48
+ name: 'CodeBuddy Code (CLI)',
49
+ value: 'codebuddy',
50
+ available: true,
51
+ successLabel: 'CodeBuddy Code',
52
+ skillsDir: '.codebuddy',
53
+ },
54
+ {
55
+ name: 'Continue',
56
+ value: 'continue',
57
+ available: true,
58
+ successLabel: 'Continue (VS Code / JetBrains / Cli)',
59
+ skillsDir: '.continue',
60
+ },
61
+ {
62
+ name: 'CoStrict',
63
+ value: 'costrict',
64
+ available: true,
65
+ successLabel: 'CoStrict',
66
+ skillsDir: '.cospec',
67
+ },
14
68
  { name: 'Crush', value: 'crush', available: true, successLabel: 'Crush', skillsDir: '.crush' },
15
- { name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor' },
16
- { name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid', skillsDir: '.factory' },
17
- { name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' },
18
- { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'] },
69
+ {
70
+ name: 'Cursor',
71
+ value: 'cursor',
72
+ available: true,
73
+ successLabel: 'Cursor',
74
+ skillsDir: '.cursor',
75
+ },
76
+ {
77
+ name: 'Factory Droid',
78
+ value: 'factory',
79
+ available: true,
80
+ successLabel: 'Factory Droid',
81
+ skillsDir: '.factory',
82
+ },
83
+ {
84
+ name: 'Gemini CLI',
85
+ value: 'gemini',
86
+ available: true,
87
+ successLabel: 'Gemini CLI',
88
+ skillsDir: '.gemini',
89
+ },
90
+ {
91
+ name: 'GitHub Copilot',
92
+ value: 'github-copilot',
93
+ available: true,
94
+ successLabel: 'GitHub Copilot',
95
+ skillsDir: '.github',
96
+ detectionPaths: [
97
+ '.github/copilot-instructions.md',
98
+ '.github/instructions',
99
+ '.github/workflows/copilot-setup-steps.yml',
100
+ '.github/prompts',
101
+ '.github/agents',
102
+ '.github/skills',
103
+ '.github/.mcp.json',
104
+ ],
105
+ },
19
106
  { name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' },
20
107
  { name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie' },
21
- { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' },
108
+ {
109
+ name: 'Kilo Code',
110
+ value: 'kilocode',
111
+ available: true,
112
+ successLabel: 'Kilo Code',
113
+ skillsDir: '.kilocode',
114
+ },
22
115
  { name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
23
- { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
116
+ {
117
+ name: 'OpenCode',
118
+ value: 'opencode',
119
+ available: true,
120
+ successLabel: 'OpenCode',
121
+ skillsDir: '.opencode',
122
+ },
24
123
  { name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
25
124
  { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },
26
- { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' },
27
- { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
28
- { name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' },
125
+ {
126
+ name: 'Lingma',
127
+ value: 'lingma',
128
+ available: true,
129
+ successLabel: 'Lingma',
130
+ skillsDir: '.lingma',
131
+ },
132
+ {
133
+ name: 'Qwen Code',
134
+ value: 'qwen',
135
+ available: true,
136
+ successLabel: 'Qwen Code',
137
+ skillsDir: '.qwen',
138
+ },
139
+ {
140
+ name: 'RooCode',
141
+ value: 'roocode',
142
+ available: true,
143
+ successLabel: 'RooCode',
144
+ skillsDir: '.roo',
145
+ },
29
146
  { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' },
30
- { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf', skillsDir: '.windsurf' },
31
- { name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' },
147
+ {
148
+ name: 'Windsurf',
149
+ value: 'windsurf',
150
+ available: true,
151
+ successLabel: 'Windsurf',
152
+ skillsDir: '.windsurf',
153
+ },
154
+ {
155
+ name: 'AGENTS.md (works with Amp, VS Code, …)',
156
+ value: 'agents',
157
+ available: false,
158
+ successLabel: 'your AGENTS.md-compatible assistant',
159
+ },
32
160
  ];
33
161
  //# sourceMappingURL=config.js.map
@@ -16,6 +16,8 @@ export declare class InitCommand {
16
16
  private hasToolDir;
17
17
  private interactiveSelect;
18
18
  private generateSkillsForTool;
19
+ /** Read a compiled script from dist/scripts/ (bundled alongside this module). */
20
+ private readCompiledScript;
19
21
  private generateCommandsForTool;
20
22
  }
21
23
  export {};
package/dist/core/init.js CHANGED
@@ -2,11 +2,12 @@ import path from 'path';
2
2
  import chalk from 'chalk';
3
3
  import * as fs from 'fs';
4
4
  import { createRequire } from 'module';
5
+ import { fileURLToPath } from 'url';
5
6
  import { FileSystemUtils } from '../utils/file-system.js';
6
7
  import { AI_TOOLS, LEARN_DIR } from './config.js';
7
8
  import { isInteractive } from '../utils/interactive.js';
8
- import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
9
- import { getSkillTemplates, getCommandContents, generateSkillContent, } from './shared/index.js';
9
+ import { generateCommands, CommandAdapterRegistry } from './command-generation/index.js';
10
+ import { getSkillTemplates, getCommandContents, generateSkillContent } from './shared/index.js';
10
11
  import { getMessages } from '../i18n/index.js';
11
12
  const require = createRequire(import.meta.url);
12
13
  const { version: VERSION } = require('../../package.json');
@@ -28,7 +29,14 @@ export class InitCommand {
28
29
  await FileSystemUtils.ensureDir(resolvedPath);
29
30
  // Create .learn/ directory in the target project
30
31
  const learnDir = path.join(resolvedPath, LEARN_DIR);
31
- await FileSystemUtils.ensureDir(path.join(learnDir, 'topics'));
32
+ const topicsDir = path.join(learnDir, 'topics');
33
+ await FileSystemUtils.ensureDir(topicsDir);
34
+ // Run v0→v1 migration for any existing learning data
35
+ const { migrateAll } = await import('./learn-protocol/index.js');
36
+ const report = await migrateAll(topicsDir);
37
+ if (report.migratedCount > 0) {
38
+ console.log(chalk.green(m.init.migrationComplete(report.migratedCount)));
39
+ }
32
40
  console.log(chalk.bold(m.init.header));
33
41
  // Detect available tools
34
42
  const availableTools = await this.detectTools(resolvedPath);
@@ -53,7 +61,10 @@ export class InitCommand {
53
61
  }
54
62
  if (selectedTools.length === 0) {
55
63
  console.log(chalk.yellow(m.init.noToolsSelected));
56
- console.log(chalk.dim(m.init.availableTools(availableTools.filter((t) => t.available).map((t) => t.value).join(', '))));
64
+ console.log(chalk.dim(m.init.availableTools(availableTools
65
+ .filter((t) => t.available)
66
+ .map((t) => t.value)
67
+ .join(', '))));
57
68
  return;
58
69
  }
59
70
  // Generate skill files for each tool
@@ -77,7 +88,7 @@ export class InitCommand {
77
88
  console.log(cmd(chalk.cyan('/learn:status [topic-name]'), chalk.dim(' — Visualize learning state as knowledge map heatmap')));
78
89
  console.log('');
79
90
  }
80
- async detectTools(resolvedPath) {
91
+ async detectTools(_resolvedPath) {
81
92
  return AI_TOOLS;
82
93
  }
83
94
  hasToolDir(resolvedPath, tool) {
@@ -93,7 +104,7 @@ export class InitCommand {
93
104
  }
94
105
  async interactiveSelect(tools) {
95
106
  const availableTools = tools.filter((t) => t.available && t.skillsDir);
96
- const { search, checkbox } = await import('@inquirer/prompts');
107
+ const { checkbox } = await import('@inquirer/prompts');
97
108
  // Auto-detect existing tool dirs and pre-select them
98
109
  const detected = availableTools.filter((t) => this.hasToolDir(process.cwd(), t));
99
110
  const detectedValues = new Set(detected.map((t) => t.value));
@@ -116,8 +127,27 @@ export class InitCommand {
116
127
  const skillFile = path.join(skillDir, 'SKILL.md');
117
128
  const content = generateSkillContent(entry.template, VERSION);
118
129
  await FileSystemUtils.writeFile(skillFile, content);
130
+ const scriptsDir = path.join(skillDir, 'scripts');
131
+ // topic / explain / practice → utils.mjs + render.mjs
132
+ if (entry.dirName === 'learn-anything-topic' ||
133
+ entry.dirName === 'learn-anything-explain' ||
134
+ entry.dirName === 'learn-anything-practice') {
135
+ await FileSystemUtils.writeFile(path.join(scriptsDir, 'utils.mjs'), this.readCompiledScript('utils.mjs'));
136
+ await FileSystemUtils.writeFile(path.join(scriptsDir, 'render.mjs'), this.readCompiledScript('render.mjs'));
137
+ }
138
+ // status → utils.mjs + status.mjs
139
+ if (entry.dirName === 'learn-anything-status') {
140
+ await FileSystemUtils.writeFile(path.join(scriptsDir, 'utils.mjs'), this.readCompiledScript('utils.mjs'));
141
+ await FileSystemUtils.writeFile(path.join(scriptsDir, 'status.mjs'), this.readCompiledScript('status.mjs'));
142
+ }
143
+ // review → no scripts needed
119
144
  }
120
145
  }
146
+ /** Read a compiled script from dist/scripts/ (bundled alongside this module). */
147
+ readCompiledScript(filename) {
148
+ const scriptPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'scripts', filename);
149
+ return fs.readFileSync(scriptPath, 'utf-8');
150
+ }
121
151
  async generateCommandsForTool(resolvedPath, tool) {
122
152
  const adapter = CommandAdapterRegistry.get(tool.value);
123
153
  if (!adapter)
@@ -0,0 +1,8 @@
1
+ export type { ConceptStatus, Concept, Domain, StateV1, Detail, V0Concept, V0State, ParsedConcept, ParsedDomain, ParsedKnowledgeMap, } from './types.js';
2
+ export { stateV1Schema, validateStateV1 } from './schema.js';
3
+ export type { StateV1Schema, ValidationResult } from './schema.js';
4
+ export { generateSlug } from './slug.js';
5
+ export { parseKnowledgeMap } from './parser.js';
6
+ export { isV0State, migrateV0ToV1, migrateAll } from './migrate.js';
7
+ export type { MigrationResult, MigrationReport } from './migrate.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ export { stateV1Schema, validateStateV1 } from './schema.js';
2
+ export { generateSlug } from './slug.js';
3
+ export { parseKnowledgeMap } from './parser.js';
4
+ export { isV0State, migrateV0ToV1, migrateAll } from './migrate.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * v0 -> v1 migration logic.
3
+ *
4
+ * Detects v0 state.yaml format and merges it with knowledge-map.md
5
+ * to produce a state.json v1 file. Migration is idempotent and
6
+ * creates .bak backups before writing.
7
+ *
8
+ * Migration chain:
9
+ * state.yaml (v0) + knowledge-map.md (v0)
10
+ * -> migrateV0ToV1()
11
+ * -> state.json (v1) + state.yaml.v0.bak + knowledge-map.md.v0.bak
12
+ */
13
+ import type { V0State } from './types.js';
14
+ /**
15
+ * Check if a parsed YAML object matches the v0 state format.
16
+ *
17
+ * v0 is identified by having `topic` and `concepts` fields but
18
+ * NO `version` field. Returns `true` when the data matches v0.
19
+ */
20
+ export declare function isV0State(data: unknown): data is V0State;
21
+ export interface MigrationResult {
22
+ migrated: boolean;
23
+ topic: string;
24
+ reason?: 'already_migrated' | 'already_v1' | 'not_v0' | 'no_state_yaml' | 'error';
25
+ error?: string;
26
+ }
27
+ export interface MigrationReport {
28
+ migratedCount: number;
29
+ skippedCount: number;
30
+ results: MigrationResult[];
31
+ }
32
+ /**
33
+ * Migrate a single topic directory from v0 to v1.
34
+ *
35
+ * Reads `state.yaml` and `knowledge-map.md`, merges them into `state.json`,
36
+ * and creates `.bak` backup files. Idempotent — skips if already migrated.
37
+ *
38
+ * @param topicDir — Absolute path to the topic directory (e.g. `.learn/topics/javascript`)
39
+ * @returns A MigrationResult describing what happened
40
+ */
41
+ export declare function migrateV0ToV1(topicDir: string): Promise<MigrationResult>;
42
+ /**
43
+ * Migrate ALL topics under a base directory from v0 to v1.
44
+ *
45
+ * Scans `.learn/topics/` for topic subdirectories and runs
46
+ * migrateV0ToV1 on each one. Returns a summary report.
47
+ *
48
+ * @param baseDir — Path to the topics directory (e.g. `.learn/topics`)
49
+ * @returns MigrationReport with counts and per-topic results
50
+ */
51
+ export declare function migrateAll(baseDir: string): Promise<MigrationReport>;
52
+ //# sourceMappingURL=migrate.d.ts.map
@@ -0,0 +1,259 @@
1
+ /**
2
+ * v0 -> v1 migration logic.
3
+ *
4
+ * Detects v0 state.yaml format and merges it with knowledge-map.md
5
+ * to produce a state.json v1 file. Migration is idempotent and
6
+ * creates .bak backups before writing.
7
+ *
8
+ * Migration chain:
9
+ * state.yaml (v0) + knowledge-map.md (v0)
10
+ * -> migrateV0ToV1()
11
+ * -> state.json (v1) + state.yaml.v0.bak + knowledge-map.md.v0.bak
12
+ */
13
+ import { promises as fs } from 'fs';
14
+ import path from 'path';
15
+ import { parse as parseYaml } from 'yaml';
16
+ import { parseKnowledgeMap } from './parser.js';
17
+ import { generateSlug } from './slug.js';
18
+ import { stateV1Schema } from './schema.js';
19
+ import { render } from '../../scripts/render.mjs';
20
+ import { FileSystemUtils } from '../../utils/file-system.js';
21
+ // ---- Public API ---------------------------------------------------------
22
+ /**
23
+ * Check if a parsed YAML object matches the v0 state format.
24
+ *
25
+ * v0 is identified by having `topic` and `concepts` fields but
26
+ * NO `version` field. Returns `true` when the data matches v0.
27
+ */
28
+ export function isV0State(data) {
29
+ if (!data || typeof data !== 'object')
30
+ return false;
31
+ const obj = data;
32
+ // v0: has topic (string) and concepts (array), no version field
33
+ if (typeof obj.topic !== 'string' || !obj.topic)
34
+ return false;
35
+ if (!Array.isArray(obj.concepts))
36
+ return false;
37
+ if (obj.version !== undefined)
38
+ return false;
39
+ return true;
40
+ }
41
+ /**
42
+ * Migrate a single topic directory from v0 to v1.
43
+ *
44
+ * Reads `state.yaml` and `knowledge-map.md`, merges them into `state.json`,
45
+ * and creates `.bak` backup files. Idempotent — skips if already migrated.
46
+ *
47
+ * @param topicDir — Absolute path to the topic directory (e.g. `.learn/topics/javascript`)
48
+ * @returns A MigrationResult describing what happened
49
+ */
50
+ export async function migrateV0ToV1(topicDir) {
51
+ const stateYamlPath = path.join(topicDir, 'state.yaml');
52
+ const knowledgeMapPath = path.join(topicDir, 'knowledge-map.md');
53
+ const stateJsonPath = path.join(topicDir, 'state.json');
54
+ const stateYamlBackup = path.join(topicDir, 'state.yaml.v0.bak');
55
+ const knowledgeMapBackup = path.join(topicDir, 'knowledge-map.md.v0.bak');
56
+ // 1. Skip if state.json already exists (already migrated)
57
+ if (await FileSystemUtils.fileExists(stateJsonPath)) {
58
+ try {
59
+ const existing = JSON.parse(await fs.readFile(stateJsonPath, 'utf-8'));
60
+ return {
61
+ migrated: false,
62
+ topic: existing.topic || path.basename(topicDir),
63
+ reason: 'already_migrated',
64
+ };
65
+ }
66
+ catch {
67
+ return {
68
+ migrated: false,
69
+ topic: path.basename(topicDir),
70
+ reason: 'already_migrated',
71
+ };
72
+ }
73
+ }
74
+ // 2. Check state.yaml exists
75
+ if (!(await FileSystemUtils.fileExists(stateYamlPath))) {
76
+ return {
77
+ migrated: false,
78
+ topic: path.basename(topicDir),
79
+ reason: 'no_state_yaml',
80
+ };
81
+ }
82
+ // 3. Read and parse state.yaml
83
+ let v0Data;
84
+ try {
85
+ const yamlContent = await fs.readFile(stateYamlPath, 'utf-8');
86
+ v0Data = parseYaml(yamlContent);
87
+ }
88
+ catch (err) {
89
+ return {
90
+ migrated: false,
91
+ topic: path.basename(topicDir),
92
+ reason: 'error',
93
+ error: `Failed to parse state.yaml: ${err.message}`,
94
+ };
95
+ }
96
+ // 4. Check v0 format (has topic + concepts, no version field)
97
+ if (!isV0State(v0Data)) {
98
+ return {
99
+ migrated: false,
100
+ topic: path.basename(topicDir),
101
+ reason: 'not_v0',
102
+ };
103
+ }
104
+ const v0State = v0Data;
105
+ // 7. Read and parse knowledge-map.md
106
+ let parsedMap;
107
+ if (await FileSystemUtils.fileExists(knowledgeMapPath)) {
108
+ try {
109
+ const kmContent = await fs.readFile(knowledgeMapPath, 'utf-8');
110
+ parsedMap = parseKnowledgeMap(kmContent);
111
+ }
112
+ catch (err) {
113
+ return {
114
+ migrated: false,
115
+ topic: v0State.topic,
116
+ reason: 'error',
117
+ error: `Failed to parse knowledge-map.md: ${err.message}`,
118
+ };
119
+ }
120
+ }
121
+ else {
122
+ // Without knowledge-map.md we can't build the hierarchy; skip
123
+ return {
124
+ migrated: false,
125
+ topic: v0State.topic,
126
+ reason: 'error',
127
+ error: 'knowledge-map.md not found',
128
+ };
129
+ }
130
+ // 8. Merge: build a lookup from v0 path -> V0Concept
131
+ const conceptLookup = new Map();
132
+ for (const c of v0State.concepts) {
133
+ conceptLookup.set(c.path, c);
134
+ }
135
+ // 9. Build StateV1 from parsed hierarchy + v0 state data
136
+ const domains = parsedMap.domains.map((pd) => ({
137
+ name: pd.name,
138
+ slug: generateSlug(pd.name),
139
+ concepts: pd.concepts.map((pc) => {
140
+ const v0Path = `${pd.name}/${pc.name}`;
141
+ const v0Concept = conceptLookup.get(v0Path);
142
+ return {
143
+ name: pc.name,
144
+ slug: generateSlug(pc.name),
145
+ status: mapStatus(v0Concept?.status),
146
+ confidence: v0Concept?.confidence ?? 0,
147
+ practice_count: v0Concept?.practice_count ?? 0,
148
+ explain_count: v0Concept?.explain_count ?? 0,
149
+ last_explained: v0Concept?.last_session ?? null,
150
+ last_practiced: v0Concept?.last_practiced ?? null,
151
+ details: pc.children,
152
+ };
153
+ }),
154
+ }));
155
+ const stateV1 = {
156
+ version: 1,
157
+ topic: v0State.topic,
158
+ slug: generateSlug(v0State.topic),
159
+ created: v0State.created,
160
+ domains,
161
+ };
162
+ // 10. Validate the generated state against the schema
163
+ const validation = stateV1Schema.safeParse(stateV1);
164
+ if (!validation.success) {
165
+ return {
166
+ migrated: false,
167
+ topic: v0State.topic,
168
+ reason: 'error',
169
+ error: `Generated state.json failed validation: ${validation.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`,
170
+ };
171
+ }
172
+ // 11. Write state.json
173
+ try {
174
+ await FileSystemUtils.writeFile(stateJsonPath, JSON.stringify(stateV1, null, 2) + '\n');
175
+ }
176
+ catch (err) {
177
+ return {
178
+ migrated: false,
179
+ topic: v0State.topic,
180
+ reason: 'error',
181
+ error: `Failed to write state.json: ${err.message}`,
182
+ };
183
+ }
184
+ // 12. Create backup files, then remove originals
185
+ // After migration, state.json is the single source of truth,
186
+ // so the v0 files should not remain alongside it.
187
+ try {
188
+ await fs.copyFile(stateYamlPath, stateYamlBackup);
189
+ await fs.copyFile(knowledgeMapPath, knowledgeMapBackup);
190
+ await fs.rm(stateYamlPath, { force: true });
191
+ await fs.rm(knowledgeMapPath, { force: true });
192
+ }
193
+ catch (err) {
194
+ // Backup/cleanup failure is non-fatal — state.json was already written
195
+ console.error(`Warning: Failed to create backup files or clean up originals in ${topicDir}: ${err.message}`);
196
+ }
197
+ // 13. Regenerate knowledge-map.md from state.json (v1 format)
198
+ try {
199
+ const rendered = render(stateV1);
200
+ await FileSystemUtils.writeFile(knowledgeMapPath, rendered);
201
+ }
202
+ catch (err) {
203
+ // Render failure is non-fatal — migration itself succeeded
204
+ console.error(`Warning: Failed to regenerate knowledge-map.md in ${topicDir}: ${err.message}`);
205
+ }
206
+ return {
207
+ migrated: true,
208
+ topic: v0State.topic,
209
+ };
210
+ }
211
+ /**
212
+ * Migrate ALL topics under a base directory from v0 to v1.
213
+ *
214
+ * Scans `.learn/topics/` for topic subdirectories and runs
215
+ * migrateV0ToV1 on each one. Returns a summary report.
216
+ *
217
+ * @param baseDir — Path to the topics directory (e.g. `.learn/topics`)
218
+ * @returns MigrationReport with counts and per-topic results
219
+ */
220
+ export async function migrateAll(baseDir) {
221
+ const results = [];
222
+ let entryNames;
223
+ try {
224
+ entryNames = await fs.readdir(baseDir);
225
+ }
226
+ catch {
227
+ // Directory doesn't exist — nothing to migrate
228
+ return { migratedCount: 0, skippedCount: 0, results: [] };
229
+ }
230
+ // Check each entry to see if it's a directory
231
+ const topicDirs = [];
232
+ for (const name of entryNames) {
233
+ const fullPath = path.join(baseDir, name);
234
+ try {
235
+ const stat = await fs.stat(fullPath);
236
+ if (stat.isDirectory())
237
+ topicDirs.push(fullPath);
238
+ }
239
+ catch {
240
+ // Skip entries we can't stat
241
+ }
242
+ }
243
+ for (const dir of topicDirs) {
244
+ const result = await migrateV0ToV1(dir);
245
+ results.push(result);
246
+ }
247
+ const migratedCount = results.filter((r) => r.migrated).length;
248
+ const skippedCount = results.filter((r) => !r.migrated).length;
249
+ return { migratedCount, skippedCount, results };
250
+ }
251
+ // ---- Helpers -----------------------------------------------------------
252
+ /** Map v0 status string to v1 ConceptStatus; default to 'unexplored'. */
253
+ function mapStatus(status) {
254
+ if (status === 'in_progress' || status === 'needs_practice' || status === 'mastered') {
255
+ return status;
256
+ }
257
+ return 'unexplored';
258
+ }
259
+ //# sourceMappingURL=migrate.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Markdown parser for v0 knowledge-map.md.
3
+ *
4
+ * Uses unified + remark-parse to extract the hierarchical structure
5
+ * (domains -> concepts -> details) from the v0 knowledge-map format.
6
+ *
7
+ * This is used ONLY during migration (init/update), not at AI runtime.
8
+ *
9
+ * v0 knowledge-map.md format:
10
+ *
11
+ * ```md
12
+ * # Topic Name
13
+ * ## Domain 1
14
+ * - Concept A
15
+ * - Detail A1
16
+ * - Detail A2
17
+ * - Concept B
18
+ * ## Domain 2
19
+ * - Concept C
20
+ * ```
21
+ */
22
+ import type { ParsedKnowledgeMap } from './types.js';
23
+ /**
24
+ * Parse a v0 knowledge-map.md file content into a structured representation.
25
+ *
26
+ * The parser walks the mdast tree:
27
+ * - `# Title` (h1) -> topic name
28
+ * - `## DomainName` (h2) -> start a new domain
29
+ * - `- Concept` (top-level list item) -> add concept to current domain
30
+ * - ` - Detail` (nested list item) -> add detail to preceding concept
31
+ */
32
+ export declare function parseKnowledgeMap(markdown: string): ParsedKnowledgeMap;
33
+ //# sourceMappingURL=parser.d.ts.map