plotters-skill 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/SKILL.md ADDED
@@ -0,0 +1,535 @@
1
+ ---
2
+ name: plotters-skill
3
+ description: 'Generate charts and plots with the plotters-skill API (matplotlib pyplot-like). Use when: creating histograms, pie charts, line charts, scatter plots, bar charts, area charts, box plots, candlestick charts, subplots, or animated GIFs from data in TypeScript or JavaScript.'
4
+ argument-hint: 'Describe the chart you want, including data and style preferences'
5
+ ---
6
+
7
+ # PlottersSkill — Copilot Skill
8
+
9
+ PlottersSkill is a high-performance plotting library with a **matplotlib pyplot-like API**.
10
+ It works in both Node.js (native addon) and the browser (WASM/SVG).
11
+
12
+ ## API Reference
13
+
14
+ ### Top-level functions
15
+
16
+ | Function | pyplot equivalent | Description |
17
+ |----------|-------------------|-------------|
18
+ | `figure()` | `plt.figure()` | Create a single figure |
19
+ | `axes()` | `fig, ax = plt.subplots()` | Create flexible Cartesian axes (mix line, scatter, bar, step, area) |
20
+ | `subplots(nrows, ncols)` | `plt.subplots(nrows, ncols)` | Create a uniform NxM grid of figures |
21
+ | `gridFigure()` | `plt.subplot_mosaic()` | Create a flexible grid where each row can have different column counts |
22
+
23
+ ### Creating a figure
24
+
25
+ ```typescript
26
+ import { figure } from 'plotters-skill';
27
+
28
+ const fig = figure();
29
+ ```
30
+
31
+ ### Axes — flexible mixed-series chart
32
+
33
+ `axes()` creates a single Cartesian chart on which you can freely mix `plot`,
34
+ `scatter`, `bar`, `step`, and `fillBetween` — like matplotlib's `Axes` object.
35
+
36
+ ```typescript
37
+ import { axes } from 'plotters-skill';
38
+
39
+ const ax = axes();
40
+ ax.figsize(900, 540);
41
+ ax.title('Mixed Series');
42
+ ax.xlabel('x');
43
+ ax.ylabel('y');
44
+
45
+ // Line series
46
+ ax.plot([0, 1, 2, 3, 4], [10, 25, 18, 30, 22], {
47
+ color: 'steelblue', lineWidth: 2, label: 'Trend',
48
+ });
49
+
50
+ // Scatter series
51
+ ax.scatter([0, 1, 2, 3, 4], [12, 20, 22, 28, 25], {
52
+ color: 'coral', markerSize: 6, label: 'Actual',
53
+ });
54
+
55
+ // Vertical bars
56
+ ax.bar([0, 1, 2, 3, 4], [8, 18, 14, 24, 20], {
57
+ color: 'green', barWidth: 0.6, label: 'Sales',
58
+ });
59
+
60
+ // Step function
61
+ ax.step([0, 1, 2, 3, 4], [9, 22, 16, 28, 21], {
62
+ color: 'orange', lineWidth: 2, label: 'Threshold',
63
+ });
64
+
65
+ // Filled area
66
+ ax.fillBetween([0, 1, 2, 3, 4], [15, 30, 22, 35, 28], {
67
+ y2: 5, alpha: 0.2, color: 'cyan', label: 'Range',
68
+ });
69
+
70
+ ax.savefig('mixed.png');
71
+ ```
72
+
73
+ #### Axes methods
74
+
75
+ | Method | pyplot equivalent | Description |
76
+ |--------|-------------------|-------------|
77
+ | `ax.plot(x, y, opts?)` | `ax.plot()` | Line series |
78
+ | `ax.scatter(x, y, opts?)` | `ax.scatter()` | Scatter / point markers |
79
+ | `ax.bar(x, heights, opts?)` | `ax.bar()` | Vertical bar chart |
80
+ | `ax.step(x, y, opts?)` | `ax.step()` | Step function |
81
+ | `ax.fillBetween(x, y1, opts?)` | `ax.fill_between()` | Filled area |
82
+ | `ax.xlim(min, max)` | `ax.set_xlim()` | Fix x-axis range |
83
+ | `ax.ylim(min, max)` | `ax.set_ylim()` | Fix y-axis range |
84
+ | `ax.margin(px)` | `ChartBuilder.margin()` | Chart margin in pixels |
85
+ | `ax.xLabelAreaSize(px)` | — | X-axis label area size |
86
+ | `ax.yLabelAreaSize(px)` | — | Y-axis label area size |
87
+ | `ax.grid(show)` | `ax.grid()` | Show/hide mesh grid |
88
+ | `ax.legend(show)` | `ax.legend()` | Show/hide legend |
89
+ | `ax.clear()` | `ax.cla()` | Clear all series |
90
+
91
+ #### ChartBuilder configuration
92
+
93
+ These methods directly expose plotters' `ChartBuilder` API for maximum
94
+ flexibility over chart layout.
95
+
96
+ | Method | plotters equivalent | Description |
97
+ |--------|---------------------|-------------|
98
+ | `ax.buildCartesian2d(xMin, xMax, yMin, yMax)` | `ChartBuilder.build_cartesian_2d()` | Set axis ranges (alternative to xlim+ylim) |
99
+ | `ax.marginTop(px)` | `ChartBuilder.margin_top()` | Top margin |
100
+ | `ax.marginBottom(px)` | `ChartBuilder.margin_bottom()` | Bottom margin |
101
+ | `ax.marginLeft(px)` | `ChartBuilder.margin_left()` | Left margin |
102
+ | `ax.marginRight(px)` | `ChartBuilder.margin_right()` | Right margin |
103
+ | `ax.topXLabelAreaSize(px)` | `ChartBuilder.top_x_label_area_size()` | Top X-axis label area size |
104
+ | `ax.rightYLabelAreaSize(px)` | `ChartBuilder.right_y_label_area_size()` | Right Y-axis label area size |
105
+ | `ax.captionFontSize(size)` | `ChartBuilder.caption(..., font_size)` | Caption/title font size |
106
+ | `ax.captionFontFamily(family)` | `ChartBuilder.caption(..., family)` | Caption/title font family |
107
+
108
+ #### `configureMesh(options)` — grid & axis appearance
109
+
110
+ Maps directly to plotters' `chart.configure_mesh()`. Pass an options object
111
+ with any subset of fields:
112
+
113
+ ```typescript
114
+ ax.configureMesh({
115
+ xLabels: 10, // number of x-axis labels
116
+ yLabels: 5, // number of y-axis labels
117
+ disableXMesh: false, // hide x grid lines
118
+ disableYMesh: true, // hide y grid lines
119
+ disableAxes: false, // hide all axes
120
+ disableXAxis: false, // hide x axis
121
+ disableYAxis: false, // hide y axis
122
+ boldLineColor: '#333', // bold grid line colour
123
+ boldLineAlpha: 0.4, // bold grid line opacity
124
+ lightLineColor: '#ccc', // light grid line colour
125
+ lightLineAlpha: 0.1, // light grid line opacity
126
+ axisColor: '#000', // axis line colour
127
+ labelFontSize: 14, // axis tick label size
128
+ axisDescFontSize: 16, // axis description font size
129
+ xLabelFontSize: 12, // x-axis label font size (overrides labelFontSize)
130
+ yLabelFontSize: 12, // y-axis label font size (overrides labelFontSize)
131
+ tickMarkSize: 5, // tick mark size in pixels
132
+ xMaxLightLines: 10, // max minor grid lines between x-axis labels
133
+ yMaxLightLines: 10, // max minor grid lines between y-axis labels
134
+ });
135
+ ```
136
+
137
+ #### `configureSeriesLabels(options)` — legend style
138
+
139
+ Maps directly to plotters' `chart.configure_series_labels()`:
140
+
141
+ ```typescript
142
+ ax.configureSeriesLabels({
143
+ position: 'upper-left', // UpperLeft, UpperRight, LowerLeft, LowerRight, etc.
144
+ backgroundColor: '#ffffff', // legend box background colour
145
+ backgroundAlpha: 0.9, // legend box opacity
146
+ borderColor: '#000000', // legend box border colour
147
+ fontSize: 14, // legend label font size
148
+ margin: 10, // margin around legend box
149
+ legendAreaSize: 30, // size of the legend marker area
150
+ });
151
+ ```
152
+
153
+ Valid positions: `upper-left`, `upper-right`, `lower-left`, `lower-right`,
154
+ `upper-middle`, `middle-left`, `middle-right`, `lower-middle`, `middle`.
155
+
156
+ ### Histogram — `fig.hist(data, options?)`
157
+
158
+ Call `hist()` multiple times to overlay datasets on the same axes — just like
159
+ `ax.hist()` in matplotlib. Each call auto-cycles through the tab10 palette
160
+ (C0 blue, C1 orange, C2 green, …) unless you set `color` explicitly.
161
+
162
+ ```typescript
163
+ // Single histogram
164
+ fig.hist([1, 2, 2, 3, 3, 3, 4, 5], {
165
+ bins: 10, // number of bins (default: 10)
166
+ color: 'steelblue', // bar colour: name, CSS name, or hex '#RRGGBB'
167
+ alpha: 0.7, // opacity 0.0–1.0
168
+ label: 'values', // legend label
169
+ rangeMin: 0, // clip data range min
170
+ rangeMax: 100, // clip data range max
171
+ });
172
+ ```
173
+
174
+ ```typescript
175
+ // Overlaid histograms — colors auto-cycle (C0, C1, …)
176
+ const fig = figure();
177
+ fig.hist(datasetA, { bins: 20, alpha: 0.5, label: 'Dataset A' }); // C0 blue
178
+ fig.hist(datasetB, { bins: 20, alpha: 0.5, label: 'Dataset B' }); // C1 orange
179
+ fig.hist(datasetC, { bins: 20, alpha: 0.5, label: 'Dataset C' }); // C2 green
180
+ fig.title('Distribution Comparison');
181
+ fig.xlabel('Value');
182
+ fig.ylabel('Frequency');
183
+ fig.savefig('comparison.png');
184
+ ```
185
+
186
+ ### Pie / Donut — `fig.pie(values, options?)`
187
+
188
+ ```typescript
189
+ fig.pie([30, 25, 20, 15, 10], {
190
+ labels: ['A', 'B', 'C', 'D', 'E'],
191
+ colors: ['steelblue', 'coral', 'gold', 'teal', 'salmon'],
192
+ startAngle: 90, // starting angle in degrees
193
+ donutHole: 60, // inner radius for donut chart
194
+ labelOffset: 1.2, // label offset relative to radius
195
+ showPercentages: true, // draw percentage labels inside slices
196
+ });
197
+ ```
198
+
199
+ ### Box Plot — `fig.boxplot(datasets, options?)`
200
+
201
+ Similar to `plt.boxplot(x)` in matplotlib. Each inner array produces one box
202
+ showing median, quartiles, whiskers, and optional outlier points.
203
+
204
+ ```typescript
205
+ fig.boxplot(
206
+ [
207
+ [72, 75, 78, 80, 82, 85, 87, 90, 92, 95, 98],
208
+ [55, 60, 63, 65, 68, 70, 72, 74, 76, 78, 80],
209
+ [88, 90, 91, 93, 94, 95, 96, 97, 98, 99, 100],
210
+ ],
211
+ {
212
+ tickLabels: ['Midterm', 'Quiz', 'Final'], // category axis labels
213
+ color: 'steelblue', // box colour (auto-cycles if omitted)
214
+ label: 'Scores', // legend label
215
+ showMeans: true, // show mean marker (cross), like showmeans=True
216
+ showFliers: true, // show outlier points, like showfliers=True
217
+ },
218
+ );
219
+ ```
220
+
221
+ ### Area Chart — `fig.fillBetween(x, y1, options?)`
222
+
223
+ Similar to `plt.fill_between(x, y1, y2=0)` in matplotlib. Fills the region
224
+ between a curve and a baseline. Call multiple times to overlay areas.
225
+
226
+ ```typescript
227
+ // Single area
228
+ fig.fillBetween(
229
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
230
+ [10, 15, 22, 35, 50, 72, 95, 130, 170, 220],
231
+ {
232
+ y2: 0, // baseline value (default: 0)
233
+ color: 'steelblue', // fill colour (auto-cycles if omitted)
234
+ alpha: 0.35, // fill opacity 0.0–1.0 (default: 0.3)
235
+ label: 'Revenue', // legend label
236
+ },
237
+ );
238
+ ```
239
+
240
+ ```typescript
241
+ // Overlapping areas — colors auto-cycle (C0, C1, …)
242
+ const fig = figure();
243
+ fig.fillBetween(x, seriesA, { alpha: 0.3, label: 'Product A' });
244
+ fig.fillBetween(x, seriesB, { alpha: 0.3, label: 'Product B' });
245
+ fig.title('Sales Comparison');
246
+ fig.xlabel('Month');
247
+ fig.ylabel('Revenue');
248
+ fig.savefig('area-comparison.png');
249
+ ```
250
+
251
+ ### Candlestick — `fig.candlestick(data, options?)`
252
+
253
+ Similar to `mplfinance.plot(df, type='candle')`. Each inner array has 4 values:
254
+ `[open, high, low, close]`. Bullish candles (close >= open) are drawn in the up
255
+ colour, bearish candles in the down colour.
256
+
257
+ ```typescript
258
+ fig.candlestick(
259
+ [
260
+ [115.34, 117.25, 114.59, 115.91],
261
+ [116.17, 117.61, 116.05, 117.57],
262
+ [118.09, 118.44, 116.99, 117.65],
263
+ [117.39, 118.75, 116.71, 117.52],
264
+ [117.14, 120.82, 117.09, 120.22],
265
+ ],
266
+ {
267
+ labels: ['Mar 15', 'Mar 18', 'Mar 19', 'Mar 20', 'Mar 21'], // x-axis date labels
268
+ upColor: 'green', // bullish candle colour (default: teal)
269
+ downColor: 'red', // bearish candle colour (default: red)
270
+ width: 15, // candle body width in pixels
271
+ label: 'MSFT', // legend label
272
+ },
273
+ );
274
+ ```
275
+
276
+ ### Line Chart — `fig.plot(x, y, options?)`
277
+
278
+ Similar to `plt.plot(x, y)`. Call `plot()` multiple times to overlay lines on
279
+ the same axes — each call auto-cycles through the tab10 palette unless you set
280
+ `color` explicitly.
281
+
282
+ ```typescript
283
+ // Single line
284
+ fig.plot([0, 1, 2, 3, 4], [10, 25, 18, 30, 22], {
285
+ color: 'steelblue', // line colour (auto-cycles if omitted)
286
+ lineWidth: 2, // line width in pixels (default: 2)
287
+ label: 'Revenue', // legend label
288
+ });
289
+ ```
290
+
291
+ ```typescript
292
+ // Multi-series overlay — colours auto-cycle (C0, C1, C2)
293
+ const fig = figure();
294
+ fig.plot(months, seriesA, { label: 'Product A' });
295
+ fig.plot(months, seriesB, { label: 'Product B' });
296
+ fig.plot(months, seriesC, { label: 'Product C' });
297
+ fig.title('Sales Trend');
298
+ fig.xlabel('Month');
299
+ fig.ylabel('Sales');
300
+ fig.savefig('multi-line.png');
301
+ ```
302
+
303
+ ### Clearing a figure — `fig.clear()`
304
+
305
+ Removes all plot elements (histograms, pies, lines, etc.) while keeping the
306
+ title, axis labels, and dimensions. Use this for real-time/animated charts.
307
+
308
+ ```typescript
309
+ fig.clear();
310
+ fig.plot(newX, newY, { color: 'blue' });
311
+ fig.savefig('updated.png');
312
+ ```
313
+
314
+ ### Animated GIF — `fig.savegif(path, options)` (Node.js only)
315
+
316
+ Similar to `matplotlib.animation.FuncAnimation` + `writer='pillow'`. Each frame
317
+ is an array of `{ x, y }` line series rendered as a GIF animation frame.
318
+
319
+ ```typescript
320
+ const allX = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
321
+ const allY = [10, 25, 18, 30, 22, 45, 35, 50, 40, 55];
322
+
323
+ // Progressive reveal: each frame adds one more data point
324
+ const frames = [];
325
+ for (let i = 2; i <= allX.length; i++) {
326
+ frames.push([{ x: allX.slice(0, i), y: allY.slice(0, i) }]);
327
+ }
328
+
329
+ const fig = figure();
330
+ fig.figsize(640, 400);
331
+ fig.title('Animated Line');
332
+ fig.xlabel('Time');
333
+ fig.ylabel('Value');
334
+ fig.savegif('animation.gif', { frames, delayMs: 200 });
335
+ ```
336
+
337
+ ### Canvas animation (WASM / browser)
338
+
339
+ In the browser, drive animation with `requestAnimationFrame`. Use `clear()`
340
+ and `plot()` each frame — no special animation API is needed.
341
+
342
+ ```typescript
343
+ const fig = figure();
344
+ fig.figsize(800, 400);
345
+ fig.title('Real-Time Monitor');
346
+
347
+ let i = 2;
348
+ function animate() {
349
+ fig.clear();
350
+ fig.plot(x.slice(0, i), y.slice(0, i), { color: 'blue', label: 'CPU' });
351
+ fig.draw(canvas);
352
+ i++;
353
+ if (i <= x.length) requestAnimationFrame(animate);
354
+ }
355
+ animate();
356
+ ```
357
+
358
+ ### Setting labels and title
359
+
360
+ ```typescript
361
+ fig.title('My Chart');
362
+ fig.xlabel('Value');
363
+ fig.ylabel('Frequency');
364
+ fig.figsize(1024, 768); // width × height in pixels
365
+ ```
366
+
367
+ ### Axis ranges, margin, grid — ChartBuilder wrapper
368
+
369
+ These methods expose plotters' `ChartBuilder` flexibility through a pyplot-like
370
+ API. They correspond to `ax.set_xlim()`, `ax.set_ylim()`, and `ax.grid()` in
371
+ matplotlib.
372
+
373
+ ```typescript
374
+ fig.xlim(-1, 1); // fix x-axis range (like ax.set_xlim)
375
+ fig.ylim(-0.1, 1); // fix y-axis range (like ax.set_ylim)
376
+ fig.margin(5); // chart margin in pixels (plotters ChartBuilder.margin)
377
+ fig.grid(false); // hide mesh grid lines (like ax.grid(False))
378
+ ```
379
+
380
+ Example — the classic y = x² chart from the plotters ChartBuilder docs:
381
+
382
+ ```typescript
383
+ const fig = figure();
384
+ fig.figsize(800, 600);
385
+ fig.title('y = x²');
386
+ fig.xlim(-1, 1);
387
+ fig.ylim(-0.1, 1);
388
+ fig.xlabel('x');
389
+ fig.ylabel('y');
390
+
391
+ const x = Array.from({ length: 101 }, (_, i) => -1 + i * 0.02);
392
+ const y = x.map(v => v * v);
393
+ fig.plot(x, y, { color: 'red', label: 'y = x²' });
394
+ fig.savefig('quadratic.png');
395
+ ```
396
+
397
+ Settings persist through `clear()` calls — ideal for fixed-axis animations:
398
+
399
+ ```typescript
400
+ const fig = figure();
401
+ fig.ylim(0, 100); // set once, kept across frames
402
+ function animate() {
403
+ fig.clear();
404
+ fig.plot(newX, newY);
405
+ fig.draw(canvas);
406
+ requestAnimationFrame(animate);
407
+ }
408
+ ```
409
+
410
+ ### Saving output (Node.js)
411
+
412
+ ```typescript
413
+ fig.savefig('output.png');
414
+ ```
415
+
416
+ ### Subplots — uniform grid
417
+
418
+ Similar to `fig, axs = plt.subplots(nrows, ncols)` in matplotlib.
419
+
420
+ **Important:** `ax()` extracts the figure from the grid. Configure it, then
421
+ put it back with `setAx()`. This is required because of Rust ownership rules
422
+ across the FFI boundary.
423
+
424
+ ```typescript
425
+ import { subplots } from 'plotters-skill';
426
+
427
+ const grid = subplots(2, 2);
428
+ grid.figsize(1200, 800);
429
+ grid.suptitle('2×2 Grid');
430
+
431
+ let ax = grid.ax(0, 0);
432
+ ax.title('Distribution');
433
+ ax.hist(data, { bins: 15, color: 'steelblue' });
434
+ grid.setAx(0, 0, ax);
435
+
436
+ ax = grid.ax(0, 1);
437
+ ax.pie([30, 25, 20], { labels: ['A', 'B', 'C'] });
438
+ grid.setAx(0, 1, ax);
439
+
440
+ ax = grid.ax(1, 0);
441
+ ax.hist(otherData, { bins: 10, color: 'coral' });
442
+ grid.setAx(1, 0, ax);
443
+
444
+ ax = grid.ax(1, 1);
445
+ ax.pie([50, 50], { labels: ['Yes', 'No'] });
446
+ grid.setAx(1, 1, ax);
447
+
448
+ grid.savefig('grid.png');
449
+ ```
450
+
451
+ ### GridFigure — flexible row layout
452
+
453
+ Similar to `plt.subplot_mosaic()` — each row can have a different number of
454
+ columns.
455
+
456
+ ```typescript
457
+ import { gridFigure } from 'plotters-skill';
458
+
459
+ const grid = gridFigure();
460
+ grid.addRow(2); // row 0: two columns
461
+ grid.addRow(1); // row 1: one full-width column
462
+ grid.figsize(1200, 800);
463
+ grid.suptitle('Mixed Layout');
464
+
465
+ let ax = grid.ax(0, 0);
466
+ ax.hist(data, { bins: 10 });
467
+ grid.setAx(0, 0, ax);
468
+
469
+ ax = grid.ax(0, 1);
470
+ ax.pie(values, { labels: ['A', 'B', 'C'] });
471
+ grid.setAx(0, 1, ax);
472
+
473
+ ax = grid.ax(1, 0);
474
+ ax.hist(otherData, { bins: 20, color: 'coral' });
475
+ grid.setAx(1, 0, ax);
476
+
477
+ grid.savefig('grid.png');
478
+ ```
479
+
480
+ ### Named colours
481
+
482
+ Supports matplotlib tab10 palette (`c0`–`c9`, `blue`, `orange`, `green`, `red`,
483
+ `purple`, `brown`, `pink`, `gray`, `olive`, `cyan`), common CSS names
484
+ (`steelblue`, `coral`, `tomato`, `gold`, `navy`, `teal`, `salmon`, `black`,
485
+ `white`), and hex codes (`#4682B4`).
486
+
487
+ ## Full example — histogram + pie in a grid
488
+
489
+ ```typescript
490
+ import { subplots } from 'plotters-skill';
491
+
492
+ const data: number[] = Array.from({ length: 1000 }, () => Math.random() * 100);
493
+
494
+ const grid = subplots(1, 2);
495
+ grid.figsize(1200, 500);
496
+ grid.suptitle('Dashboard');
497
+
498
+ let ax = grid.ax(0, 0);
499
+ ax.title('Random Distribution');
500
+ ax.xlabel('Value');
501
+ ax.ylabel('Count');
502
+ ax.hist(data, { bins: 25, color: 'steelblue', alpha: 0.8 });
503
+ grid.setAx(0, 0, ax);
504
+
505
+ ax = grid.ax(0, 1);
506
+ ax.title('Categories');
507
+ ax.pie([40, 30, 20, 10], {
508
+ labels: ['Web', 'Mobile', 'Desktop', 'Other'],
509
+ colors: ['steelblue', 'coral', 'gold', 'teal'],
510
+ });
511
+ grid.setAx(0, 1, ax);
512
+
513
+ grid.savefig('dashboard.png');
514
+ ```
515
+
516
+ ## CLI Usage
517
+
518
+ ```bash
519
+ # Install skill for all AI tools (Copilot, Claude, generic agents)
520
+ npx plotters-skill install-skill
521
+
522
+ # Install for a specific tool only
523
+ npx plotters-skill install-skill --target copilot # → .github/skills/plotters-skill/SKILL.md
524
+ npx plotters-skill install-skill --target claude # → .claude/skills/plotters-skill/SKILL.md
525
+ npx plotters-skill install-skill --target agents # → .agents/skills/plotters-skill/SKILL.md
526
+
527
+ # Install into a different project directory
528
+ npx plotters-skill install-skill /path/to/project
529
+
530
+ # Run a script with the addon pre-imported
531
+ npx plotters-skill eval my_chart.js
532
+
533
+ # Run inline code
534
+ npx plotters-skill eval-inline "const fig = figure(); fig.hist([1,2,3]); fig.savefig('out.png');"
535
+ ```
package/index.js ADDED
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ // plotters-skill CLI
3
+ // Commands:
4
+ // install-skill [--target <tool>] [dir] Install SKILL.md for AI coding tools
5
+ // eval <file.js> Run a JS file with the addon pre-imported
6
+ // eval-inline <code> Run inline JS code with the addon pre-imported
7
+
8
+ const { execFileSync } = require('node:child_process');
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+ const os = require('node:os');
12
+
13
+ const PKG_ROOT = path.resolve(__dirname);
14
+ const SKILL_SRC = path.join(PKG_ROOT, 'SKILL.md');
15
+
16
+ // The preamble that gets prepended to inline scripts and eval'd files.
17
+ // It imports the native addon so user code can call figure(), etc. directly.
18
+ const PREAMBLE = `const { figure, JsFigure } = require(${JSON.stringify(
19
+ path.join(PKG_ROOT, 'node', 'index.js')
20
+ )});\n`;
21
+
22
+ // Where each AI tool looks for skill files (relative to project root).
23
+ const SKILL_TARGETS = {
24
+ copilot: '.github/skills/plotters-skill',
25
+ claude: '.claude/skills/plotters-skill',
26
+ agents: '.agents/skills/plotters-skill',
27
+ };
28
+ const ALL_TARGETS = Object.keys(SKILL_TARGETS);
29
+
30
+ function usage() {
31
+ console.log(`
32
+ plotters-skill CLI
33
+
34
+ Usage:
35
+ plotters-skill install-skill [options] [dir]
36
+ Install SKILL.md so AI tools can discover the plotters-skill API.
37
+
38
+ dir Project root directory (default: current working directory)
39
+
40
+ Options:
41
+ --target <tool> Install for a specific tool: copilot, claude, agents, or all (default: all)
42
+ copilot → .github/skills/plotters-skill/SKILL.md
43
+ claude → .claude/skills/plotters-skill/SKILL.md
44
+ agents → .agents/skills/plotters-skill/SKILL.md
45
+ all → installs for all three
46
+
47
+ plotters-skill eval <file.js> Execute a JS file with the addon pre-imported
48
+ plotters-skill eval-inline <code> Execute inline JS code with the addon pre-imported
49
+ plotters-skill --help Show this help message
50
+ `.trim());
51
+ }
52
+
53
+ function installSkill(args) {
54
+ if (!fs.existsSync(SKILL_SRC)) {
55
+ console.error('Error: SKILL.md not found in package.');
56
+ process.exit(1);
57
+ }
58
+
59
+ let target = 'all';
60
+ let dir = process.cwd();
61
+
62
+ // Parse arguments
63
+ for (let i = 0; i < args.length; i++) {
64
+ if (args[i] === '--target' && args[i + 1]) {
65
+ target = args[++i];
66
+ } else if (!args[i].startsWith('-')) {
67
+ dir = path.resolve(args[i]);
68
+ }
69
+ }
70
+
71
+ const targets = target === 'all' ? ALL_TARGETS : [target];
72
+ for (const t of targets) {
73
+ const relDir = SKILL_TARGETS[t];
74
+ if (!relDir) {
75
+ console.error(`Error: unknown target '${t}'. Choose from: ${ALL_TARGETS.join(', ')}, all`);
76
+ process.exit(1);
77
+ }
78
+ const destDir = path.join(dir, relDir);
79
+ fs.mkdirSync(destDir, { recursive: true });
80
+ const dest = path.join(destDir, 'SKILL.md');
81
+ fs.copyFileSync(SKILL_SRC, dest);
82
+ console.log(` ✅ ${t}: ${path.relative(dir, dest)}`);
83
+ }
84
+ console.log('\nDone! AI tools will discover plotters-skill automatically.');
85
+ }
86
+
87
+ function findNode() {
88
+ return process.execPath; // re-use the current Node.js binary
89
+ }
90
+
91
+ function evalFile(filePath) {
92
+ if (!filePath) {
93
+ console.error('Error: eval requires a file path.');
94
+ process.exit(1);
95
+ }
96
+ const resolved = path.resolve(filePath);
97
+ if (!fs.existsSync(resolved)) {
98
+ console.error(`Error: file not found: ${resolved}`);
99
+ process.exit(1);
100
+ }
101
+ const userCode = fs.readFileSync(resolved, 'utf-8');
102
+ runCode(PREAMBLE + userCode, resolved);
103
+ }
104
+
105
+ function evalInline(code) {
106
+ if (!code) {
107
+ console.error('Error: eval-inline requires a code string.');
108
+ process.exit(1);
109
+ }
110
+ runCode(PREAMBLE + code, '<inline>');
111
+ }
112
+
113
+ function runCode(fullCode, label) {
114
+ const tmpFile = path.join(
115
+ os.tmpdir(),
116
+ `plotters-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.cjs`
117
+ );
118
+ try {
119
+ fs.writeFileSync(tmpFile, fullCode, 'utf-8');
120
+ const node = findNode();
121
+ execFileSync(node, [tmpFile], { stdio: 'inherit' });
122
+ } catch (err) {
123
+ if (err.status) process.exit(err.status);
124
+ throw err;
125
+ } finally {
126
+ try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
127
+ }
128
+ }
129
+
130
+ // --- main ---
131
+ const args = process.argv.slice(2);
132
+ const cmd = args[0];
133
+
134
+ switch (cmd) {
135
+ case 'install-skill':
136
+ installSkill(args.slice(1));
137
+ break;
138
+ case 'eval':
139
+ evalFile(args[1]);
140
+ break;
141
+ case 'eval-inline':
142
+ evalInline(args.slice(1).join(' '));
143
+ break;
144
+ case '--help':
145
+ case '-h':
146
+ case undefined:
147
+ usage();
148
+ break;
149
+ default:
150
+ console.error(`Unknown command: ${cmd}`);
151
+ usage();
152
+ process.exit(1);
153
+ }