@mnemosyne_os/forge 1.2.5 → 1.2.6

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.
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+ // Tests — lib/chronicle-parser.ts
8
+ // Uses Node.js built-in test runner (node:test) — zero extra deps
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ const node_test_1 = require("node:test");
11
+ const strict_1 = __importDefault(require("node:assert/strict"));
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const node_os_1 = __importDefault(require("node:os"));
15
+ const chronicle_parser_js_1 = require("../lib/chronicle-parser.js");
16
+ let tmpDir;
17
+ (0, node_test_1.before)(() => { tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'mnemo-test-')); });
18
+ (0, node_test_1.after)(() => { node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true }); });
19
+ function write(filename, content) {
20
+ node_fs_1.default.writeFileSync(node_path_1.default.join(tmpDir, filename), content, 'utf8');
21
+ }
22
+ (0, node_test_1.describe)('parseChronicle — filename', () => {
23
+ (0, node_test_1.it)('extracts date from CHRONICLE-YYYY-MM-DD prefix', () => {
24
+ const f = 'CHRONICLE-2026-04-05-my-session.md';
25
+ write(f, '');
26
+ strict_1.default.equal((0, chronicle_parser_js_1.parseChronicle)(f, tmpDir).date, '2026-04-05');
27
+ });
28
+ (0, node_test_1.it)('converts slug to title when no frontmatter', () => {
29
+ const f = 'CHRONICLE-2026-04-05-my-cool-session.md';
30
+ write(f, '');
31
+ strict_1.default.equal((0, chronicle_parser_js_1.parseChronicle)(f, tmpDir).title, 'My cool session');
32
+ });
33
+ });
34
+ (0, node_test_1.describe)('parseChronicle — frontmatter', () => {
35
+ (0, node_test_1.it)('reads title from frontmatter', () => {
36
+ const f = 'CHRONICLE-2026-04-05-fm.md';
37
+ write(f, '---\ntitle: "My Real Title"\ntype: decision\ndate: 2026-04-05\n---\n\nContent.');
38
+ const r = (0, chronicle_parser_js_1.parseChronicle)(f, tmpDir);
39
+ strict_1.default.equal(r.title, 'My Real Title');
40
+ strict_1.default.equal(r.type, 'decision');
41
+ });
42
+ (0, node_test_1.it)('reads type from frontmatter', () => {
43
+ const f = 'CHRONICLE-2026-04-05-fm2.md';
44
+ write(f, '---\ntype: reflection\n---\n');
45
+ strict_1.default.equal((0, chronicle_parser_js_1.parseChronicle)(f, tmpDir).type, 'reflection');
46
+ });
47
+ });
48
+ (0, node_test_1.describe)('parseChronicle — markdown headers', () => {
49
+ (0, node_test_1.it)('reads title from h1', () => {
50
+ const f = 'CHRONICLE-2026-04-05-h1.md';
51
+ write(f, '# My H1 Title\n\n**Type**: session\n\nSome content here.');
52
+ strict_1.default.equal((0, chronicle_parser_js_1.parseChronicle)(f, tmpDir).title, 'My H1 Title');
53
+ });
54
+ (0, node_test_1.it)('reads type from **Type**: line', () => {
55
+ const f = 'CHRONICLE-2026-04-05-type.md';
56
+ write(f, '# Title\n\n**Type**: sweep\n\nContent.');
57
+ strict_1.default.equal((0, chronicle_parser_js_1.parseChronicle)(f, tmpDir).type, 'sweep');
58
+ });
59
+ (0, node_test_1.it)('generates excerpt from first meaningful line', () => {
60
+ const f = 'CHRONICLE-2026-04-05-excerpt.md';
61
+ write(f, '# Title\n\n**Type**: session\n\nThis is a meaningful excerpt that is long enough.');
62
+ strict_1.default.ok((0, chronicle_parser_js_1.parseChronicle)(f, tmpDir).excerpt.length > 10);
63
+ });
64
+ });
65
+ (0, node_test_1.describe)('parseChronicle — defaults', () => {
66
+ (0, node_test_1.it)('defaults type to session', () => {
67
+ const f = 'CHRONICLE-2026-04-05-def.md';
68
+ write(f, '# A title\n\nNo type specified.');
69
+ strict_1.default.equal((0, chronicle_parser_js_1.parseChronicle)(f, tmpDir).type, 'session');
70
+ });
71
+ (0, node_test_1.it)('handles missing file gracefully', () => {
72
+ const r = (0, chronicle_parser_js_1.parseChronicle)('CHRONICLE-2026-01-01-missing.md', tmpDir);
73
+ strict_1.default.equal(r.type, 'session');
74
+ strict_1.default.equal(r.date, '2026-01-01');
75
+ });
76
+ });
77
+ (0, node_test_1.describe)('getChronicleType', () => {
78
+ (0, node_test_1.it)('extracts type from **Type**: line', () => {
79
+ const f = 'CHRONICLE-2026-04-05-gct.md';
80
+ write(f, '# T\n\n**Type**: decision\n\nBody.');
81
+ strict_1.default.equal((0, chronicle_parser_js_1.getChronicleType)(f, tmpDir), 'decision');
82
+ });
83
+ (0, node_test_1.it)('extracts type from frontmatter', () => {
84
+ const f = 'CHRONICLE-2026-04-05-gct2.md';
85
+ write(f, '---\ntype: narcissus\n---\n# T\n');
86
+ strict_1.default.equal((0, chronicle_parser_js_1.getChronicleType)(f, tmpDir), 'narcissus');
87
+ });
88
+ (0, node_test_1.it)('defaults to session on missing file', () => {
89
+ strict_1.default.equal((0, chronicle_parser_js_1.getChronicleType)('CHRONICLE-2026-04-05-nope.md', tmpDir), 'session');
90
+ });
91
+ });
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+ // Tests — lib/canvas/renderer.ts
8
+ // Uses Node.js built-in test runner (node:test) — zero extra deps
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ const node_test_1 = require("node:test");
11
+ const strict_1 = __importDefault(require("node:assert/strict"));
12
+ const renderer_js_1 = require("../lib/canvas/renderer.js");
13
+ const BASE = {
14
+ PROJECT_NAME: '', PROJECT_SLUG: '', PROJECT_PASCAL: '',
15
+ WORKSPACE: '', ECOSYSTEM: '', DATE: '', AUTHOR: '', AUTHOR_EMAIL: '', MNEMOFORGE_VERSION: '',
16
+ };
17
+ (0, node_test_1.describe)('toSlug', () => {
18
+ (0, node_test_1.it)('converts spaces to dashes + lowercase', () => {
19
+ strict_1.default.equal((0, renderer_js_1.toSlug)('My Awesome CLI'), 'my-awesome-cli');
20
+ });
21
+ (0, node_test_1.it)('handles camelCase', () => {
22
+ strict_1.default.equal((0, renderer_js_1.toSlug)('MnemoForge'), 'mnemoforge');
23
+ });
24
+ (0, node_test_1.it)('strips special chars + keeps numbers dashed', () => {
25
+ strict_1.default.equal((0, renderer_js_1.toSlug)('Hello! World@2024'), 'hello-world-2024');
26
+ });
27
+ (0, node_test_1.it)('collapses multiple dashes', () => {
28
+ strict_1.default.equal((0, renderer_js_1.toSlug)('foo -- bar'), 'foo-bar');
29
+ });
30
+ });
31
+ (0, node_test_1.describe)('toPascal', () => {
32
+ (0, node_test_1.it)('converts slug to PascalCase', () => {
33
+ strict_1.default.equal((0, renderer_js_1.toPascal)('my-awesome-cli'), 'MyAwesomeCli');
34
+ });
35
+ (0, node_test_1.it)('handles single word', () => {
36
+ strict_1.default.equal((0, renderer_js_1.toPascal)('mnemoforge'), 'Mnemoforge');
37
+ });
38
+ (0, node_test_1.it)('handles words split by spaces — same as slug split on dashes', () => {
39
+ // toPascal splits on '-', not spaces — spaces become separate words
40
+ strict_1.default.equal((0, renderer_js_1.toPascal)('my-project'), 'MyProject');
41
+ });
42
+ });
43
+ (0, node_test_1.describe)('render', () => {
44
+ (0, node_test_1.it)('replaces a variable', () => {
45
+ strict_1.default.equal((0, renderer_js_1.render)('Hello {{PROJECT_NAME}}!', { ...BASE, PROJECT_NAME: 'World' }), 'Hello World!');
46
+ });
47
+ (0, node_test_1.it)('replaces multiple variables', () => {
48
+ strict_1.default.equal((0, renderer_js_1.render)('{{PROJECT_NAME}} / {{WORKSPACE}}', { ...BASE, PROJECT_NAME: 'foo', WORKSPACE: 'bar' }), 'foo / bar');
49
+ });
50
+ (0, node_test_1.it)('replaces same variable multiple times', () => {
51
+ strict_1.default.equal((0, renderer_js_1.render)('{{PROJECT_NAME}} is {{PROJECT_NAME}}', { ...BASE, PROJECT_NAME: 'great' }), 'great is great');
52
+ });
53
+ (0, node_test_1.it)('leaves unknown tags untouched', () => {
54
+ strict_1.default.equal((0, renderer_js_1.render)('Hello {{UNKNOWN}}!', BASE), 'Hello {{UNKNOWN}}!');
55
+ });
56
+ (0, node_test_1.it)('handles empty content', () => {
57
+ strict_1.default.equal((0, renderer_js_1.render)('', BASE), '');
58
+ });
59
+ });
60
+ (0, node_test_1.describe)('buildVars', () => {
61
+ (0, node_test_1.it)('generates expected keys', () => {
62
+ const vars = (0, renderer_js_1.buildVars)('My CLI', 'Mnemosyne-OS');
63
+ strict_1.default.equal(vars.PROJECT_NAME, 'My CLI');
64
+ strict_1.default.equal(vars.WORKSPACE, 'Mnemosyne-OS');
65
+ strict_1.default.ok(vars.PROJECT_SLUG);
66
+ strict_1.default.ok(vars.PROJECT_PASCAL);
67
+ strict_1.default.ok(vars.DATE);
68
+ strict_1.default.ok(vars.MNEMOFORGE_VERSION);
69
+ });
70
+ (0, node_test_1.it)('slug is lowercase with dashes', () => {
71
+ strict_1.default.equal((0, renderer_js_1.buildVars)('Hello World', 'ws').PROJECT_SLUG, 'hello-world');
72
+ });
73
+ (0, node_test_1.it)('pascal is PascalCase', () => {
74
+ strict_1.default.equal((0, renderer_js_1.buildVars)('hello world', 'ws').PROJECT_PASCAL, 'HelloWorld');
75
+ });
76
+ (0, node_test_1.it)('date matches YYYY-MM-DD format', () => {
77
+ strict_1.default.match((0, renderer_js_1.buildVars)('Test', 'ws').DATE, /^\d{4}-\d{2}-\d{2}$/);
78
+ });
79
+ });
@@ -80,15 +80,16 @@ exports.canvasCommand
80
80
  console.log(chalk_1.default.yellow(`\n ⏳ This template is not yet available. Coming soon!\n`));
