modscape 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/package.json +27 -0
- package/src/build.js +108 -0
- package/src/dev.js +154 -0
- package/src/index.js +47 -0
- package/src/init.js +102 -0
- package/src/templates/claude/clauderules +9 -0
- package/src/templates/claude/command.md +14 -0
- package/src/templates/codex/prompt.md +15 -0
- package/src/templates/gemini/SKILL.md +20 -0
- package/src/templates/gemini/command.toml +14 -0
- package/src/templates/rules.md +88 -0
- package/visualizer-dist/assets/index-B6WXUgSk.css +1 -0
- package/visualizer-dist/assets/index-D1C3Ic_0.js +40 -0
- package/visualizer-dist/favicon.svg +24 -0
- package/visualizer-dist/index.html +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yujikawa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# <img src="./readme_assets/logo.svg" width="32" height="32" align="center" /> Modscape
|
|
2
|
+
|
|
3
|
+
Modscape is a YAML-driven data modeling visualizer. It helps data engineers and architects bridge the gap between conceptual, logical, and physical data models while maintaining sample data "stories".
|
|
4
|
+
|
|
5
|
+
[sample page](https://yujikawa.github.io/modscape/)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **YAML-First**: Define your entire data model in a single, simple YAML file.
|
|
10
|
+
- **Unified Sidebar**: A feature-rich sidebar accessible in both `dev` and `build` modes.
|
|
11
|
+
- **Tabs**: Switch between **Editor** (YAML) and **Entities** (Navigation).
|
|
12
|
+
- **Collapsible**: Collapse the sidebar to maximize your ER diagram viewing area.
|
|
13
|
+
- **Interactive ER Diagram**: Drag-and-drop entities to create the perfect layout.
|
|
14
|
+
- **Diagram Navigation**: Click entities in the sidebar to smoothly focus and zoom in on them.
|
|
15
|
+
- **Sandbox Mode**: Temporary in-memory editing in static builds. Try "What-if" modeling without affecting the source.
|
|
16
|
+
- **Layout Persistence**: Diagram positions (including **Domains**) are automatically saved to your YAML file in dev mode.
|
|
17
|
+
- **CLI-Driven Workflow**:
|
|
18
|
+
- `modscape dev`: Interactive editor with live updates.
|
|
19
|
+
- `modscape build`: Package your model into a standalone static site.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### Prerequisites
|
|
24
|
+
- Node.js (v18 or higher)
|
|
25
|
+
|
|
26
|
+
### Global Installation (via GitHub)
|
|
27
|
+
You can install Modscape directly from GitHub to use the `modscape` command anywhere:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g https://github.com/yujikawa/modscape
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Local Setup (for Development)
|
|
34
|
+
```bash
|
|
35
|
+
# Clone the repository
|
|
36
|
+
git clone https://github.com/yujikawa/modscape.git
|
|
37
|
+
cd modscape
|
|
38
|
+
|
|
39
|
+
# Install dependencies for both CLI and Visualizer
|
|
40
|
+
npm install
|
|
41
|
+
cd visualizer && npm install
|
|
42
|
+
cd ..
|
|
43
|
+
|
|
44
|
+
# Link the command to your system
|
|
45
|
+
npm link
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### 0. Initialization (AI Agent Setup)
|
|
51
|
+
Scaffold project-specific modeling rules and configure your favorite AI agents (Gemini, Codex, Claude) to follow them.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
modscape init
|
|
55
|
+
```
|
|
56
|
+
- Creates `.modscape/rules.md` as your project's "Source of Truth" for modeling.
|
|
57
|
+
- Generates agent-specific configurations (skills, prompts, commands) that point to these rules.
|
|
58
|
+
- **Why?**: This ensures AI agents generate YAML that perfectly matches your organization's standards.
|
|
59
|
+
|
|
60
|
+
### 1. Development Mode (Interactive Editor)
|
|
61
|
+
Start a local session to edit your YAML and arrange entities.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Point to a directory to manage all models within it
|
|
65
|
+
modscape dev samples/
|
|
66
|
+
|
|
67
|
+
# Or point to a specific file
|
|
68
|
+
modscape dev my-model.yaml
|
|
69
|
+
```
|
|
70
|
+
- Opens `http://localhost:5173` automatically.
|
|
71
|
+
- **Multi-file Support**: Switch between models using the dropdown in the sidebar.
|
|
72
|
+
- **Secure Routing**: Models are accessed via slugs (e.g., `?model=ecommerce`), keeping your local paths private.
|
|
73
|
+
- **Editor Tab**: Edit YAML with live updates to the diagram.
|
|
74
|
+
- **Entities Tab**: Search and quickly navigate to specific tables or domains.
|
|
75
|
+
- **Persistence**: Drag entities to save positions directly to the source YAML file.
|
|
76
|
+
|
|
77
|
+
### 2. Static Site Build
|
|
78
|
+
Generate a standalone documentation site from your YAML model.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
modscape build my-model.yaml -o ./docs-site
|
|
82
|
+
```
|
|
83
|
+
- Generates a `docs-site/` folder with a single-file visualizer.
|
|
84
|
+
- Includes the **Sandbox Mode**, allowing viewers to temporarily edit the model in their browser.
|
|
85
|
+
- Perfect for hosting on **GitHub Pages**, S3, or internal documentation portals.
|
|
86
|
+
|
|
87
|
+
## YAML Schema Example
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
tables:
|
|
91
|
+
- id: hub_customer
|
|
92
|
+
name: HUB_CUSTOMER
|
|
93
|
+
appearance: # Optional: Visual style (icon and color)
|
|
94
|
+
type: "hub" # Predefined: hub, link, satellite, fact, dimension
|
|
95
|
+
icon: "🌐" # Optional: Emoji override
|
|
96
|
+
color: "#fbbf24" # Optional: Hex color override
|
|
97
|
+
conceptual:
|
|
98
|
+
description: "Business keys for unique customers."
|
|
99
|
+
tags: ["HUB", "PARTY"]
|
|
100
|
+
columns:
|
|
101
|
+
- id: hk_customer
|
|
102
|
+
logical: { name: "HK_CUSTOMER", type: "Binary", isPrimaryKey: true }
|
|
103
|
+
physical: { name: "HK_CUST", type: "BINARY(16)", constraints: ["NOT NULL"] }
|
|
104
|
+
|
|
105
|
+
domains:
|
|
106
|
+
- id: customer_domain
|
|
107
|
+
name: Customer Domain
|
|
108
|
+
tables: ["hub_customer"]
|
|
109
|
+
color: "rgba(59, 130, 246, 0.05)"
|
|
110
|
+
|
|
111
|
+
relationships:
|
|
112
|
+
- from: { table: hub_customer, column: hk_customer }
|
|
113
|
+
to: { table: sat_customer_crm, column: hk_customer }
|
|
114
|
+
type: "one-to-many"
|
|
115
|
+
|
|
116
|
+
layout: # Automatically managed by the visualizer or AI Agent
|
|
117
|
+
hub_customer: { x: 100, y: 100 }
|
|
118
|
+
customer_domain: { x: 50, y: 50, width: 600, height: 400 }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Key Attributes
|
|
122
|
+
- **appearance**: Controls the node's visual identity. `type` sets a default icon and color (e.g., `hub` is Amber 🌐, `fact` is Red 📊). Use `icon` or `color` to override.
|
|
123
|
+
- **layout**: Stores (x, y) coordinates and dimensions. While the visualizer manages this automatically during drag-and-drop, AI Agents use this to arrange new entities logically.
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
## Credits
|
|
127
|
+
|
|
128
|
+
Modscape is built upon several incredible open-source projects:
|
|
129
|
+
|
|
130
|
+
- **[React Flow](https://reactflow.dev/)**: Powering the interactive graph engine.
|
|
131
|
+
- **[Radix UI](https://www.radix-ui.com/)**: Providing accessible, high-quality primitives for the sidebar.
|
|
132
|
+
- **[Lucide](https://lucide.dev/)**: Beautiful, consistent iconography.
|
|
133
|
+
- **[shadcn/ui](https://ui.shadcn.com/)**: Design inspiration and component patterns.
|
|
134
|
+
- **[Express](https://expressjs.com/)**: Serving the development environment.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "modscape",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Modscape: A YAML-driven data modeling visualizer CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"modscape": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"visualizer-dist",
|
|
12
|
+
"package.json",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build-ui": "cd visualizer && npm run build && rm -rf ../visualizer-dist && mv dist ../visualizer-dist",
|
|
17
|
+
"dev": "node src/index.js dev"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@inquirer/prompts": "^8.3.0",
|
|
21
|
+
"chokidar": "^5.0.0",
|
|
22
|
+
"commander": "^14.0.3",
|
|
23
|
+
"express": "^5.2.1",
|
|
24
|
+
"js-yaml": "^4.1.1",
|
|
25
|
+
"open": "^11.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/build.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
function scanFiles(inputPaths) {
|
|
9
|
+
const modelMap = new Map();
|
|
10
|
+
inputPaths.forEach(inputPath => {
|
|
11
|
+
const absolutePath = path.resolve(process.cwd(), inputPath);
|
|
12
|
+
if (!fs.existsSync(absolutePath)) {
|
|
13
|
+
console.warn(` ⚠️ Warning: Path not found: ${inputPath}`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const stats = fs.statSync(absolutePath);
|
|
18
|
+
if (stats.isDirectory()) {
|
|
19
|
+
const files = fs.readdirSync(absolutePath);
|
|
20
|
+
files.forEach(file => {
|
|
21
|
+
if (file.endsWith('.yaml') || file.endsWith('.yml')) {
|
|
22
|
+
const slug = path.parse(file).name;
|
|
23
|
+
const fullPath = path.join(absolutePath, file);
|
|
24
|
+
if (modelMap.has(slug)) {
|
|
25
|
+
const parentName = path.basename(absolutePath);
|
|
26
|
+
modelMap.set(`${parentName}-${slug}`, fullPath);
|
|
27
|
+
} else {
|
|
28
|
+
modelMap.set(slug, fullPath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
} else if (stats.isFile() && (inputPath.endsWith('.yaml') || inputPath.endsWith('.yml'))) {
|
|
33
|
+
const slug = path.parse(absolutePath).name;
|
|
34
|
+
modelMap.set(slug, absolutePath);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return modelMap;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function build(paths, visualizerPath, outputDir) {
|
|
41
|
+
const modelMap = scanFiles(Array.isArray(paths) ? paths : [paths]);
|
|
42
|
+
const absoluteOutputDir = path.resolve(process.cwd(), outputDir);
|
|
43
|
+
const distPath = path.resolve(__dirname, '../visualizer-dist');
|
|
44
|
+
|
|
45
|
+
if (modelMap.size === 0) {
|
|
46
|
+
console.error('\n ❌ Error: No YAML files found in the specified paths.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(distPath)) {
|
|
51
|
+
console.error('\n ❌ Error: visualizer-dist not found. Please run "npm run build-ui" first.\n');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(`\n 📦 Building static site with ${modelMap.size} model(s)...`);
|
|
56
|
+
|
|
57
|
+
// Create output dir and copy all files from visualizer-dist
|
|
58
|
+
if (!fs.existsSync(absoluteOutputDir)) {
|
|
59
|
+
fs.mkdirSync(absoluteOutputDir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Copy recursive helper
|
|
63
|
+
const copyDir = (src, dest) => {
|
|
64
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
65
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
66
|
+
for (let entry of entries) {
|
|
67
|
+
const srcPath = path.join(src, entry.name);
|
|
68
|
+
const destPath = path.join(dest, entry.name);
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
copyDir(srcPath, destPath);
|
|
71
|
+
} else {
|
|
72
|
+
fs.copyFileSync(srcPath, destPath);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
copyDir(distPath, absoluteOutputDir);
|
|
78
|
+
|
|
79
|
+
// Prepare data for injection
|
|
80
|
+
const modelsData = [];
|
|
81
|
+
for (const [slug, absolutePath] of modelMap.entries()) {
|
|
82
|
+
try {
|
|
83
|
+
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
84
|
+
const schema = yaml.load(content);
|
|
85
|
+
const name = slug.charAt(0).toUpperCase() + slug.slice(1).replace(/[-_]/g, ' ');
|
|
86
|
+
modelsData.push({ slug, name, schema });
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.warn(` ⚠️ Warning: Failed to load ${absolutePath}: ${e.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Inject data into index.html
|
|
93
|
+
const indexPath = path.join(absoluteOutputDir, 'index.html');
|
|
94
|
+
let html = fs.readFileSync(indexPath, 'utf8');
|
|
95
|
+
|
|
96
|
+
const injectionData = {
|
|
97
|
+
isMultiFile: modelsData.length > 1,
|
|
98
|
+
models: modelsData
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
html = html.replace(
|
|
102
|
+
'</head>',
|
|
103
|
+
`<script>window.__MODSCAPE_DATA__ = ${JSON.stringify(injectionData)}; window.MODSCAPE_CLI_MODE = false;</script></head>`
|
|
104
|
+
);
|
|
105
|
+
fs.writeFileSync(indexPath, html, 'utf8');
|
|
106
|
+
|
|
107
|
+
console.log(`\n ✅ Build complete! Static site generated in: ${outputDir}`);
|
|
108
|
+
}
|
package/src/dev.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import chokidar from 'chokidar';
|
|
6
|
+
import open from 'open';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
function scanFiles(inputPaths) {
|
|
12
|
+
const modelMap = new Map();
|
|
13
|
+
inputPaths.forEach(inputPath => {
|
|
14
|
+
const absolutePath = path.resolve(process.cwd(), inputPath);
|
|
15
|
+
if (!fs.existsSync(absolutePath)) {
|
|
16
|
+
console.warn(` ⚠️ Warning: Path not found: ${inputPath}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const stats = fs.statSync(absolutePath);
|
|
21
|
+
if (stats.isDirectory()) {
|
|
22
|
+
const files = fs.readdirSync(absolutePath);
|
|
23
|
+
files.forEach(file => {
|
|
24
|
+
if (file.endsWith('.yaml') || file.endsWith('.yml')) {
|
|
25
|
+
const slug = path.parse(file).name;
|
|
26
|
+
const fullPath = path.join(absolutePath, file);
|
|
27
|
+
if (modelMap.has(slug)) {
|
|
28
|
+
// Collision handling: append folder name
|
|
29
|
+
const parentName = path.basename(absolutePath);
|
|
30
|
+
modelMap.set(`${parentName}-${slug}`, fullPath);
|
|
31
|
+
} else {
|
|
32
|
+
modelMap.set(slug, fullPath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
} else if (stats.isFile() && (inputPath.endsWith('.yaml') || inputPath.endsWith('.yml'))) {
|
|
37
|
+
const slug = path.parse(absolutePath).name;
|
|
38
|
+
modelMap.set(slug, absolutePath);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return modelMap;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function startDevServer(paths, visualizerPath) {
|
|
45
|
+
const app = express();
|
|
46
|
+
app.use(express.json());
|
|
47
|
+
|
|
48
|
+
const modelMap = scanFiles(Array.isArray(paths) ? paths : [paths]);
|
|
49
|
+
const distPath = path.resolve(__dirname, '../visualizer-dist');
|
|
50
|
+
|
|
51
|
+
if (modelMap.size === 0) {
|
|
52
|
+
console.error(`\n ❌ Error: No YAML files found in the specified paths.`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!fs.existsSync(distPath)) {
|
|
57
|
+
console.error(`\n ❌ Error: visualizer-dist not found at ${distPath}`);
|
|
58
|
+
console.log(' Please run "npm run build-ui" first.\n');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Helper to get absolute path from model slug
|
|
63
|
+
const getModelPath = (slug) => {
|
|
64
|
+
if (slug) return modelMap.get(slug);
|
|
65
|
+
// If no slug provided, return the first available model
|
|
66
|
+
return modelMap.values().next().value;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// API to list all available models
|
|
70
|
+
app.get('/api/files', (req, res) => {
|
|
71
|
+
const files = Array.from(modelMap.entries()).map(([slug, fullPath]) => ({
|
|
72
|
+
slug,
|
|
73
|
+
name: slug.charAt(0).toUpperCase() + slug.slice(1).replace(/[-_]/g, ' '),
|
|
74
|
+
path: path.relative(process.cwd(), fullPath)
|
|
75
|
+
}));
|
|
76
|
+
res.json(files);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// API to get current YAML data
|
|
80
|
+
app.get('/api/model', (req, res) => {
|
|
81
|
+
const modelPath = getModelPath(req.query.model);
|
|
82
|
+
if (!modelPath || !fs.existsSync(modelPath)) {
|
|
83
|
+
return res.status(404).json({ error: 'Model not found' });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const content = fs.readFileSync(modelPath, 'utf8');
|
|
88
|
+
res.json(yaml.load(content));
|
|
89
|
+
} catch (e) {
|
|
90
|
+
res.status(500).json({ error: e.message });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// API to save layout changes
|
|
95
|
+
app.post('/api/layout', (req, res) => {
|
|
96
|
+
const modelPath = getModelPath(req.query.model);
|
|
97
|
+
if (!modelPath) return res.status(404).json({ error: 'Model not found' });
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const layout = req.body;
|
|
101
|
+
const content = fs.readFileSync(modelPath, 'utf8');
|
|
102
|
+
const data = yaml.load(content);
|
|
103
|
+
data.layout = layout;
|
|
104
|
+
fs.writeFileSync(modelPath, yaml.dump(data, { indent: 2, lineWidth: -1 }), 'utf8');
|
|
105
|
+
res.json({ success: true });
|
|
106
|
+
} catch (e) {
|
|
107
|
+
res.status(500).json({ error: e.message });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// API to save entire YAML content
|
|
112
|
+
app.post('/api/save-yaml', (req, res) => {
|
|
113
|
+
const modelPath = getModelPath(req.query.model);
|
|
114
|
+
if (!modelPath) return res.status(404).json({ error: 'Model not found' });
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const { yaml: yamlContent } = req.body;
|
|
118
|
+
yaml.load(yamlContent);
|
|
119
|
+
fs.writeFileSync(modelPath, yamlContent, 'utf8');
|
|
120
|
+
res.json({ success: true });
|
|
121
|
+
} catch (e) {
|
|
122
|
+
res.status(500).json({ error: e.message });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Serve static files EXCEPT index.html
|
|
127
|
+
app.use(express.static(distPath, { index: false }));
|
|
128
|
+
|
|
129
|
+
// Intercept index.html to inject CLI_MODE flag
|
|
130
|
+
app.get('{/*path}', (req, res) => {
|
|
131
|
+
try {
|
|
132
|
+
let html = fs.readFileSync(path.join(distPath, 'index.html'), 'utf8');
|
|
133
|
+
html = html.replace(
|
|
134
|
+
'</head>',
|
|
135
|
+
'<script>window.MODSCAPE_CLI_MODE = true;</script></head>'
|
|
136
|
+
);
|
|
137
|
+
res.send(html);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
res.status(500).send('Error loading visualizer');
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const port = 5173;
|
|
144
|
+
app.listen(port, () => {
|
|
145
|
+
const url = `http://localhost:${port}`;
|
|
146
|
+
console.log(`\n 🚀 Modscape Visualizer running at: ${url}`);
|
|
147
|
+
console.log(` Watching ${modelMap.size} file(s)`);
|
|
148
|
+
open(url);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
chokidar.watch(Array.from(modelMap.values())).on('change', (changedPath) => {
|
|
152
|
+
console.log(` File changed: ${path.relative(process.cwd(), changedPath)}. Please refresh the browser.`);
|
|
153
|
+
});
|
|
154
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { startDevServer } from './dev.js';
|
|
7
|
+
import { build } from './build.js';
|
|
8
|
+
import { initProject } from './init.js';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const VISUALIZER_PATH = path.resolve(__dirname, '../visualizer');
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('modscape')
|
|
16
|
+
.description('A YAML-driven data modeling visualizer CLI')
|
|
17
|
+
.version('0.1.0');
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command('init')
|
|
21
|
+
.description('Initialize project with AI modeling rules')
|
|
22
|
+
.option('-g, --gemini', 'Scaffold for Gemini CLI')
|
|
23
|
+
.option('-x, --codex', 'Scaffold for Codex')
|
|
24
|
+
.option('-c, --claude', 'Scaffold for Claude Code')
|
|
25
|
+
.option('-a, --all', 'Scaffold for all agents')
|
|
26
|
+
.action((options) => {
|
|
27
|
+
initProject(options);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command('dev')
|
|
32
|
+
.description('Start the development visualizer with local YAML files or directories')
|
|
33
|
+
.argument('<paths...>', 'paths to YAML model files or directories')
|
|
34
|
+
.action((paths) => {
|
|
35
|
+
startDevServer(paths, VISUALIZER_PATH);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command('build')
|
|
40
|
+
.description('Build a static site from YAML models')
|
|
41
|
+
.argument('<paths...>', 'paths to YAML model files or directories')
|
|
42
|
+
.option('-o, --output <dir>', 'output directory', 'dist')
|
|
43
|
+
.action((paths, options) => {
|
|
44
|
+
build(paths, VISUALIZER_PATH, options.output);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
program.parse();
|
package/src/init.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
async function safeWriteFile(filePath, content) {
|
|
9
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
10
|
+
const dir = path.dirname(absolutePath);
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(dir)) {
|
|
13
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (fs.existsSync(absolutePath)) {
|
|
17
|
+
const overwrite = await confirm({
|
|
18
|
+
message: `File ${filePath} already exists. Overwrite?`,
|
|
19
|
+
default: false,
|
|
20
|
+
});
|
|
21
|
+
if (!overwrite) {
|
|
22
|
+
console.log(` Skipping ${filePath}`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fs.writeFileSync(absolutePath, content, 'utf8');
|
|
28
|
+
console.log(` Created ${filePath}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function initProject(options = {}) {
|
|
32
|
+
console.log('\n 🛠️ ModMod Project Initialization\n');
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const agents = [];
|
|
36
|
+
|
|
37
|
+
// If options are provided via CLI flags, use them
|
|
38
|
+
if (options.all) {
|
|
39
|
+
agents.push('gemini', 'codex', 'claude');
|
|
40
|
+
} else if (options.gemini || options.codex || options.claude) {
|
|
41
|
+
if (options.gemini) agents.push('gemini');
|
|
42
|
+
if (options.codex) agents.push('codex');
|
|
43
|
+
if (options.claude) agents.push('claude');
|
|
44
|
+
} else {
|
|
45
|
+
// Otherwise, ask one by one (more robust than checkbox in some terminals)
|
|
46
|
+
console.log(' Please confirm which AI agents you want to scaffold for:\n');
|
|
47
|
+
|
|
48
|
+
if (await confirm({ message: 'Scaffold for Gemini CLI?', default: false })) {
|
|
49
|
+
agents.push('gemini');
|
|
50
|
+
}
|
|
51
|
+
if (await confirm({ message: 'Scaffold for Codex?', default: false })) {
|
|
52
|
+
agents.push('codex');
|
|
53
|
+
}
|
|
54
|
+
if (await confirm({ message: 'Scaffold for Claude Code?', default: false })) {
|
|
55
|
+
agents.push('claude');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (agents.length === 0) {
|
|
60
|
+
console.log('\n ⚠️ No agents selected. Only ".modscape/rules.md" will be created.');
|
|
61
|
+
} else {
|
|
62
|
+
console.log(`\n Selected agents: ${agents.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log('\n Scaffolding modeling rules and commands...');
|
|
66
|
+
|
|
67
|
+
// 1. Create .modscape/rules.md
|
|
68
|
+
const rulesTemplatePath = path.join(__dirname, 'templates/rules.md');
|
|
69
|
+
const rulesTemplate = fs.readFileSync(rulesTemplatePath, 'utf8');
|
|
70
|
+
await safeWriteFile('.modscape/rules.md', rulesTemplate);
|
|
71
|
+
|
|
72
|
+
// 2. Create agent-specific files
|
|
73
|
+
if (agents.includes('gemini')) {
|
|
74
|
+
const skillTemplate = fs.readFileSync(path.join(__dirname, 'templates/gemini/SKILL.md'), 'utf8');
|
|
75
|
+
await safeWriteFile('.gemini/skills/modmod/SKILL.md', skillTemplate);
|
|
76
|
+
|
|
77
|
+
const commandTemplate = fs.readFileSync(path.join(__dirname, 'templates/gemini/command.toml'), 'utf8');
|
|
78
|
+
await safeWriteFile('.gemini/commands/modmod/modeling.toml', commandTemplate);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (agents.includes('codex')) {
|
|
82
|
+
const promptTemplate = fs.readFileSync(path.join(__dirname, 'templates/codex/prompt.md'), 'utf8');
|
|
83
|
+
await safeWriteFile('.codex/prompts/modmod-modeling.md', promptTemplate);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (agents.includes('claude')) {
|
|
87
|
+
const clauderules = fs.readFileSync(path.join(__dirname, 'templates/claude/clauderules'), 'utf8');
|
|
88
|
+
await safeWriteFile('.clauderules', clauderules);
|
|
89
|
+
|
|
90
|
+
const commandTemplate = fs.readFileSync(path.join(__dirname, 'templates/claude/command.md'), 'utf8');
|
|
91
|
+
await safeWriteFile('.claude/commands/modmod/modeling.md', commandTemplate);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log('\n ✅ Initialization complete! Customize ".modscape/rules.md" to match your project standards.\n');
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error.name === 'ExitPromptError') {
|
|
97
|
+
console.log('\n Initialization cancelled by user.');
|
|
98
|
+
} else {
|
|
99
|
+
console.error('\n An error occurred during initialization:', error.message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
You are a professional Data Modeler. Your primary directive is to manage `model.yaml`.
|
|
2
|
+
|
|
3
|
+
## COMMAND: /modscape:modeling
|
|
4
|
+
When the user issues this command:
|
|
5
|
+
1. READ `.modscape/rules.md` to understand project strategy and conventions.
|
|
6
|
+
2. ANALYZE `model.yaml` (if present).
|
|
7
|
+
3. INTERACT with the user to gather requirements and update the model strictly following the rules.
|
|
8
|
+
|
|
9
|
+
BEFORE making any suggestions or changes, you MUST read and strictly follow the rules defined in `.modscape/rules.md`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# /modscape:modeling
|
|
2
|
+
|
|
3
|
+
Start an interactive data modeling session.
|
|
4
|
+
|
|
5
|
+
## Instructions
|
|
6
|
+
1. FIRST, read `.modscape/rules.md` to understand the project strategy, naming conventions, and YAML schema.
|
|
7
|
+
2. SECOND, analyze the existing `model.yaml` if it exists.
|
|
8
|
+
3. Listen to the user's requirements and propose/apply changes to `model.yaml` strictly following the rules.
|
|
9
|
+
|
|
10
|
+
## Appearance & Layout
|
|
11
|
+
- **Appearance**: For new tables, include the `appearance: { type: "..." }` block.
|
|
12
|
+
- **Layout**: When creating new entities, always assign initial `x` and `y` coordinates in the `layout` section. Position them logically near their related entities to avoid stacking.
|
|
13
|
+
|
|
14
|
+
Always prioritize consistency and project-specific standards.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Data Modeling Instructions
|
|
2
|
+
|
|
3
|
+
You are a professional Data Modeler. Your primary directive is to manage `model.yaml`.
|
|
4
|
+
|
|
5
|
+
## COMMAND: /modscape:modeling
|
|
6
|
+
When the user issues this command:
|
|
7
|
+
1. READ `.modscape/rules.md` to understand project strategy and conventions.
|
|
8
|
+
2. ANALYZE `model.yaml` (if present).
|
|
9
|
+
3. INTERACT with the user to gather requirements and update the model strictly following the rules.
|
|
10
|
+
|
|
11
|
+
## Appearance & Layout
|
|
12
|
+
- **Appearance**: When creating new tables, include the `appearance` block with an appropriate `type`.
|
|
13
|
+
- **Layout**: For any new entity, assign logical `x` and `y` coordinates in the `layout` section to prevent overlapping and ensure a clean initial visualization.
|
|
14
|
+
|
|
15
|
+
ALWAYS follow the rules defined in `.modscape/rules.md` for any modeling tasks.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Data Modeling Expert
|
|
2
|
+
|
|
3
|
+
You are a professional Data Modeler. Your primary directive is to manage `model.yaml`.
|
|
4
|
+
|
|
5
|
+
BEFORE making any suggestions or changes, you MUST read and strictly follow the rules defined in `.modscape/rules.md`.
|
|
6
|
+
|
|
7
|
+
If a requested change violates these rules, warn the user.
|
|
8
|
+
|
|
9
|
+
## 📁 Multi-file Awareness
|
|
10
|
+
`modscape dev` supports pointing to a directory (e.g., `modscape dev samples/`).
|
|
11
|
+
- **Switching Models**: Identify which YAML file you are editing from the directory.
|
|
12
|
+
- **Domain Separation**: Suggest splitting large models into multiple, domain-specific YAML files to improve organization.
|
|
13
|
+
- **Slug-based Access**: Be aware that the visualizer identifies models via slugs (filename without extension).
|
|
14
|
+
|
|
15
|
+
## Layout & Appearance Management
|
|
16
|
+
- **Appearance**: When creating new tables, assign an appropriate `appearance.type` (e.g., `fact`, `hub`) to ensure correct visualization.
|
|
17
|
+
- **Layout**: You are responsible for the initial placement of new entities. Assign logical `x` and `y` coordinates in the `layout` section so they don't overlap existing nodes. The user will fine-tune the layout via the GUI.
|
|
18
|
+
|
|
19
|
+
## Interactive Modeling
|
|
20
|
+
When the user wants to perform modeling tasks, ensure you are utilizing the strategy and conventions defined in the project rules. You can be triggered via the `/modscape:modeling` command which provides a dedicated workflow.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# /modscape:modeling command definition
|
|
2
|
+
|
|
3
|
+
description = "Start an interactive data modeling session using project rules"
|
|
4
|
+
|
|
5
|
+
[prompt]
|
|
6
|
+
instruction = """
|
|
7
|
+
You are now in **Modscape Modeling Mode**.
|
|
8
|
+
|
|
9
|
+
1. FIRST, read `.modscape/rules.md` to understand the project strategy, naming conventions, and YAML schema.
|
|
10
|
+
2. SECOND, analyze the existing `model.yaml` if it exists.
|
|
11
|
+
3. Listen to the user's requirements and propose/apply changes to `model.yaml` strictly following the rules.
|
|
12
|
+
|
|
13
|
+
Stay in this mode until the task is complete. Always prioritize consistency and project-specific standards.
|
|
14
|
+
"""
|