@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
|
+
});
|
package/dist/commands/canvas.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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 &&
|
|
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.
|
|
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
|
-
"
|
|
69
|
+
"tsx": "^4.21.0",
|
|
70
|
+
"typescript": "^5.3.3",
|
|
71
|
+
"vitest": "^2.1.9"
|
|
68
72
|
}
|
|
69
73
|
}
|