81
81
  return;
82
82
  }
83
- const { projectName, workspace, runInstall } = await inquirer_1.default.prompt([
83
+ const { projectName, workspace, ecosystem, runInstall } = await inquirer_1.default.prompt([
84
84
  { type: 'input', name: 'projectName', message: chalk_1.default.hex('#A78BFA')('Project name:'), validate: (v) => v.trim().length > 0 || 'Required' },
85
85
  { type: 'input', name: 'workspace', message: chalk_1.default.hex('#A78BFA')('Workspace (Resonance):'), default: defaultWorkspace },
86
+ { type: 'input', name: 'ecosystem', message: chalk_1.default.hex('#A78BFA')('Ecosystem name (white-label):'), default: 'Mnemosyne Neural OS' },
86
87
  { type: 'confirm', name: 'runInstall', message: chalk_1.default.hex('#A78BFA')('Run npm install?'), default: false },
87
88
  ]);
88
89
  const slug = (0, renderer_js_1.toSlug)(projectName);
89
90
  console.log(chalk_1.default.gray(`\n ⟳ Scaffolding ${chalk_1.default.white(projectName)} into ./${slug}...\n`));
90
91
  try {
91
- const result = (0, canvas_js_1.scaffold)({ projectName, workspace, template: templateId }, (file) => {
92
+ const result = (0, canvas_js_1.scaffold)({ projectName, workspace, ecosystem, template: templateId }, (file) => {
92
93
  console.log(chalk_1.default.hex('#4ADE80')(' ✔ ') + chalk_1.default.hex('#6B7280')(file));
93
94
  });
94
95
  console.log(chalk_1.default.hex('#4ADE80').bold(`\n ✔ ${result.filesCreated.length} files created`));
@@ -138,13 +139,19 @@ exports.canvasCommand
138
139
  .description('Deploy a template directly (non-interactive)')
139
140
  .option('--name <name>', 'Project name')
140
141
  .option('--workspace <ws>', 'Workspace name', 'Mnemosyne-OS')
142
+ .option('--ecosystem <eco>', 'Ecosystem name (white-label)', 'Mnemosyne Neural OS')
141
143
  .option('--dir <dir>', 'Target directory')
144
+ .option('--dry-run', 'Preview files without writing to disk')
142
145
  .action(async (templateId, opts) => {
143
146
  const projectName = opts.name ?? templateId;
147
+ const dryRun = opts.dryRun ?? false;
144
148
  console.log(chalk_1.default.hex('#8B5CF6').bold('\n ⬡ MnemoCanvas — Deploy\n'));
149
+ if (dryRun)
150
+ console.log(chalk_1.default.yellow(' [dry-run] No files will be written.\n'));
145
151
  try {
146
- const result = (0, canvas_js_1.scaffold)({ projectName, workspace: opts.workspace, targetDir: opts.dir, template: templateId }, (file) => console.log(chalk_1.default.hex('#4ADE80')(' ✔ ') + chalk_1.default.hex('#6B7280')(file)));
147
- console.log(chalk_1.default.hex('#4ADE80').bold(`\n ✔ ${result.filesCreated.length} files · ${result.rootDir}\n`));
152
+ const result = (0, canvas_js_1.scaffold)({ projectName, workspace: opts.workspace, ecosystem: opts.ecosystem, targetDir: opts.dir, template: templateId, dryRun }, (file) => console.log(chalk_1.default.hex('#4ADE80')(' ✔ ') + chalk_1.default.hex('#6B7280')(file)));
153
+ const label = dryRun ? 'files would be created' : 'files created';
154
+ console.log(chalk_1.default.hex('#4ADE80').bold(`\n ✔ ${result.filesCreated.length} ${label} · ${result.rootDir}\n`));
148
155
  }
149
156
  catch (err) {
150
157
  console.log(chalk_1.default.red(`\n ✖ ${err.message}\n`));
@@ -66,9 +66,17 @@ function scaffold(opts, onFile) {
66
66
  throw new Error(`Template "${opts.template}" is not yet available.`);
67
67
  const slug = (0, renderer_js_1.toSlug)(opts.projectName);
68
68
  const rootDir = opts.targetDir ?? path_1.default.join(process.cwd(), slug);
69
- const vars = (0, renderer_js_1.buildVars)(opts.projectName, opts.workspace, opts.author, opts.email);
69
+ const vars = (0, renderer_js_1.buildVars)(opts.projectName, opts.workspace, opts.author, opts.email, opts.ecosystem);
70
70
  const filesCreated = [];
71
71
  const errors = [];
72
+ // Dry-run mode — list files without writing
73
+ if (opts.dryRun) {
74
+ for (const file of template.files) {
75
+ filesCreated.push(file.path);
76
+ onFile?.(file.path);
77
+ }
78
+ return { rootDir, filesCreated, errors };
79
+ }
72
80
  // Refuse to overwrite non-empty existing directory
73
81
  if (fs_1.default.existsSync(rootDir)) {
74
82
  const contents = fs_1.default.readdirSync(rootDir);
@@ -25,16 +25,17 @@ function toSlug(name) {
25
25
  function toPascal(slug) {
26
26
  return slug.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('');
27
27
  }
28
- function buildVars(projectName, workspace, author = 'XPACEGEMS', email = '') {
28
+ function buildVars(projectName, workspace, author = 'XPACEGEMS', email = '', ecosystem = 'Mnemosyne Neural OS') {
29
29
  const slug = toSlug(projectName);
30
30
  return {
31
31
  PROJECT_NAME: projectName,
32
32
  PROJECT_SLUG: slug,
33
33
  PROJECT_PASCAL: toPascal(slug),
34
34
  WORKSPACE: workspace,
35
+ ECOSYSTEM: ecosystem,
35
36
  DATE: new Date().toISOString().slice(0, 10),
36
37
  AUTHOR: author,
37
38
  AUTHOR_EMAIL: email,
38
- MNEMOFORGE_VERSION: '1.2.4',
39
+ MNEMOFORGE_VERSION: '1.2.5',
39
40
  };
40
41
  }
@@ -68,7 +68,7 @@ function parseChronicle(filename, dir) {
68
68
  continue;
69
69
  if (l === '---')
70
70
  continue;
71
- if (l.length > 8 && frontmatterDone && !l.startsWith('#') && !l.match(/^[\w_]+:\s*/))
71
+ if (l.length > 8 && !l.startsWith('#') && !l.match(/^\w+:\s*/) && (frontmatterDone || dividerCount === 0))
72
72
  bodyLines.push(l);
73
73
  }
74
74
  const firstMeaningful = bodyLines.find(l => l.length > 10);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemosyne_os/forge",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "MnemoForge CLI — The AI Inception Engine for the Mnemosyne Neural OS ecosystem. Scaffold sovereign, AI-governed modules in one command.",
5
5
  "files": [
6
6
  "dist/",
@@ -16,6 +16,8 @@
16
16
  "build": "tsc",
17
17
  "start": "node dist/cli.js",
18
18
  "dev": "tsc -w",
19
+ "test": "tsx --test src/__tests__/**/*.test.ts",
20
+ "test:watch": "tsx --test --watch src/__tests__/**/*.test.ts",
19
21
  "prepublishOnly": "npm run build"
20
22
  },
21
23
  "publishConfig": {
@@ -64,6 +66,8 @@
64
66
  "devDependencies": {
65
67
  "@types/inquirer": "^8.2.10",
66
68
  "@types/node": "^20.10.0",
67
- "typescript": "^5.3.3"
69
+ "tsx": "^4.21.0",
70
+ "typescript": "^5.3.3",
71
+ "vitest": "^2.1.9"
68
72
  }
69
73
  }