create-imagine 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 +40 -0
- package/bin/create-imagine.js +407 -0
- package/package.json +19 -0
- package/templates/blank/README.md +96 -0
- package/templates/blank/gitignore +6 -0
- package/templates/blank/index.html +13 -0
- package/templates/blank/package.json +42 -0
- package/templates/blank/projects/example/figures/hello-world.tsx +45 -0
- package/templates/blank/projects/example/manifest.ts +44 -0
- package/templates/blank/projects/example/project.ts +16 -0
- package/templates/blank/projects/example/props.json +4 -0
- package/templates/blank/scripts/list.ts +46 -0
- package/templates/blank/scripts/projects.ts +43 -0
- package/templates/blank/scripts/render.ts +288 -0
- package/templates/blank/scripts/server.ts +117 -0
- package/templates/blank/src/core/__tests__/controls.test.ts +14 -0
- package/templates/blank/src/core/__tests__/manifest.test.ts +45 -0
- package/templates/blank/src/core/__tests__/props.test.ts +9 -0
- package/templates/blank/src/core/controls.ts +14 -0
- package/templates/blank/src/core/manifest.ts +134 -0
- package/templates/blank/src/core/props.ts +7 -0
- package/templates/blank/src/framework/Figure.tsx +34 -0
- package/templates/blank/src/framework/__tests__/sizing.test.ts +29 -0
- package/templates/blank/src/framework/charts/Axes.tsx +103 -0
- package/templates/blank/src/framework/charts/GridLines.tsx +59 -0
- package/templates/blank/src/framework/charts/Legend.tsx +31 -0
- package/templates/blank/src/framework/charts/Series.tsx +50 -0
- package/templates/blank/src/framework/charts/scales.ts +19 -0
- package/templates/blank/src/framework/diagrams/primitives.tsx +134 -0
- package/templates/blank/src/framework/layout/PanelGrid.tsx +60 -0
- package/templates/blank/src/framework/math/MathSvg.tsx +35 -0
- package/templates/blank/src/framework/math/mathjax.ts +64 -0
- package/templates/blank/src/framework/sizing.ts +28 -0
- package/templates/blank/src/framework/theme.ts +35 -0
- package/templates/blank/src/framework/types.ts +42 -0
- package/templates/blank/src/main.tsx +11 -0
- package/templates/blank/src/studio/StudioApp.tsx +130 -0
- package/templates/blank/src/studio/StudioRoot.tsx +14 -0
- package/templates/blank/src/studio/base64url.ts +8 -0
- package/templates/blank/src/studio/figureLoader.ts +30 -0
- package/templates/blank/src/studio/projectLoader.ts +40 -0
- package/templates/blank/src/studio/propsApi.ts +26 -0
- package/templates/blank/src/studio/routes/FigureView.tsx +365 -0
- package/templates/blank/src/studio/routes/ProjectHome.tsx +107 -0
- package/templates/blank/src/studio/routes/ProjectsHome.tsx +63 -0
- package/templates/blank/src/studio/routes/RenderView.tsx +123 -0
- package/templates/blank/src/studio/studio.css +540 -0
- package/templates/blank/src/studio/useProjectProps.ts +129 -0
- package/templates/blank/src/vite-env.d.ts +2 -0
- package/templates/blank/tsconfig.json +20 -0
- package/templates/blank/vite.config.ts +82 -0
- package/templates/blank/vitest.config.ts +8 -0
- package/templates/example/README.md +96 -0
- package/templates/example/gitignore +6 -0
- package/templates/example/index.html +13 -0
- package/templates/example/package.json +42 -0
- package/templates/example/projects/example/figures/ai-agent-architecture.tsx +133 -0
- package/templates/example/projects/example/figures/equation.tsx +29 -0
- package/templates/example/projects/example/figures/hello-world.tsx +45 -0
- package/templates/example/projects/example/figures/line-chart.tsx +80 -0
- package/templates/example/projects/example/figures/multi-panel.tsx +51 -0
- package/templates/example/projects/example/figures/pipeline-diagram.tsx +51 -0
- package/templates/example/projects/example/manifest.ts +161 -0
- package/templates/example/projects/example/project.ts +31 -0
- package/templates/example/projects/example/props.json +10 -0
- package/templates/example/public/projects/example/previews/ai-agent-architecture--default.png +0 -0
- package/templates/example/public/projects/example/previews/equation--default.png +0 -0
- package/templates/example/public/projects/example/previews/hello-world--default.png +0 -0
- package/templates/example/public/projects/example/previews/line-chart--default.png +0 -0
- package/templates/example/public/projects/example/previews/multi-panel--default.png +0 -0
- package/templates/example/public/projects/example/previews/pipeline-diagram--default.png +0 -0
- package/templates/example/scripts/list.ts +46 -0
- package/templates/example/scripts/projects.ts +43 -0
- package/templates/example/scripts/render.ts +288 -0
- package/templates/example/scripts/server.ts +117 -0
- package/templates/example/src/core/__tests__/controls.test.ts +14 -0
- package/templates/example/src/core/__tests__/manifest.test.ts +45 -0
- package/templates/example/src/core/__tests__/props.test.ts +9 -0
- package/templates/example/src/core/controls.ts +14 -0
- package/templates/example/src/core/manifest.ts +134 -0
- package/templates/example/src/core/props.ts +7 -0
- package/templates/example/src/framework/Figure.tsx +34 -0
- package/templates/example/src/framework/__tests__/sizing.test.ts +29 -0
- package/templates/example/src/framework/charts/Axes.tsx +103 -0
- package/templates/example/src/framework/charts/GridLines.tsx +59 -0
- package/templates/example/src/framework/charts/Legend.tsx +31 -0
- package/templates/example/src/framework/charts/Series.tsx +50 -0
- package/templates/example/src/framework/charts/scales.ts +19 -0
- package/templates/example/src/framework/diagrams/primitives.tsx +134 -0
- package/templates/example/src/framework/layout/PanelGrid.tsx +60 -0
- package/templates/example/src/framework/math/MathSvg.tsx +35 -0
- package/templates/example/src/framework/math/mathjax.ts +64 -0
- package/templates/example/src/framework/sizing.ts +28 -0
- package/templates/example/src/framework/theme.ts +35 -0
- package/templates/example/src/framework/types.ts +42 -0
- package/templates/example/src/main.tsx +11 -0
- package/templates/example/src/studio/StudioApp.tsx +130 -0
- package/templates/example/src/studio/StudioRoot.tsx +14 -0
- package/templates/example/src/studio/base64url.ts +8 -0
- package/templates/example/src/studio/figureLoader.ts +30 -0
- package/templates/example/src/studio/projectLoader.ts +40 -0
- package/templates/example/src/studio/propsApi.ts +26 -0
- package/templates/example/src/studio/routes/FigureView.tsx +365 -0
- package/templates/example/src/studio/routes/ProjectHome.tsx +107 -0
- package/templates/example/src/studio/routes/ProjectsHome.tsx +63 -0
- package/templates/example/src/studio/routes/RenderView.tsx +123 -0
- package/templates/example/src/studio/studio.css +540 -0
- package/templates/example/src/studio/useProjectProps.ts +129 -0
- package/templates/example/src/vite-env.d.ts +2 -0
- package/templates/example/tsconfig.json +20 -0
- package/templates/example/vite.config.ts +82 -0
- package/templates/example/vitest.config.ts +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MX
|
|
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,40 @@
|
|
|
1
|
+
# create-imagine
|
|
2
|
+
|
|
3
|
+
Scaffold a new Imagine project (React → scientific figures).
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Recommended
|
|
9
|
+
npx create-imagine@latest my-figures
|
|
10
|
+
|
|
11
|
+
# Also works (because the package name is create-imagine)
|
|
12
|
+
npm create imagine@latest my-figures
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Templates
|
|
16
|
+
|
|
17
|
+
- `blank`: minimal starter project (no preview images)
|
|
18
|
+
- `example`: full starter project (includes example figures + preview PNGs)
|
|
19
|
+
|
|
20
|
+
## Options
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
create-imagine [targetDir]
|
|
24
|
+
|
|
25
|
+
--template blank|example
|
|
26
|
+
--pm npm|pnpm|yarn|bun
|
|
27
|
+
--install / --no-install
|
|
28
|
+
--skills / --no-skills
|
|
29
|
+
--yes
|
|
30
|
+
--force
|
|
31
|
+
--help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Optional: install agent skills
|
|
35
|
+
|
|
36
|
+
This scaffolder can optionally install the `imagine-best-practices` skill into your project:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx skills add https://github.com/midhunxavier/imagine-skills --skill imagine-best-practices
|
|
40
|
+
```
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { createInterface } from 'node:readline/promises';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
function usage() {
|
|
13
|
+
const text = `
|
|
14
|
+
create-imagine
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
create-imagine [targetDir] [options]
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--template blank|example
|
|
21
|
+
--pm npm|pnpm|yarn|bun
|
|
22
|
+
--install / --no-install
|
|
23
|
+
--skills / --no-skills
|
|
24
|
+
--yes
|
|
25
|
+
--force
|
|
26
|
+
--help
|
|
27
|
+
`.trim();
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.log(text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
/** @type {{ targetDir?: string; template?: string; pm?: string; install?: boolean; skills?: boolean; yes: boolean; force: boolean; help: boolean }} */
|
|
34
|
+
const out = { yes: false, force: false, help: false };
|
|
35
|
+
/** @type {string[]} */
|
|
36
|
+
const positional = [];
|
|
37
|
+
/** @type {string[]} */
|
|
38
|
+
const unknown = [];
|
|
39
|
+
|
|
40
|
+
for (let i = 2; i < argv.length; i++) {
|
|
41
|
+
const a = argv[i];
|
|
42
|
+
if (!a) continue;
|
|
43
|
+
|
|
44
|
+
if (a === '--') {
|
|
45
|
+
positional.push(...argv.slice(i + 1).filter(Boolean));
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (a === '--help' || a === '-h') {
|
|
50
|
+
out.help = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (a === '--yes' || a === '-y') {
|
|
54
|
+
out.yes = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (a === '--force') {
|
|
58
|
+
out.force = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (a === '--install') {
|
|
62
|
+
out.install = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (a === '--no-install') {
|
|
66
|
+
out.install = false;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (a === '--skills') {
|
|
70
|
+
out.skills = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (a === '--no-skills') {
|
|
74
|
+
out.skills = false;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (a === '--template') {
|
|
79
|
+
const v = argv[i + 1];
|
|
80
|
+
if (!v) unknown.push(a);
|
|
81
|
+
else out.template = v;
|
|
82
|
+
i += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (a.startsWith('--template=')) {
|
|
86
|
+
out.template = a.slice('--template='.length);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (a === '--pm') {
|
|
91
|
+
const v = argv[i + 1];
|
|
92
|
+
if (!v) unknown.push(a);
|
|
93
|
+
else out.pm = v;
|
|
94
|
+
i += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (a.startsWith('--pm=')) {
|
|
98
|
+
out.pm = a.slice('--pm='.length);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (a.startsWith('-')) {
|
|
103
|
+
unknown.push(a);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
positional.push(a);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (unknown.length) {
|
|
111
|
+
const msg = `Unknown option(s): ${unknown.join(', ')}`;
|
|
112
|
+
const err = new Error(msg);
|
|
113
|
+
// @ts-ignore
|
|
114
|
+
err.code = 'USAGE';
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (positional.length > 1) {
|
|
119
|
+
const err = new Error(`Too many arguments. Expected at most 1 targetDir, got: ${positional.join(' ')}`);
|
|
120
|
+
// @ts-ignore
|
|
121
|
+
err.code = 'USAGE';
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (positional.length === 1) out.targetDir = positional[0];
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isNonEmptyDir(entries) {
|
|
130
|
+
return entries.some((e) => e !== '.' && e !== '..');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function dirExists(p) {
|
|
134
|
+
return fs
|
|
135
|
+
.stat(p)
|
|
136
|
+
.then((s) => s.isDirectory())
|
|
137
|
+
.catch(() => false);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function fileExists(p) {
|
|
141
|
+
return fs
|
|
142
|
+
.stat(p)
|
|
143
|
+
.then((s) => s.isFile())
|
|
144
|
+
.catch(() => false);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function isDirEmpty(p) {
|
|
148
|
+
const entries = await fs.readdir(p).catch(() => null);
|
|
149
|
+
if (!entries) return true;
|
|
150
|
+
return !isNonEmptyDir(entries);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function toKebabCase(input) {
|
|
154
|
+
return String(input)
|
|
155
|
+
.trim()
|
|
156
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
157
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
158
|
+
.replace(/^-+|-+$/g, '')
|
|
159
|
+
.toLowerCase();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function toValidPackageName(baseName) {
|
|
163
|
+
let name = toKebabCase(baseName);
|
|
164
|
+
name = name.replace(/^[_\\.]+/, '');
|
|
165
|
+
name = name.replace(/[^a-z0-9-]+/g, '');
|
|
166
|
+
name = name.replace(/^-+|-+$/g, '');
|
|
167
|
+
if (!name) name = 'imagine-project';
|
|
168
|
+
return name;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function toTitle(input) {
|
|
172
|
+
const words = String(input)
|
|
173
|
+
.trim()
|
|
174
|
+
.replace(/[-_]+/g, ' ')
|
|
175
|
+
.replace(/\s+/g, ' ')
|
|
176
|
+
.split(' ')
|
|
177
|
+
.filter(Boolean);
|
|
178
|
+
if (!words.length) return 'Imagine Project';
|
|
179
|
+
return words.map((w) => w.slice(0, 1).toUpperCase() + w.slice(1)).join(' ');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function detectPackageManager() {
|
|
183
|
+
const ua = process.env.npm_config_user_agent ?? '';
|
|
184
|
+
const m = ua.match(/^(npm|pnpm|yarn|bun)\//);
|
|
185
|
+
return m?.[1] ?? null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function platformBin(cmd) {
|
|
189
|
+
if (process.platform !== 'win32') return cmd;
|
|
190
|
+
return `${cmd}.cmd`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function run(cmd, args, opts) {
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
const child = spawn(cmd, args, {
|
|
196
|
+
cwd: opts?.cwd ?? process.cwd(),
|
|
197
|
+
stdio: 'inherit',
|
|
198
|
+
env: process.env
|
|
199
|
+
});
|
|
200
|
+
child.on('error', reject);
|
|
201
|
+
child.on('exit', (code) => {
|
|
202
|
+
if (code === 0) resolve();
|
|
203
|
+
else reject(new Error(`${cmd} ${args.join(' ')} exited with code ${code}`));
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function copyDirContents(srcDir, destDir) {
|
|
209
|
+
const entries = await fs.readdir(srcDir, { withFileTypes: true });
|
|
210
|
+
for (const e of entries) {
|
|
211
|
+
const src = path.join(srcDir, e.name);
|
|
212
|
+
const dest = path.join(destDir, e.name);
|
|
213
|
+
await fs.cp(src, dest, { recursive: true });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function ensureTemplate(value) {
|
|
218
|
+
if (value !== 'blank' && value !== 'example') {
|
|
219
|
+
const err = new Error(`Invalid --template: ${value}. Expected "blank" or "example".`);
|
|
220
|
+
// @ts-ignore
|
|
221
|
+
err.code = 'USAGE';
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function ensurePm(value) {
|
|
228
|
+
if (value !== 'npm' && value !== 'pnpm' && value !== 'yarn' && value !== 'bun') {
|
|
229
|
+
const err = new Error(`Invalid --pm: ${value}. Expected npm|pnpm|yarn|bun.`);
|
|
230
|
+
// @ts-ignore
|
|
231
|
+
err.code = 'USAGE';
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function main() {
|
|
238
|
+
const args = parseArgs(process.argv);
|
|
239
|
+
if (args.help) {
|
|
240
|
+
usage();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
245
|
+
const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const imagineSkillsRepo = 'https://github.com/midhunxavier/imagine-skills';
|
|
249
|
+
const imagineSkillsName = 'imagine-best-practices';
|
|
250
|
+
|
|
251
|
+
const cwdBase = path.basename(process.cwd());
|
|
252
|
+
const defaultTargetDir = cwdBase && cwdBase !== path.parse(process.cwd()).root ? cwdBase : 'imagine-project';
|
|
253
|
+
|
|
254
|
+
let targetDirInput = args.targetDir;
|
|
255
|
+
if (!targetDirInput) {
|
|
256
|
+
if (args.yes || !interactive) {
|
|
257
|
+
targetDirInput = defaultTargetDir;
|
|
258
|
+
} else {
|
|
259
|
+
const answer = await rl.question(`Project directory (${defaultTargetDir}): `);
|
|
260
|
+
targetDirInput = answer.trim() || defaultTargetDir;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const targetPath = path.resolve(process.cwd(), targetDirInput);
|
|
265
|
+
const targetBaseName = path.basename(targetPath);
|
|
266
|
+
|
|
267
|
+
const template = ensureTemplate(
|
|
268
|
+
args.template ??
|
|
269
|
+
(args.yes || !interactive
|
|
270
|
+
? 'blank'
|
|
271
|
+
: ensureTemplate((await rl.question('Template (blank/example) (blank): ')).trim() || 'blank'))
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const install =
|
|
275
|
+
args.install ??
|
|
276
|
+
(args.yes || !interactive
|
|
277
|
+
? true
|
|
278
|
+
: (() => {
|
|
279
|
+
return null;
|
|
280
|
+
})());
|
|
281
|
+
|
|
282
|
+
let shouldInstall = install;
|
|
283
|
+
if (shouldInstall === null) {
|
|
284
|
+
const answer = (await rl.question('Install dependencies? (Y/n): ')).trim().toLowerCase();
|
|
285
|
+
shouldInstall = answer === '' || answer === 'y' || answer === 'yes';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let pm = args.pm ?? null;
|
|
289
|
+
if (shouldInstall) {
|
|
290
|
+
if (!pm) pm = args.yes || !interactive ? detectPackageManager() ?? 'npm' : null;
|
|
291
|
+
if (!pm && interactive) {
|
|
292
|
+
const answer = (await rl.question('Package manager (npm/pnpm/yarn/bun) (npm): ')).trim().toLowerCase();
|
|
293
|
+
pm = answer || 'npm';
|
|
294
|
+
}
|
|
295
|
+
pm = ensurePm(pm ?? 'npm');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const skills =
|
|
299
|
+
args.skills ??
|
|
300
|
+
(args.yes || !interactive
|
|
301
|
+
? false
|
|
302
|
+
: (() => {
|
|
303
|
+
return null;
|
|
304
|
+
})());
|
|
305
|
+
|
|
306
|
+
let shouldInstallSkills = skills;
|
|
307
|
+
if (shouldInstallSkills === null) {
|
|
308
|
+
const answer = (await rl.question(`Add Imagine agent skill "${imagineSkillsName}"? (y/N): `)).trim().toLowerCase();
|
|
309
|
+
shouldInstallSkills = answer === 'y' || answer === 'yes';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const existsDir = await dirExists(targetPath);
|
|
313
|
+
if (existsDir) {
|
|
314
|
+
const empty = await isDirEmpty(targetPath);
|
|
315
|
+
if (!empty) {
|
|
316
|
+
if (!args.force) {
|
|
317
|
+
throw new Error(`Target directory is not empty: ${targetPath}\nUse --force to overwrite.`);
|
|
318
|
+
}
|
|
319
|
+
await fs.rm(targetPath, { recursive: true, force: true });
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
const existsFile = await fileExists(targetPath);
|
|
323
|
+
if (existsFile) throw new Error(`Target path exists and is a file: ${targetPath}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await fs.mkdir(targetPath, { recursive: true });
|
|
327
|
+
|
|
328
|
+
const templateDir = path.resolve(__dirname, '..', 'templates', template);
|
|
329
|
+
await copyDirContents(templateDir, targetPath);
|
|
330
|
+
|
|
331
|
+
const gitignoreSrc = path.join(targetPath, 'gitignore');
|
|
332
|
+
const gitignoreDest = path.join(targetPath, '.gitignore');
|
|
333
|
+
if (await fileExists(gitignoreSrc)) {
|
|
334
|
+
await fs.rename(gitignoreSrc, gitignoreDest).catch(async (err) => {
|
|
335
|
+
if (String(err?.code ?? '') === 'EEXIST') {
|
|
336
|
+
await fs.rm(gitignoreDest, { force: true });
|
|
337
|
+
await fs.rename(gitignoreSrc, gitignoreDest);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
throw err;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const pkgPath = path.join(targetPath, 'package.json');
|
|
345
|
+
const pkgRaw = await fs.readFile(pkgPath, 'utf8');
|
|
346
|
+
const pkg = JSON.parse(pkgRaw);
|
|
347
|
+
pkg.name = toValidPackageName(targetBaseName);
|
|
348
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
349
|
+
|
|
350
|
+
const readmePath = path.join(targetPath, 'README.md');
|
|
351
|
+
if (await fileExists(readmePath)) {
|
|
352
|
+
const raw = await fs.readFile(readmePath, 'utf8');
|
|
353
|
+
const lines = raw.split(/\\r?\\n/);
|
|
354
|
+
lines[0] = `# ${toTitle(targetBaseName)}`;
|
|
355
|
+
await fs.writeFile(readmePath, lines.join('\n'), 'utf8');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (shouldInstall) {
|
|
359
|
+
const bin = platformBin(pm);
|
|
360
|
+
await run(bin, ['install'], { cwd: targetPath });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (shouldInstallSkills) {
|
|
364
|
+
const npx = platformBin('npx');
|
|
365
|
+
try {
|
|
366
|
+
await run(
|
|
367
|
+
npx,
|
|
368
|
+
['--yes', 'skills', 'add', imagineSkillsRepo, '--skill', imagineSkillsName, '-y'],
|
|
369
|
+
{ cwd: targetPath }
|
|
370
|
+
);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
// eslint-disable-next-line no-console
|
|
373
|
+
console.error('\nFailed to install skills (optional). You can run this later:\n');
|
|
374
|
+
// eslint-disable-next-line no-console
|
|
375
|
+
console.error(` npx skills add ${imagineSkillsRepo} --skill ${imagineSkillsName}\n`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const cdPath = path.isAbsolute(targetDirInput) ? targetPath : targetDirInput;
|
|
380
|
+
|
|
381
|
+
// eslint-disable-next-line no-console
|
|
382
|
+
console.log('\nDone.\n');
|
|
383
|
+
// eslint-disable-next-line no-console
|
|
384
|
+
console.log('Next steps:');
|
|
385
|
+
// eslint-disable-next-line no-console
|
|
386
|
+
console.log(` cd ${cdPath}`);
|
|
387
|
+
if (!shouldInstall) {
|
|
388
|
+
// eslint-disable-next-line no-console
|
|
389
|
+
console.log(' npm install');
|
|
390
|
+
}
|
|
391
|
+
// eslint-disable-next-line no-console
|
|
392
|
+
console.log(' npm run dev');
|
|
393
|
+
// eslint-disable-next-line no-console
|
|
394
|
+
console.log(' npm run render -- --project example');
|
|
395
|
+
// eslint-disable-next-line no-console
|
|
396
|
+
console.log(`\nOptional: npx skills add ${imagineSkillsRepo} --skill ${imagineSkillsName}`);
|
|
397
|
+
} finally {
|
|
398
|
+
if (rl) rl.close();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
main().catch((err) => {
|
|
403
|
+
if (String(err?.code ?? '') === 'USAGE') usage();
|
|
404
|
+
// eslint-disable-next-line no-console
|
|
405
|
+
console.error(err instanceof Error ? err.message : err);
|
|
406
|
+
process.exitCode = 1;
|
|
407
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-imagine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a new Imagine project (React → scientific figures).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-imagine": "bin/create-imagine.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"templates",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Imagine
|
|
2
|
+
|
|
3
|
+
React → scientific figures (PNG + SVG) with a live Studio and a Playwright renderer.
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open the Studio at http://localhost:5173
|
|
13
|
+
|
|
14
|
+
## Projects
|
|
15
|
+
|
|
16
|
+
Figures are organized into projects under `projects/<id>/`.
|
|
17
|
+
|
|
18
|
+
The Studio home shows projects. Clicking a project shows:
|
|
19
|
+
- a preview gallery (static images from `public/`)
|
|
20
|
+
- the live React-generated figures for that project
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm run list
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
List one project’s figures:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm run list -- --project example
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Render exports
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm run render -- --project example
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
By default outputs are written to `out/<projectId>/`:
|
|
39
|
+
- `out/<projectId>/<figure>--<variant>.png`
|
|
40
|
+
- `out/<projectId>/<figure>--<variant>.svg`
|
|
41
|
+
- `out/<projectId>/manifest.json`
|
|
42
|
+
|
|
43
|
+
### Render options
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Only one figure
|
|
47
|
+
npm run render -- --project example --fig line-chart
|
|
48
|
+
|
|
49
|
+
# Only one variant
|
|
50
|
+
npm run render -- --project example --fig hello-world --variant transparent
|
|
51
|
+
|
|
52
|
+
# Dev-mode rendering (requires `npm run dev` already running)
|
|
53
|
+
npm run render:dev -- --project example
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Common flags:
|
|
57
|
+
- `--project <id>`
|
|
58
|
+
- `--fig <id>` (repeatable or comma-separated)
|
|
59
|
+
- `--variant <id>` (repeatable or comma-separated)
|
|
60
|
+
- `--formats png,svg`
|
|
61
|
+
- `--out <dir>`
|
|
62
|
+
- `--mode build|dev`
|
|
63
|
+
- `--url http://localhost:5173` (dev mode)
|
|
64
|
+
- `--no-props` (ignore Studio-saved overrides)
|
|
65
|
+
|
|
66
|
+
## Text editing (Studio Controls)
|
|
67
|
+
|
|
68
|
+
When you edit figure text in Studio, it saves overrides into:
|
|
69
|
+
- `projects/<projectId>/props.json`
|
|
70
|
+
|
|
71
|
+
`npm run render` automatically uses those overrides (unless `--no-props`).
|
|
72
|
+
|
|
73
|
+
## Create a new project
|
|
74
|
+
|
|
75
|
+
1. Copy `projects/example/` to `projects/<your-id>/`
|
|
76
|
+
2. Update `projects/<your-id>/project.ts` and `projects/<your-id>/manifest.ts`
|
|
77
|
+
3. Restart `npm run dev` (project discovery is build-time)
|
|
78
|
+
|
|
79
|
+
## Create a new figure (inside a project)
|
|
80
|
+
|
|
81
|
+
1. Add a new file in `projects/<projectId>/figures/` that default-exports a React component whose root is an `<svg>`.
|
|
82
|
+
2. Register it in `projects/<projectId>/manifest.ts` with an `id`, `title`, `moduleKey`, and at least one variant.
|
|
83
|
+
3. Optionally add preview images under `public/projects/<projectId>/previews/` and reference them from `projects/<projectId>/project.ts`.
|
|
84
|
+
|
|
85
|
+
Notes:
|
|
86
|
+
- `moduleKey` must match the figure filename (without `.tsx`). Example: `projects/example/figures/line-chart.tsx` → `moduleKey: "line-chart"`.
|
|
87
|
+
- Sizes can be specified as pixels or paper-friendly `mm + dpi` (converted to px for rendering).
|
|
88
|
+
- MathJax is loaded on-demand from CDN by default. Override with `VITE_MATHJAX_URL` if needed.
|
|
89
|
+
|
|
90
|
+
## Generate example previews
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm run render:previews
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This writes PNGs to `public/projects/example/previews/`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Imagine Studio</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
13
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "imagine-project",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"list": "tsx scripts/list.ts",
|
|
11
|
+
"render": "tsx scripts/render.ts",
|
|
12
|
+
"render:dev": "tsx scripts/render.ts --mode dev",
|
|
13
|
+
"render:previews": "tsx scripts/render.ts --project example --variant default --formats png --no-props --no-manifest --out public/projects/example/previews",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"d3-array": "^3.2.4",
|
|
20
|
+
"d3-format": "^3.1.0",
|
|
21
|
+
"d3-scale": "^4.0.2",
|
|
22
|
+
"d3-shape": "^3.2.0",
|
|
23
|
+
"react": "^18.3.1",
|
|
24
|
+
"react-dom": "^18.3.1",
|
|
25
|
+
"react-router-dom": "^6.28.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/d3-array": "^3.2.2",
|
|
29
|
+
"@types/d3-format": "^3.0.4",
|
|
30
|
+
"@types/d3-scale": "^4.0.9",
|
|
31
|
+
"@types/d3-shape": "^3.1.8",
|
|
32
|
+
"@types/node": "^22.10.7",
|
|
33
|
+
"@types/react": "^18.3.12",
|
|
34
|
+
"@types/react-dom": "^18.3.1",
|
|
35
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
36
|
+
"playwright": "^1.49.1",
|
|
37
|
+
"tsx": "^4.19.2",
|
|
38
|
+
"typescript": "^5.7.2",
|
|
39
|
+
"vite": "^6.0.6",
|
|
40
|
+
"vitest": "^2.1.8"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Figure } from '@/framework/Figure';
|
|
2
|
+
import type { FigureComponentBaseProps } from '@/framework/types';
|
|
3
|
+
import { theme } from '@/framework/theme';
|
|
4
|
+
|
|
5
|
+
export default function HelloWorldFigure({
|
|
6
|
+
width,
|
|
7
|
+
height,
|
|
8
|
+
background,
|
|
9
|
+
heading = 'Imagine',
|
|
10
|
+
subtitle = 'React components → scientific figures (PNG + SVG)',
|
|
11
|
+
tipHeading = 'Tips',
|
|
12
|
+
tip1 = 'Edit the figure component and watch this update live.',
|
|
13
|
+
tip2 = 'Use the Controls panel to adjust text, then export via `npm run render`.'
|
|
14
|
+
}: FigureComponentBaseProps & {
|
|
15
|
+
heading?: string;
|
|
16
|
+
subtitle?: string;
|
|
17
|
+
tipHeading?: string;
|
|
18
|
+
tip1?: string;
|
|
19
|
+
tip2?: string;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<Figure width={width} height={height} background={background} title="Hello world">
|
|
23
|
+
<g>
|
|
24
|
+
<text x={40} y={70} fontSize={34} fontWeight={700} fill={theme.colors.text}>
|
|
25
|
+
{heading}
|
|
26
|
+
</text>
|
|
27
|
+
<text x={40} y={110} fontSize={16} fill={theme.colors.subtleText}>
|
|
28
|
+
{subtitle}
|
|
29
|
+
</text>
|
|
30
|
+
|
|
31
|
+
<rect x={40} y={150} width={width - 80} height={height - 190} rx={theme.radii.md} fill={theme.colors.panel} />
|
|
32
|
+
|
|
33
|
+
<text x={70} y={210} fontSize={16} fill={theme.colors.text} fontWeight={600}>
|
|
34
|
+
{tipHeading}
|
|
35
|
+
</text>
|
|
36
|
+
<text x={70} y={245} fontSize={14} fill={theme.colors.text}>
|
|
37
|
+
• {tip1}
|
|
38
|
+
</text>
|
|
39
|
+
<text x={70} y={270} fontSize={14} fill={theme.colors.text}>
|
|
40
|
+
• {tip2}
|
|
41
|
+
</text>
|
|
42
|
+
</g>
|
|
43
|
+
</Figure>
|
|
44
|
+
);
|
|
45
|
+
}
|