@maka/maka-cli 5.384.0 → 5.385.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/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +77 -9
- package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-geometry.js +25 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.385.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
|
|
6
6
|
"description": "A command line tool for scaffolding Meteor 3.x applications using React.",
|
|
@@ -7,9 +7,9 @@ import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
|
7
7
|
import { describeFledTarget } from '../utilities/pursuit.js';
|
|
8
8
|
import { crossPlaneRefusal } from '../utilities/planes.js';
|
|
9
9
|
import { spotOf, ensureAtSpot, furnitureCells, standingOccupants } from '../utilities/spots.js';
|
|
10
|
-
import { key as gridKey } from '../utilities/room-grid.js';
|
|
10
|
+
import { key as gridKey, METERS_PER_CELL } from '../utilities/room-grid.js';
|
|
11
11
|
import { isSolid, isVoid, roomAtCell, wallsBetween } from '../utilities/district-plane.js';
|
|
12
|
-
import { buildBarrier, clipFaces, formatCoord, isCoordToken, parseCoord, planDirectionalWall, planDome, planWall } from '../utilities/barrier-geometry.js';
|
|
12
|
+
import { DIRECTIONAL_WALL_FORCE, DOME_FORCE, forceForWall, buildBarrier, clipFaces, formatCoord, isCoordToken, parseCoord, planDirectionalWall, planDome, planWall } from '../utilities/barrier-geometry.js';
|
|
13
13
|
import { NPC } from '../models/npc.js';
|
|
14
14
|
import { spellFor, catalogSpells } from '../utilities/catalog.js';
|
|
15
15
|
import { getAspect } from '../aspects.js';
|
|
@@ -307,6 +307,7 @@ export class CastCommand extends BypassCommand {
|
|
|
307
307
|
const spellDef = spellFor(spellName);
|
|
308
308
|
const safeForce = this.actor.magic > 0 ? this.actor.magic : 4;
|
|
309
309
|
let force = Math.max(1, -(spellDef?.drainOffset ?? -3));
|
|
310
|
+
let forceGiven = true;
|
|
310
311
|
if (rest[0]?.toLowerCase() === 'force' && /^\d+$/.test(rest[1] ?? '')) {
|
|
311
312
|
force = parseInt(rest[1], 10);
|
|
312
313
|
rest = rest.slice(2);
|
|
@@ -315,6 +316,9 @@ export class CastCommand extends BypassCommand {
|
|
|
315
316
|
force = parseInt(rest[0], 10);
|
|
316
317
|
rest = rest.slice(1);
|
|
317
318
|
}
|
|
319
|
+
else {
|
|
320
|
+
forceGiven = false;
|
|
321
|
+
}
|
|
318
322
|
if (force < 1) {
|
|
319
323
|
return `Force below 1 isn't a spell, it's a wish.`;
|
|
320
324
|
}
|
|
@@ -335,7 +339,7 @@ export class CastCommand extends BypassCommand {
|
|
|
335
339
|
case 'heal': return this.castHeal(force, overcast, rest);
|
|
336
340
|
case 'armor': return this.castArmor(force, overcast, rest);
|
|
337
341
|
case 'sustained-utility': return this.castSustainedUtility(spellName, force, overcast, rest);
|
|
338
|
-
case 'barrier': return this.castBarrier(spellName, force, overcast, rest);
|
|
342
|
+
case 'barrier': return this.castBarrier(spellName, force, overcast, rest, forceGiven ? undefined : safeForce);
|
|
339
343
|
}
|
|
340
344
|
}
|
|
341
345
|
/**
|
|
@@ -980,7 +984,8 @@ export class CastCommand extends BypassCommand {
|
|
|
980
984
|
* cast barrier [F] <x,y[,z]> <x,y[,z]> a wall between two points
|
|
981
985
|
* cast barrier [F] <x,y[,z]> a dome around one point
|
|
982
986
|
* cast barrier [F] <direction> [n] a 1x3 wall n cells that way
|
|
983
|
-
* cast barrier [F] at <someone>
|
|
987
|
+
* cast barrier [F] at <someone> [size] a dome around them ("me"/"self": you);
|
|
988
|
+
* size = radius in cells = Force / 2
|
|
984
989
|
* cast barrier [F] a dome around yourself
|
|
985
990
|
* cast barrier off let it go (free)
|
|
986
991
|
*
|
|
@@ -993,7 +998,9 @@ export class CastCommand extends BypassCommand {
|
|
|
993
998
|
* Bare "cast north" no longer reaches a ward from here -- that is
|
|
994
999
|
* "dispel north" (user ruling 2026-10-05).
|
|
995
1000
|
*/
|
|
996
|
-
async castBarrier(spellKey, force, overcast, rest
|
|
1001
|
+
async castBarrier(spellKey, force, overcast, rest,
|
|
1002
|
+
/** Set when no Force was named: the talent a sized cast may reach. */
|
|
1003
|
+
autoUpTo) {
|
|
997
1004
|
const actor = this.actor;
|
|
998
1005
|
const mana = spellKey === 'manabarrier';
|
|
999
1006
|
const kind = mana ? 'mana' : 'physical';
|
|
@@ -1028,11 +1035,12 @@ export class CastCommand extends BypassCommand {
|
|
|
1028
1035
|
let plan;
|
|
1029
1036
|
let around;
|
|
1030
1037
|
let center;
|
|
1031
|
-
const usage = `"cast ${spellKey} [force] <x,y> <x,y>" (a wall), "cast ${spellKey} [force] <x,y>" (a dome), "cast ${spellKey} [force] <direction> [cells]", or "cast ${spellKey} at <someone>". The map rulers give x and y; you're at ${formatCoord(from)}.`;
|
|
1038
|
+
const usage = `"cast ${spellKey} [force] <x,y> <x,y>" (a wall), "cast ${spellKey} [force] <x,y>" (a dome), "cast ${spellKey} [force] <direction> [cells]", or "cast ${spellKey} at <someone|me> [radius in cells]". Leave the Force off and it is sized for you. The map rulers give x and y; you're at ${formatCoord(from)}.`;
|
|
1032
1039
|
const unseen = (what) => `You can't see ${what} from here -- a barrier is cast at what you can see (LOS, p.294).`;
|
|
1033
1040
|
if (words.length === 0) {
|
|
1034
1041
|
center = from;
|
|
1035
1042
|
around = 'you';
|
|
1043
|
+
force = this.domeForce(force, autoUpTo);
|
|
1036
1044
|
plan = planDome(from, force);
|
|
1037
1045
|
}
|
|
1038
1046
|
else if (isCoordToken(words[0])) {
|
|
@@ -1041,6 +1049,10 @@ export class CastCommand extends BypassCommand {
|
|
|
1041
1049
|
const a = parseCoord(words[0], from.z);
|
|
1042
1050
|
if (words.length === 2) {
|
|
1043
1051
|
const b = parseCoord(words[1], from.z);
|
|
1052
|
+
const sized = this.sizeForce(spellKey, force, forceForWall(a, b), autoUpTo);
|
|
1053
|
+
if (typeof sized === 'string')
|
|
1054
|
+
return sized;
|
|
1055
|
+
force = sized;
|
|
1044
1056
|
plan = planWall(a, b, force);
|
|
1045
1057
|
if (plan.error)
|
|
1046
1058
|
return plan.error;
|
|
@@ -1050,6 +1062,7 @@ export class CastCommand extends BypassCommand {
|
|
|
1050
1062
|
}
|
|
1051
1063
|
else {
|
|
1052
1064
|
center = a;
|
|
1065
|
+
force = this.domeForce(force, autoUpTo);
|
|
1053
1066
|
plan = planDome(a, force);
|
|
1054
1067
|
if (!this.inSight(plane, from, a))
|
|
1055
1068
|
return unseen(formatCoord(a));
|
|
@@ -1060,6 +1073,10 @@ export class CastCommand extends BypassCommand {
|
|
|
1060
1073
|
return `"cast ${spellKey} ${words[0]} [cells]" -- the distance is a whole number of cells.`;
|
|
1061
1074
|
}
|
|
1062
1075
|
const distance = words.length === 2 ? parseInt(words[1], 10) : 1;
|
|
1076
|
+
const sized = this.sizeForce(spellKey, force, DIRECTIONAL_WALL_FORCE, autoUpTo);
|
|
1077
|
+
if (typeof sized === 'string')
|
|
1078
|
+
return sized;
|
|
1079
|
+
force = sized;
|
|
1063
1080
|
plan = planDirectionalWall(from, words[0].toLowerCase(), distance, force);
|
|
1064
1081
|
if (plan.error)
|
|
1065
1082
|
return plan.error;
|
|
@@ -1070,12 +1087,22 @@ export class CastCommand extends BypassCommand {
|
|
|
1070
1087
|
}
|
|
1071
1088
|
else {
|
|
1072
1089
|
const lead = words[0].toLowerCase();
|
|
1073
|
-
|
|
1090
|
+
let nameWords = lead === 'at' || lead === 'on' || lead === 'around' ? words.slice(1) : words;
|
|
1091
|
+
// "at <someone> [size]" (user 2026-10-05): a trailing number is the
|
|
1092
|
+
// dome's RADIUS in map cells. p.294 makes the radius the Force in
|
|
1093
|
+
// metres, so the size IS a Force -- two cells is Force 4.
|
|
1094
|
+
let size;
|
|
1095
|
+
if (nameWords.length > 1 && /^\d+$/.test(nameWords[nameWords.length - 1])) {
|
|
1096
|
+
size = parseInt(nameWords[nameWords.length - 1], 10);
|
|
1097
|
+
nameWords = nameWords.slice(0, -1);
|
|
1098
|
+
}
|
|
1074
1099
|
if (nameWords.length === 0)
|
|
1075
1100
|
return usage;
|
|
1101
|
+
// "me" / "self" / "myself" are the caster (user 2026-10-05).
|
|
1102
|
+
const selfWord = nameWords.length === 1 && /^(me|self|myself|yourself)$/i.test(nameWords[0]);
|
|
1076
1103
|
const candidates = this.scene.getActorsInRoom(room).filter(a => a === actor || ((a.plane === 'meat' || a.plane === actor.plane) && actor.canPerceive(a)));
|
|
1077
|
-
const picked = fuzzyPickName(nameWords.join(' '), candidates.map(a => a.name));
|
|
1078
|
-
const target = picked ? candidates.find(a => a.name === picked) : undefined;
|
|
1104
|
+
const picked = selfWord ? actor.name : fuzzyPickName(nameWords.join(' '), candidates.map(a => a.name));
|
|
1105
|
+
const target = selfWord ? actor : picked ? candidates.find(a => a.name === picked) : undefined;
|
|
1079
1106
|
if (!target)
|
|
1080
1107
|
return `You don't see "${nameWords.join(' ')}" here. ${usage}`;
|
|
1081
1108
|
const at = this.scene.worldCellOf?.(target);
|
|
@@ -1085,6 +1112,21 @@ export class CastCommand extends BypassCommand {
|
|
|
1085
1112
|
return unseen(target.name);
|
|
1086
1113
|
center = at;
|
|
1087
1114
|
around = target === actor ? 'you' : target.name;
|
|
1115
|
+
if (size !== undefined) {
|
|
1116
|
+
if (size < 1)
|
|
1117
|
+
return `A dome's size is its radius in cells, 1 or more.`;
|
|
1118
|
+
const needed = Math.ceil(size * METERS_PER_CELL);
|
|
1119
|
+
if (autoUpTo === undefined && needed !== force) {
|
|
1120
|
+
return `A dome's radius IS its Force in metres (p.294): ${size} cell${size === 1 ? '' : 's'} is Force ${needed}, not ${force}. Name one or the other.`;
|
|
1121
|
+
}
|
|
1122
|
+
const sized = this.sizeForce(spellKey, force, needed, autoUpTo, 'dome');
|
|
1123
|
+
if (typeof sized === 'string')
|
|
1124
|
+
return sized;
|
|
1125
|
+
force = sized;
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
force = this.domeForce(force, autoUpTo);
|
|
1129
|
+
}
|
|
1088
1130
|
plan = planDome(at, force);
|
|
1089
1131
|
}
|
|
1090
1132
|
if (plan.error)
|
|
@@ -1156,6 +1198,32 @@ export class CastCommand extends BypassCommand {
|
|
|
1156
1198
|
lines.push(...await this.drainCollapse(new CombatExchange(this.scene, this.logger, this.actor)));
|
|
1157
1199
|
return this.finishSpell(lines);
|
|
1158
1200
|
}
|
|
1201
|
+
/**
|
|
1202
|
+
* NO FORCE NAMED: SIZE IT TO THE SHAPE (user 2026-10-05). A wall gets
|
|
1203
|
+
* the least Force that raises it -- never less than the default, and
|
|
1204
|
+
* never past your talent: overcasting turns the drain PHYSICAL (p.281),
|
|
1205
|
+
* and that is a choice to make out loud, not one made for you.
|
|
1206
|
+
*/
|
|
1207
|
+
sizeForce(spellKey, force, needed, autoUpTo, what = 'wall') {
|
|
1208
|
+
// A wall only ever grows to fit; a sized dome IS its size.
|
|
1209
|
+
if (autoUpTo === undefined || (what === 'wall' && needed <= force))
|
|
1210
|
+
return force;
|
|
1211
|
+
if (needed > autoUpTo) {
|
|
1212
|
+
return `That ${what} needs Force ${needed}, past your talent (${autoUpTo}) -- name the Force to overcast it on purpose ("cast ${spellKey} ${needed} ..."; the drain lands PHYSICAL, p.281).`;
|
|
1213
|
+
}
|
|
1214
|
+
return needed;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* A DOME WITH NO FORCE NAMED stands one storey: Force 3, a dome 3 m
|
|
1218
|
+
* high (p.294: height = radius = Force m). Anything lower would not
|
|
1219
|
+
* cover the person it is raised around. Never past your talent -- a
|
|
1220
|
+
* caster with less Magic gets the tallest dome they can hold safely.
|
|
1221
|
+
*/
|
|
1222
|
+
domeForce(force, autoUpTo) {
|
|
1223
|
+
if (autoUpTo === undefined)
|
|
1224
|
+
return force;
|
|
1225
|
+
return Math.max(force, Math.min(DOME_FORCE, autoUpTo));
|
|
1226
|
+
}
|
|
1159
1227
|
/** LINE OF SIGHT for an area spell's point (p.281): no wall between. */
|
|
1160
1228
|
inSight(plane, from, to) {
|
|
1161
1229
|
return wallsBetween(plane, from, to) === 0;
|
|
@@ -177,9 +177,9 @@ export class SpellsCommand extends Command {
|
|
|
177
177
|
line: 'sustained: light slides around the bearer, +3 on sneak contests at -2 dice. Drain F-4.' },
|
|
178
178
|
{ key: 'reflexes', cast: 'increase reflexes', how: 'cast increase reflexes [on <ally>]',
|
|
179
179
|
line: 'sustained: +2 initiative for the bearer at -2 dice. Drain F-3.' },
|
|
180
|
-
{ key: 'barrier', cast: 'barrier', how: 'cast barrier [F] <x,y> <x,y> (wall) | <x,y> (dome) | <direction> [cells] | at <someone> | (bare: a dome round you); cast barrier off',
|
|
180
|
+
{ key: 'barrier', cast: 'barrier', how: 'cast barrier [F] <x,y> <x,y> (wall) | <x,y> (dome) | <direction> [cells] | at <someone|me> [radius] | (bare: a dome round you); Force optional -- sized to fit; cast barrier off',
|
|
181
181
|
line: 'sustained: hardened air -- a wall Force x 2 m long and Force m high, or a Force-metre dome. Armor/Structure = hits, a track per face; nobody meat-side passes (you included) at -2 dice to you. Drain F-1.' },
|
|
182
|
-
{ key: 'manabarrier', cast: 'manabarrier', how: 'cast manabarrier [F] <x,y> <x,y> | <x,y> | <direction> [cells] | at <someone
|
|
182
|
+
{ key: 'manabarrier', cast: 'manabarrier', how: 'cast manabarrier [F] <x,y> <x,y> | <x,y> | <direction> [cells] | at <someone|me> [radius]; cast manabarrier off',
|
|
183
183
|
line: 'sustained: woven mana, same shapes -- stops SPIRITS and astral forms; the living pass freely, and so do you. Armor/Structure = Force. Drain F-2.' },
|
|
184
184
|
];
|
|
185
185
|
for (const entry of book) {
|
|
@@ -3355,5 +3355,14 @@
|
|
|
3355
3355
|
// - Wire: the room grid carries `barriers` (room-local cells; a mana
|
|
3356
3356
|
// barrier only for eyes that can see it). Docks draw x/y rulers in
|
|
3357
3357
|
// plane coordinates and the caption gives your x,y,z.
|
|
3358
|
-
|
|
3358
|
+
// 1.197.0 (2026-10-05): BARRIERS SIZE THEMSELVES. With no Force named, a
|
|
3359
|
+
// wall is cast at the least Force that raises it (a cell of length per
|
|
3360
|
+
// Force, Force 3 a level), the directional wall at Force 3, and a dome
|
|
3361
|
+
// at Force 3 (one storey) -- never past the caster's talent; an
|
|
3362
|
+
// overcast must be named. `cast barrier [F] at <someone> [size]` takes
|
|
3363
|
+
// a radius in cells (size x 2 = Force; a Force and size that disagree
|
|
3364
|
+
// are refused), and "me"/"self" name the caster.
|
|
3365
|
+
// The HUD's sustained line and `spells` show the barrier's Armor,
|
|
3366
|
+
// Structure and every face's damage track.
|
|
3367
|
+
export const ENGINE_VERSION = '1.197.0';
|
|
3359
3368
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -149,7 +149,7 @@ import { ForceCommand } from './commands/force.js';
|
|
|
149
149
|
import { CompleteCommand } from './commands/complete.js';
|
|
150
150
|
import { DisableCommand, BrickCommand } from './commands/disable.js';
|
|
151
151
|
import { CastCommand } from './commands/cast.js';
|
|
152
|
-
import { barrierSpan } from './utilities/barrier-geometry.js';
|
|
152
|
+
import { barrierSpan, barrierStats } from './utilities/barrier-geometry.js';
|
|
153
153
|
import { FollowCommand, UnfollowCommand } from './commands/follow.js';
|
|
154
154
|
import { BringCommand } from './commands/bring.js';
|
|
155
155
|
import { SurrenderCommand } from './commands/surrender.js';
|
|
@@ -9478,10 +9478,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
9478
9478
|
// the one that strands the team -- and both of them seal an exit
|
|
9479
9479
|
// that the caster cannot pass either.
|
|
9480
9480
|
if (player.sustainedBarrier) {
|
|
9481
|
-
heldSpells.push(`physical barrier (${player.sustainedBarrier.shape} ${barrierSpan(player.sustainedBarrier)}, ${player.sustainedBarrier.roomName})`);
|
|
9481
|
+
heldSpells.push(`physical barrier (${player.sustainedBarrier.shape} ${barrierSpan(player.sustainedBarrier)}, ${player.sustainedBarrier.roomName}; ${barrierStats(player.sustainedBarrier)})`);
|
|
9482
9482
|
}
|
|
9483
9483
|
if (player.sustainedManaBarrier) {
|
|
9484
|
-
heldSpells.push(`mana barrier (${player.sustainedManaBarrier.shape} ${barrierSpan(player.sustainedManaBarrier)}, ${player.sustainedManaBarrier.roomName})`);
|
|
9484
|
+
heldSpells.push(`mana barrier (${player.sustainedManaBarrier.shape} ${barrierSpan(player.sustainedManaBarrier)}, ${player.sustainedManaBarrier.roomName}; ${barrierStats(player.sustainedManaBarrier)})`);
|
|
9485
9485
|
}
|
|
9486
9486
|
// What this character holds on OTHERS -- the caster's burden, named
|
|
9487
9487
|
// by bearer so the mage knows whose armor is costing them two dice.
|
|
@@ -22,6 +22,21 @@ export function maxWallCells(force) {
|
|
|
22
22
|
export function maxWallLevels(force) {
|
|
23
23
|
return Math.max(1, Math.floor(force / METERS_PER_LEVEL));
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* THE LEAST FORCE THAT RAISES THIS WALL -- what a cast with no Force
|
|
27
|
+
* named is sized to (user 2026-10-05: "the force is auto calculated if
|
|
28
|
+
* omitted"). Length needs Force cells (Force x 2 m); every level past
|
|
29
|
+
* the first needs another 3 m of height, i.e. Force 3 a level.
|
|
30
|
+
*/
|
|
31
|
+
export function forceForWall(a, b) {
|
|
32
|
+
const length = Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y)) + 1;
|
|
33
|
+
const levels = Math.abs(a.z - b.z) + 1;
|
|
34
|
+
return Math.max(1, Math.ceil(length * METERS_PER_CELL / 2), levels > 1 ? levels * METERS_PER_LEVEL : 1);
|
|
35
|
+
}
|
|
36
|
+
/** The directional wall is three cells across: Force 3. */
|
|
37
|
+
export const DIRECTIONAL_WALL_FORCE = 3;
|
|
38
|
+
/** A dome with no Force named rises one full 3 m storey: Force 3. */
|
|
39
|
+
export const DOME_FORCE = METERS_PER_LEVEL;
|
|
25
40
|
/**
|
|
26
41
|
* A WALL BETWEEN TWO POINTS, ends included. Straight along an axis only:
|
|
27
42
|
* a diagonal run of cells leaves corner gaps an 8-way walker slips
|
|
@@ -191,10 +206,19 @@ export function barrierSpan(barrier) {
|
|
|
191
206
|
}
|
|
192
207
|
export function describeBarrier(barrier) {
|
|
193
208
|
const span = barrierSpan(barrier);
|
|
209
|
+
return `a ${barrier.shape} over ${span} in ${barrier.roomName}, Force ${barrier.force}, ${barrierStats(barrier)}`;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Armor and every face's damage track, in one breath: "Armor 3,
|
|
213
|
+
* Structure 3 [N 0/3, E 1/3, S BREACHED, W 0/3]". The HUD's sustained
|
|
214
|
+
* line and the spells list both print it -- a barrier you hold is a
|
|
215
|
+
* thing being shot at, and the track is what tells you which side gives.
|
|
216
|
+
*/
|
|
217
|
+
export function barrierStats(barrier) {
|
|
194
218
|
const tracks = barrier.faces
|
|
195
219
|
.map(f => `${f.id === 'wall' ? 'wall' : f.id.toUpperCase()} ${f.breached ? 'BREACHED' : `${f.damage}/${barrier.structure}`}`)
|
|
196
220
|
.join(', ');
|
|
197
|
-
return `
|
|
221
|
+
return `Armor ${barrier.armor}, Structure ${barrier.structure} [${tracks}]`;
|
|
198
222
|
}
|
|
199
223
|
export function allBreached(barrier) {
|
|
200
224
|
return barrier.faces.every(f => f.breached);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.385.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
|
|
6
6
|
"description": "A command line tool for scaffolding Meteor 3.x applications using React.",
|