@maka/maka-cli 5.131.0 → 5.133.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/companions.js +73 -9
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +18 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js +18 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +24 -4
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +18 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +22 -5
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/companion-heel.js +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/drone-prose.js +30 -9
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +39 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.133.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.",
|
|
@@ -5,7 +5,8 @@ import { agentRatingFromName } from '../utilities/programs.js';
|
|
|
5
5
|
import { resonanceAction } from '../resonance-actions.js';
|
|
6
6
|
import { FIREARM_SKILLS } from '../models/player.js';
|
|
7
7
|
import { ownedDrones, droneListLines } from '../utilities/owned-drones.js';
|
|
8
|
-
import { droneLaunchAction, droneLaunchLine, droneRecallLine, droneWreckFragment } from '../utilities/drone-prose.js';
|
|
8
|
+
import { droneLaunchAction, droneLaunchLine, droneRecallLine, droneStowLine, droneHandHeld, droneWreckFragment } from '../utilities/drone-prose.js';
|
|
9
|
+
import { HolsterCommand } from './brandish.js';
|
|
9
10
|
/**
|
|
10
11
|
* The rigger's autonomous frames (SR5e p.269-271): "deploy <drone>" puts
|
|
11
12
|
* a carried frame in the air under its own DOG-BRAIN -- no control rig
|
|
@@ -70,7 +71,7 @@ export class DeployCommand extends Command {
|
|
|
70
71
|
// every catalog frame.
|
|
71
72
|
return [
|
|
72
73
|
`${droneLaunchLine(drone, from)} -- dog-brain running, Pilot ${drone.pilotRating}.`,
|
|
73
|
-
` It follows where you walk${armed ? ` and its mount tracks what threatens you` : ` -- cameras only; it will not fight`}. Hull ${drone.droneSummary()}.${hint(` ("order ${drone.name.toLowerCase()} ..." to direct it, "recall" to bring it
|
|
74
|
+
` It follows where you walk${armed ? ` and its mount tracks what threatens you` : ` -- cameras only; it will not fight`}. Hull ${drone.droneSummary()}.${hint(` ("order ${drone.name.toLowerCase()} ..." to direct it, "recall" to bring it to your side, "stow" to pack it away, "jump" to take the stick yourself, wherever it flies.)`)}`,
|
|
74
75
|
].join('\n');
|
|
75
76
|
}
|
|
76
77
|
}
|
|
@@ -137,15 +138,66 @@ export class AgentCommand extends Command {
|
|
|
137
138
|
].join('\n');
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
function pickDeployedDrone(game, args) {
|
|
142
|
+
if (!game || args.length === 0)
|
|
143
|
+
return undefined;
|
|
144
|
+
const drones = game.companions.filter(c => c.kind === 'drone');
|
|
145
|
+
const names = drones.map(c => c.boundDevice?.name ?? c.npc.name);
|
|
146
|
+
const picked = fuzzyPickName(args.join(' '), names);
|
|
147
|
+
return picked ? drones[names.indexOf(picked)] : undefined;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* "stow <drone>": fold a deployed frame back into your pack -- what
|
|
151
|
+
* "recall" used to do before the 2026-09-06 ruling made recall "return
|
|
152
|
+
* to me". Hand-held frames only (drone-prose.ts droneHandHeld): a Steel
|
|
153
|
+
* Lynx is not a pocket job and stays deployed ("store" parks it in the
|
|
154
|
+
* garage at the hideout). The verb was already the holster's ("stow"
|
|
155
|
+
* = holster your weapon) and still is: only a deployed frame the words
|
|
156
|
+
* name -- or a bare "stow" with a frame out and no steel drawn -- goes
|
|
157
|
+
* to the drone; everything else holsters exactly as before.
|
|
158
|
+
*/
|
|
159
|
+
export class StowCommand extends Command {
|
|
160
|
+
static verb = 'stow';
|
|
161
|
+
static description = 'Stow a deployed drone back into your pack ("stow <drone>"; frames too big to carry stay out), or holster your drawn weapon.';
|
|
162
|
+
async execute(args = []) {
|
|
163
|
+
const game = this.game;
|
|
164
|
+
const drones = game?.companions.filter(c => c.kind === 'drone') ?? [];
|
|
165
|
+
const named = pickDeployedDrone(game, args);
|
|
166
|
+
const bareForDrone = args.length === 0 && drones.length > 0 && !this.actor.weaponDrawn;
|
|
167
|
+
const entry = named ?? (bareForDrone ? drones[0] : undefined);
|
|
168
|
+
if (!entry) {
|
|
169
|
+
return new HolsterCommand({ actor: this.actor, scene: this.scene, rooms: this.rooms, game: this.game }).execute(args);
|
|
170
|
+
}
|
|
171
|
+
const item = entry.boundDevice;
|
|
172
|
+
const name = item?.name ?? entry.npc.name;
|
|
173
|
+
if (this.actor.plane !== 'meat') {
|
|
174
|
+
return `Stowing is a hands job -- come back to your body.`;
|
|
175
|
+
}
|
|
176
|
+
if (item && !droneHandHeld(item)) {
|
|
177
|
+
return `The ${name} is not a pocket job -- it stays deployed.${game?.homeRoom ? ` At the hideout, "store ${name.toLowerCase()}" parks it in the garage.` : ''} "recall" brings it to your side.`;
|
|
178
|
+
}
|
|
179
|
+
if (entry.npc.currentLocation !== this.actor.currentLocation) {
|
|
180
|
+
return `The ${name} isn't here to stow -- it's in ${entry.npc.currentLocation.name}. "recall" it first.`;
|
|
181
|
+
}
|
|
182
|
+
if (entry.npc.inExchange) {
|
|
183
|
+
return `The ${name} is fighting for its life -- let the exchange settle before folding it.`;
|
|
184
|
+
}
|
|
185
|
+
game.dismissCompanion(entry, 'stowed');
|
|
186
|
+
this.actor.performAction('stows', name);
|
|
187
|
+
this.logger.write(`StowCommand: ${this.actor.name} stowed ${name}.`);
|
|
188
|
+
this.scene.updateStatus();
|
|
189
|
+
return item ? droneStowLine(item) : `The ${name} folds into your pack.`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** "recall": bring a deployed drone to your side (it stays deployed and
|
|
193
|
+
* follows again -- "stow" is the fold), spin a remote agent down into
|
|
194
|
+
* its deck, or work CALL/DISMISS SPRITE -- see the sprite branch below. */
|
|
143
195
|
export class RecallCommand extends Command {
|
|
144
196
|
static verb = 'recall';
|
|
145
|
-
static description = `Recall your deployed drone to your
|
|
197
|
+
static description = `Recall your deployed drone to your side -- it comes to you and follows again ("stow <drone>" folds it into your pack) -- fold a running agent back into its deck, or call a registered sprite out of the Resonance. "${resonanceAction('call-dismiss-sprite')?.name}" (${resonanceAction('call-dismiss-sprite')?.actionType}, SR5 p.250): "recall <name>" also sends a standing registered sprite back -- which releases every task it still owes you. To park one cheaply instead, "standby" spends a single task (p.256).`;
|
|
146
198
|
async execute(args = []) {
|
|
147
199
|
const game = this.game;
|
|
148
|
-
const entry = game?.companions.find(c => c.kind === 'drone');
|
|
200
|
+
const entry = pickDeployedDrone(game, args) ?? game?.companions.find(c => c.kind === 'drone');
|
|
149
201
|
if (!entry) {
|
|
150
202
|
// No drone aloft -- but an agent on the grid answers to the same
|
|
151
203
|
// word (a real session tried every phrasing but the bare "agent"
|
|
@@ -163,11 +215,23 @@ export class RecallCommand extends Command {
|
|
|
163
215
|
return sprite;
|
|
164
216
|
return `Nothing of yours is in the air.`;
|
|
165
217
|
}
|
|
218
|
+
// RECALL IS "RETURN TO ME" (player ruling 2026-09-06, after a
|
|
219
|
+
// playtest: recall folded the frame into the hand and "it
|
|
220
|
+
// disappeared, which doesn't make sense -- it should just return to
|
|
221
|
+
// me and begin following me again"). The frame comes to your side,
|
|
222
|
+
// stays deployed, and is back on heel; "stow" is the fold.
|
|
166
223
|
const name = entry.boundDevice?.name ?? entry.npc.name;
|
|
167
|
-
|
|
224
|
+
if (entry.npc.inExchange) {
|
|
225
|
+
return `The ${name} is fighting for its life -- let the exchange settle before calling it off.`;
|
|
226
|
+
}
|
|
227
|
+
const { moved } = game.heelCompanion(entry);
|
|
228
|
+
if (!moved)
|
|
229
|
+
return `The ${name} is already at your side.`;
|
|
230
|
+
entry.npc.performAction('returns', `to ${this.actor.name}'s side`);
|
|
168
231
|
this.actor.performAction('recalls', name);
|
|
232
|
+
this.logger.write(`RecallCommand: ${name} recalled to ${this.actor.name}'s side in ${this.actor.currentLocation.name}.`);
|
|
169
233
|
this.scene.updateStatus();
|
|
170
|
-
return entry.boundDevice ? droneRecallLine(entry.boundDevice) : `The ${name} comes
|
|
234
|
+
return entry.boundDevice ? droneRecallLine(entry.boundDevice) : `The ${name} comes to your side and follows again.`;
|
|
171
235
|
}
|
|
172
236
|
/**
|
|
173
237
|
* CALL/DISMISS SPRITE (SR5 p.250, Simple Action), on this verb rather
|
|
@@ -50,6 +50,23 @@ export class GoCommand extends Command {
|
|
|
50
50
|
sprite: (plane) => plane === 'matrix',
|
|
51
51
|
ally: () => false
|
|
52
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* Does this companion follow the master's step? Hired crew never do
|
|
55
|
+
* (ruling 2026-08-25, above); a shell whose plane can't keep up does
|
|
56
|
+
* not; one that is down does not; and a drone HOLDING POSITION -- the
|
|
57
|
+
* frame a rigger jumped out of and left where it was (rigger pass
|
|
58
|
+
* 2026-09-06) -- waits there until "recall" brings it to heel. Pulled
|
|
59
|
+
* out of the loop so the rule is one function a test can ask.
|
|
60
|
+
*/
|
|
61
|
+
static heels(c, plane) {
|
|
62
|
+
if (c.kind === 'ally')
|
|
63
|
+
return false;
|
|
64
|
+
if (c.holding)
|
|
65
|
+
return false;
|
|
66
|
+
if (c.npc.isIncapacitated())
|
|
67
|
+
return false;
|
|
68
|
+
return GoCommand.HEELS_ON[c.kind](plane);
|
|
69
|
+
}
|
|
53
70
|
static verb = 'go';
|
|
54
71
|
static description = 'Move in the specified direction ("go back" retraces your last move). Fleeing someone you just fought hands them one free parting shot. Jacked in, the grid sees no distance: "go <any room>" jumps your persona straight there ("map" lights the icons). Inside a host there is no distance to cross at all -- "exit" first.';
|
|
55
72
|
async execute(args) {
|
|
@@ -470,10 +487,7 @@ export class GoCommand extends Command {
|
|
|
470
487
|
// with their own feet: they hold whatever ground they're on
|
|
471
488
|
// until they decide to move or you "order" them to, which is
|
|
472
489
|
// also what makes sending one ahead to scout mean anything.
|
|
473
|
-
if (c.
|
|
474
|
-
continue;
|
|
475
|
-
const canFollow = GoCommand.HEELS_ON[c.kind](this.actor.plane);
|
|
476
|
-
if (!canFollow || c.npc.currentLocation === dest || c.npc.isIncapacitated())
|
|
490
|
+
if (!GoCommand.heels(c, this.actor.plane) || c.npc.currentLocation === dest)
|
|
477
491
|
continue;
|
|
478
492
|
c.npc.currentLocation = dest;
|
|
479
493
|
c.npc.atSpot = this.actor.atSpot; // at their master's heel = at their spot
|
|
@@ -16,7 +16,7 @@ import { droneWreckFragment } from '../utilities/drone-prose.js';
|
|
|
16
16
|
*/
|
|
17
17
|
export class JumpCommand extends Command {
|
|
18
18
|
static verb = 'jump';
|
|
19
|
-
static description = 'Jump INTO a drone (control rig required): fly it, see through it, fight with its mount. A deployed frame is seized wherever it flies. "jump hot" for hot-sim; "jump" again returns to your body and leaves the frame out on its dog-brain where you left it ("recall" brings it
|
|
19
|
+
static description = 'Jump INTO a drone (control rig required): fly it, see through it, fight with its mount. A deployed frame is seized wherever it flies. "jump hot" for hot-sim; "jump" again returns to your body and leaves the frame out on its dog-brain where you left it ("recall" brings it to your side, "stow" packs it away).';
|
|
20
20
|
async execute(args = []) {
|
|
21
21
|
const actor = this.actor;
|
|
22
22
|
// Already riding: this is the jump OUT.
|
|
@@ -38,10 +38,23 @@ export class JumpCommand extends Command {
|
|
|
38
38
|
// (utilities/owned-drones.ts): a frame parked at home jumps from
|
|
39
39
|
// home and stays parked while you ride it.
|
|
40
40
|
const owned = ownedDrones(actor, this.game?.homeRoom, { reach: 'here' });
|
|
41
|
-
|
|
41
|
+
// ...AND EVERY DEPLOYED FRAME, WHEREVER IT FLIES (transcript
|
|
42
|
+
// 2026-09-06: a Direktionssekretar deployed from the garage could
|
|
43
|
+
// not be jumped into once the rigger left the hideout -- "Nothing
|
|
44
|
+
// to jump into -- no drone in your pack" -- while this verb's own
|
|
45
|
+
// description promised seizure "wherever it flies"). Jumping in
|
|
46
|
+
// rides the link, not your hands: the hands rule above is for
|
|
47
|
+
// frames still stowed; a frame already out is in reach by definition.
|
|
48
|
+
const deployed = (this.game?.companions ?? [])
|
|
49
|
+
.filter(c => c.kind === 'drone' && c.boundDevice && !owned.some(d => d.item === c.boundDevice));
|
|
50
|
+
const drones = [...owned.map(d => d.item), ...deployed.map(c => c.boundDevice)];
|
|
51
|
+
const listing = [
|
|
52
|
+
...droneListLines(owned),
|
|
53
|
+
...deployed.map(c => ` • ${c.boundDevice.name.padEnd(26)} deployed -- ${c.npc.currentLocation.name}${c.boundDevice.maxDroneBoxes > 0 ? ` hull ${c.boundDevice.droneSummary()}` : ''}`),
|
|
54
|
+
];
|
|
42
55
|
const atHome = this.game?.homeRoom !== undefined && actor.currentLocation === this.game.homeRoom;
|
|
43
56
|
if (drones.length === 0) {
|
|
44
|
-
return `Nothing to jump into -- no drone in your pack${atHome ? ' or your garage' : ''}.${hint(` (Ratchet's Circuit Bazaar stocks airframes.)`)}`;
|
|
57
|
+
return `Nothing to jump into -- no drone in your pack${atHome ? ' or your garage' : ''}, none deployed.${hint(` (Ratchet's Circuit Bazaar stocks airframes.)`)}`;
|
|
45
58
|
}
|
|
46
59
|
// "jump hot" / "jump <drone> hot": strip the mode word, pick the frame.
|
|
47
60
|
const words = args.map(w => w.toLowerCase());
|
|
@@ -52,11 +65,11 @@ export class JumpCommand extends Command {
|
|
|
52
65
|
const picked = fuzzyPickName(nameWords.join(' '), drones.map(d => d.name));
|
|
53
66
|
drone = picked ? drones.find(d => d.name === picked) : undefined;
|
|
54
67
|
if (!drone) {
|
|
55
|
-
return [`No drone by that name in reach. Yours:`, ...
|
|
68
|
+
return [`No drone by that name in reach. Yours:`, ...listing].join('\n');
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
if (!drone) {
|
|
59
|
-
return [`Which airframe? ("jump <name>", add "hot" for hot-sim)`, ...
|
|
72
|
+
return [`Which airframe? ("jump <name>", add "hot" for hot-sim)`, ...listing].join('\n');
|
|
60
73
|
}
|
|
61
74
|
if (drone.isWrecked) {
|
|
62
75
|
return `The ${drone.name} is a wreck -- ${droneWreckFragment(drone)} -- and a wreck jumps nowhere. It repairs while you rest somewhere safe.`;
|
|
@@ -7,7 +7,7 @@ import { Direction } from '../types/shared/direction-enum.js';
|
|
|
7
7
|
import { Door } from '../models/door.js';
|
|
8
8
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
9
9
|
import { heldBy, holding, physicalLimit } from '../utilities/grapple.js';
|
|
10
|
-
import { spotsActive, resolveSpot, spotOf, describeSpotRoster, OPEN_FLOOR, pathBetweenSpots, spotDistanceMeters, spotClimbSteps, blockingRegion, terrainRefusal, reseatOpenFloor, routeForMove, stepFrom, spotForCell, actorCell, standingRoomNear, furnitureCells, approachCellFor, exitSpot, concealedFrom, revealActor, onTheFloor } from '../utilities/spots.js';
|
|
10
|
+
import { spotsActive, resolveSpot, spotOf, describeSpotRoster, OPEN_FLOOR, pathBetweenSpots, spotDistanceMeters, spotClimbSteps, blockingRegion, terrainRefusal, reseatOpenFloor, routeForMove, stepFrom, spotForCell, actorCell, standingRoomNear, yieldSquareToMaster, furnitureCells, approachCellFor, exitSpot, concealedFrom, revealActor, onTheFloor } from '../utilities/spots.js';
|
|
11
11
|
// fallDamageDVMeters, not fallDamageDV: a climb that fell short drops
|
|
12
12
|
// you from where you GOT to, which is a distance in metres, not a whole
|
|
13
13
|
// storey. The storey-based wrapper still exists for callers that really
|
|
@@ -759,6 +759,8 @@ export class MoveCommand extends Command {
|
|
|
759
759
|
const wasAt = actor.atSpot;
|
|
760
760
|
actor.atCell = cell;
|
|
761
761
|
actor.atSpot = spotForCell(room, cell);
|
|
762
|
+
// A shell of yours on that square steps aside (spots.ts heelsTo).
|
|
763
|
+
yieldSquareToMaster(room, actor);
|
|
762
764
|
actor.performAction('steps', `${taken} ${taken === 1 ? 'pace' : 'paces'} ${direction}`);
|
|
763
765
|
this.scene.updateStatus();
|
|
764
766
|
this.scene.updateExits(room);
|
|
@@ -2,7 +2,7 @@ import { Command } from './command.js';
|
|
|
2
2
|
import { hint } from '../utilities/hints.js';
|
|
3
3
|
import { fuzzyPickName, significantWords, normalizeLoose, wordsAreCloseEnough } from '../utilities/fuzzy-match.js';
|
|
4
4
|
import { AI } from '../../../../tools/ai/ai.class.js';
|
|
5
|
-
import { DRONE_COLOUR, DRONE_ICON, droneRecallLine } from '../utilities/drone-prose.js';
|
|
5
|
+
import { DRONE_COLOUR, DRONE_ICON, droneRecallLine, droneStowLine, droneHandHeld } from '../utilities/drone-prose.js';
|
|
6
6
|
/**
|
|
7
7
|
* What "you have nobody" says, one phrase per COMPANION KIND.
|
|
8
8
|
*
|
|
@@ -107,8 +107,20 @@ export class OrderCommand extends Command {
|
|
|
107
107
|
// body." (a real session's agent couldn't be brought home by any
|
|
108
108
|
// natural phrasing: "order agent return", "return to deck"...).
|
|
109
109
|
// ("return fire" is exempt -- that's a shooting order, not a fold.)
|
|
110
|
+
// A DRONE ORDERED TO COME comes -- and stays deployed, back on heel
|
|
111
|
+
// (player ruling 2026-09-06: recall is "return to me"). "fold" /
|
|
112
|
+
// "stow" is the pack, handled with the other kinds below.
|
|
113
|
+
if (entry.kind === 'drone'
|
|
114
|
+
&& /^(return(?!\s+fire)|recall|come(\s+(back|home|here|to\s+me))?|follow(\s+me)?|heel|stand\s+down|go\s+home)\b/i.test(cmd)) {
|
|
115
|
+
const { moved } = game.heelCompanion(entry);
|
|
116
|
+
this.scene.updateStatus();
|
|
117
|
+
const line = !moved
|
|
118
|
+
? `${entry.npc.name} is already at your heel.`
|
|
119
|
+
: entry.boundDevice ? droneRecallLine(entry.boundDevice) : `${entry.npc.name} comes to your side.`;
|
|
120
|
+
return `{${feed.color}-fg}${feed.icon} ${line}{/${feed.color}-fg}`;
|
|
121
|
+
}
|
|
110
122
|
if (entry.kind !== 'ally'
|
|
111
|
-
&& /^(return(?!\s+fire)|recall|come\s+(back|home)|fold|dismiss|stand\s+down|go\s+home)\b/i.test(cmd)) {
|
|
123
|
+
&& /^(return(?!\s+fire)|recall|come\s+(back|home)|fold|dismiss|stand\s+down|go\s+home|stow|pack)\b/i.test(cmd)) {
|
|
112
124
|
// A SPRITE ORDERED HOME IS DISMISSED, NOT DESTROYED, and it is not
|
|
113
125
|
// an agent. This branch had no sprite case at all: the ternary
|
|
114
126
|
// fell through to the AGENT wording, so a technomancer who told
|
|
@@ -135,13 +147,21 @@ export class OrderCommand extends Command {
|
|
|
135
147
|
: `${feed.icon} ${name} unravels into clean static -- an unregistered thread has no Resonance to wait in.`;
|
|
136
148
|
return `{${feed.color}-fg}${line}{/${feed.color}-fg}`;
|
|
137
149
|
}
|
|
150
|
+
// A frame you cannot carry has no pack to fold into; one across
|
|
151
|
+
// the district has to be recalled first -- stowing is hands-on.
|
|
152
|
+
if (entry.kind === 'drone' && entry.boundDevice && !droneHandHeld(entry.boundDevice)) {
|
|
153
|
+
return `{${feed.color}-fg}${feed.icon} The ${entry.boundDevice.name} is not a pocket job -- it stays deployed. "recall" brings it to your side.{/${feed.color}-fg}`;
|
|
154
|
+
}
|
|
155
|
+
if (entry.kind === 'drone' && entry.npc.currentLocation !== this.actor.currentLocation) {
|
|
156
|
+
return `{${feed.color}-fg}${feed.icon} The ${entry.boundDevice?.name ?? entry.npc.name} isn't here to stow -- "recall" it first.{/${feed.color}-fg}`;
|
|
157
|
+
}
|
|
138
158
|
const foldLine = entry.kind === 'spirit'
|
|
139
159
|
? `${feed.icon} You speak the release. The spirit inclines its head -- the bargain closes${(entry.services ?? 0) > 0 ? `, ${entry.services} unspent service${entry.services === 1 ? '' : 's'} forfeit` : ''} -- and it thins back to its own plane.`
|
|
140
160
|
: entry.kind === 'drone'
|
|
141
|
-
// The same words "
|
|
161
|
+
// The same words "stow" speaks (drone-prose.ts), classed by
|
|
142
162
|
// how the frame moves -- this line used to be a verbatim copy
|
|
143
163
|
// of companions.ts's, and both said rotors to a Steel Lynx.
|
|
144
|
-
? `${feed.icon} ${entry.boundDevice ?
|
|
164
|
+
? `${feed.icon} ${entry.boundDevice ? droneStowLine(entry.boundDevice) : `The ${entry.npc.name} folds into your pack.`}`
|
|
145
165
|
: this.actor.plane === 'matrix'
|
|
146
166
|
? `${feed.icon} Your agent's icon collapses back into your persona -- folded into the deck.`
|
|
147
167
|
: `${feed.icon} The deck spins its agent down -- the DECK IN AR light dies as the icon folds home.`;
|
|
@@ -336,5 +336,22 @@
|
|
|
336
336
|
// under its own power; a locked door stops every frame but a palm-flier
|
|
337
337
|
// on the recon sweep. Two tables on different versions disagree about
|
|
338
338
|
// where a frame is after a jump-out.
|
|
339
|
-
|
|
339
|
+
// 1.37.0 (2026-09-06): RECALL IS "RETURN TO ME"; STOW PUTS IT AWAY. COMMAND
|
|
340
|
+
// SEMANTICS in shared scenes: "recall <drone>" now brings a deployed frame
|
|
341
|
+
// to your side and back on heel, still deployed (it used to fold the frame
|
|
342
|
+
// into your hand -- playtest: "it disappeared, which doesn't make sense");
|
|
343
|
+
// "stow <drone>" is the fold (hand-held frames only; the holster meaning
|
|
344
|
+
// of "stow" is untouched), and "order <drone> come/return/follow" heels
|
|
345
|
+
// while "order <drone> fold/stow" packs. A frame you jump out of HOLDS
|
|
346
|
+
// its position (Game.companions.holding) instead of teleporting to your
|
|
347
|
+
// heel on your next step; recall clears the hold. HUD shows "holding at".
|
|
348
|
+
// 1.38.0 (2026-09-06): YOUR OWN SHELL IS NEVER IN YOUR WAY. COMMAND SEMANTICS
|
|
349
|
+
// in shared scenes: a companion shell (drone, spirit, sprite, agent) at its
|
|
350
|
+
// master's heel no longer blocks the master's "move <direction>" -- it
|
|
351
|
+
// yields the square (spots.ts heelsTo / yieldSquareToMaster); hired crew
|
|
352
|
+
// still hold theirs. "jump" reaches every DEPLOYED frame wherever it flies,
|
|
353
|
+
// garage-born or not (it used to answer "no drone in your pack" away from
|
|
354
|
+
// the hideout). An NPC with no dialogue and no AI no longer greets you
|
|
355
|
+
// with "<name>: undefined".
|
|
356
|
+
export const ENGINE_VERSION = '1.38.0';
|
|
340
357
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -132,7 +132,7 @@ import { SustainCommand } from './commands/sustain.js';
|
|
|
132
132
|
import { AbandonCommand } from './commands/abandon.js';
|
|
133
133
|
import { UnravelCommand } from './commands/unravel.js';
|
|
134
134
|
import { OrderCommand } from './commands/order.js';
|
|
135
|
-
import { DeployCommand, AgentCommand, RecallCommand } from './commands/companions.js';
|
|
135
|
+
import { DeployCommand, AgentCommand, RecallCommand, StowCommand } from './commands/companions.js';
|
|
136
136
|
import { DeckCommand, LoadProgramCommand, UnloadProgramCommand } from './commands/deck.js';
|
|
137
137
|
import { getProgram } from './utilities/programs.js';
|
|
138
138
|
import { MoveCommand, RunCommand, SprintCommand } from './commands/move.js';
|
|
@@ -183,6 +183,7 @@ import { AI } from '../../../tools/ai/ai.class.js';
|
|
|
183
183
|
import { BULLET } from './utilities/log-style.js';
|
|
184
184
|
import { spotOf, resolveSpot } from './utilities/spots.js';
|
|
185
185
|
import { DRONE_COLOUR, DRONE_ICON, droneFoldForTravelLine, droneUnfoldAtCurbLine } from './utilities/drone-prose.js';
|
|
186
|
+
import { bringToHeel } from './utilities/companion-heel.js';
|
|
186
187
|
export default class Game {
|
|
187
188
|
initialized;
|
|
188
189
|
static instance;
|
|
@@ -1445,6 +1446,17 @@ export default class Game {
|
|
|
1445
1446
|
shell.maxCarryingWeight = Game.DRONE_CARGO_KG[item.size] ?? 2;
|
|
1446
1447
|
return shell;
|
|
1447
1448
|
}
|
|
1449
|
+
/**
|
|
1450
|
+
* BRING A COMPANION TO HEEL (player ruling 2026-09-06: "recall" is
|
|
1451
|
+
* "return to me" -- the frame comes to your side and follows again;
|
|
1452
|
+
* putting it away is "stow"). The shell crosses to the master's room
|
|
1453
|
+
* and spot and any HOLDING flag clears, so the next "go" drags it
|
|
1454
|
+
* along like any companion. Returns whether it had anywhere to come
|
|
1455
|
+
* from.
|
|
1456
|
+
*/
|
|
1457
|
+
heelCompanion(entry) {
|
|
1458
|
+
return bringToHeel(entry, this.player);
|
|
1459
|
+
}
|
|
1448
1460
|
/** Drops a companion's shell and its roster entry. */
|
|
1449
1461
|
dismissCompanion(entry, reason) {
|
|
1450
1462
|
if (this.scene.getActor(entry.npc.name) === entry.npc) {
|
|
@@ -2508,7 +2520,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2508
2520
|
CommandFactory.registerCommand('brandish', BrandishCommand);
|
|
2509
2521
|
CommandFactory.registerCommand('draw', BrandishCommand);
|
|
2510
2522
|
CommandFactory.registerCommand('holster', HolsterCommand);
|
|
2511
|
-
|
|
2523
|
+
// "stow" is the holster's word AND the drone fold's (2026-09-06):
|
|
2524
|
+
// StowCommand takes a deployed frame the words name and hands
|
|
2525
|
+
// everything else to HolsterCommand unchanged.
|
|
2526
|
+
CommandFactory.registerCommand('stow', StowCommand);
|
|
2512
2527
|
// "end" and "hangup" both bounced in real sessions when players
|
|
2513
2528
|
// wanted to end a call -- the intent is unambiguous mid-call.
|
|
2514
2529
|
CommandFactory.registerCommand('end', EndCallCommand);
|
|
@@ -5420,7 +5435,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5420
5435
|
// PILOT rating, agents run at a RATING, and a spirit's Force is
|
|
5421
5436
|
// already in its name.
|
|
5422
5437
|
const grade = c.kind === 'drone' ? `Pilot ${c.force} -- ` : c.kind === 'agent' ? `Rating ${c.force} -- ` : c.kind === 'sprite' ? `Level ${c.force} -- ` : '';
|
|
5423
|
-
|
|
5438
|
+
// A frame left on its dog-brain says where it waits.
|
|
5439
|
+
const hold = c.holding ? ` -- holding at ${c.npc.currentLocation.name}` : '';
|
|
5440
|
+
lines.push(`{green-fg}👥 ${c.npc.name} -- ${grade}${track}${hold}{/green-fg}`);
|
|
5424
5441
|
}
|
|
5425
5442
|
// DECK-AR INDICATOR (player request): "Link online" is the
|
|
5426
5443
|
// COMMLINK's AR overlay; this is the DECK's -- lit when an agent
|
|
@@ -5970,13 +5987,13 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5970
5987
|
*/
|
|
5971
5988
|
static HELP_CATEGORIES = [
|
|
5972
5989
|
{ title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach'], ['run'], ['sprint'], ['climb', 'mantle', 'scale', 'clamber'], ['descend'], ['sit', 'kneel'], ['lie', 'prone'], ['stand'], ['follow'], ['unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
|
|
5973
|
-
{ title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['loot'], ['drop'], ['give'], ['store'], ['open'], ['close'], ['put'], ['equip'], ['unequip'], ['fit'], ['unfit'], ['brandish', 'draw'], ['holster'
|
|
5990
|
+
{ title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['loot'], ['drop'], ['give'], ['store'], ['open'], ['close'], ['put'], ['equip'], ['unequip'], ['fit'], ['unfit'], ['brandish', 'draw'], ['holster'], ['reload'], ['use'], ['read']] },
|
|
5974
5991
|
{ title: 'combat', entries: [['attack'], ['kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['stance'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
|
|
5975
5992
|
// The party layer: everyone who walks (or flies, or manifests) at
|
|
5976
5993
|
// your side answers to these.
|
|
5977
5994
|
// "players" folded back under crew after playtesting ("crew
|
|
5978
5995
|
// players" / "crew street"); the bare verb still answers quietly.
|
|
5979
|
-
{ title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall']] },
|
|
5996
|
+
{ title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
|
|
5980
5997
|
{ title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense']] },
|
|
5981
5998
|
{ title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
5982
5999
|
// The Emerged get their own shelf (player request: "there MUST be
|
|
@@ -1546,7 +1546,13 @@ ${worldEventsSummary}
|
|
|
1546
1546
|
this._triedThisStimulus = [];
|
|
1547
1547
|
if (!AI.isConfigured()) {
|
|
1548
1548
|
this.logger.write(`No AI available`);
|
|
1549
|
-
|
|
1549
|
+
// A shell with no dialogue -- every companion is built with
|
|
1550
|
+
// `dialog: []` -- has nothing to say, and used to say so as
|
|
1551
|
+
// "<name>: undefined" (transcript 2026-09-06: a drone waking as
|
|
1552
|
+
// its rigger walked in). Silence is the right line.
|
|
1553
|
+
const line = this.speak();
|
|
1554
|
+
if (line)
|
|
1555
|
+
await actor.hear(this, line);
|
|
1550
1556
|
return;
|
|
1551
1557
|
}
|
|
1552
1558
|
// ---------- helpers ----------
|
|
@@ -2679,6 +2679,10 @@ export class Player extends AbstractPlayer {
|
|
|
2679
2679
|
const overLink = opts?.viaLink || this.activeCallPartner === actor;
|
|
2680
2680
|
if (!overLink && !this.canPerceive(actor))
|
|
2681
2681
|
return;
|
|
2682
|
+
// Nothing said is nothing heard: a caller passing an empty line
|
|
2683
|
+
// (an NPC with no dialogue, AI off) must not print "<name>: undefined".
|
|
2684
|
+
if (!message)
|
|
2685
|
+
return;
|
|
2682
2686
|
this.logger.write(`${this.name} heard ${actor.name}`);
|
|
2683
2687
|
// A voice arriving over an active CALL from someone who isn't in the
|
|
2684
2688
|
// room wears the comm dressing (player note: Mr. Johnson -- who lives
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BRING A COMPANION TO HEEL (player ruling 2026-09-06: "recall" is
|
|
3
|
+
* "return to me" -- the frame comes to your side and follows again;
|
|
4
|
+
* putting it away is "stow"). The shell crosses to the master's room
|
|
5
|
+
* and spot, its cell is released so seating finds it a place beside
|
|
6
|
+
* them, and any HOLDING flag clears so the next "go" drags it along
|
|
7
|
+
* like any companion. Returns whether it had anywhere to come from.
|
|
8
|
+
*
|
|
9
|
+
* One function, used by Game.heelCompanion and by the tests that pin
|
|
10
|
+
* recall, so a mock Game and the real one cannot disagree about it.
|
|
11
|
+
*/
|
|
12
|
+
export function bringToHeel(entry, master) {
|
|
13
|
+
const here = master.currentLocation;
|
|
14
|
+
const moved = entry.npc.currentLocation !== here || entry.npc.atSpot !== master.atSpot || !!entry.holding;
|
|
15
|
+
if (entry.npc.currentLocation !== here)
|
|
16
|
+
entry.npc.currentLocation = here;
|
|
17
|
+
entry.npc.atSpot = master.atSpot;
|
|
18
|
+
entry.npc.atCell = undefined;
|
|
19
|
+
entry.holding = false;
|
|
20
|
+
return { moved };
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=companion-heel.js.map
|
|
@@ -113,21 +113,42 @@ export function droneLaunchLine(item, from) {
|
|
|
113
113
|
missile: `The ${name} kicks off its rail and holds a tight circle overhead`,
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Recall / "order <drone> come": the frame comes to your side and is
|
|
118
|
+
* back at heel, still deployed (player ruling 2026-09-06: "recall is
|
|
119
|
+
* just return to me"; putting it away is "stow" -- droneStowLine).
|
|
120
|
+
*/
|
|
117
121
|
export function droneRecallLine(item) {
|
|
118
122
|
const name = item.name;
|
|
119
123
|
const back = pick(item, {
|
|
120
|
-
'flier-palm': `The ${name} loops back and
|
|
121
|
-
flier: `The ${name} banks once and settles
|
|
122
|
-
ground: `The ${name} rolls back to your side and
|
|
123
|
-
walker: `The ${name} picks its way back and
|
|
124
|
-
anthro: `The ${name} walks back and
|
|
125
|
-
missile: `The ${name} comes around and
|
|
124
|
+
'flier-palm': `The ${name} loops back and hangs at your shoulder, rotors whining.`,
|
|
125
|
+
flier: `The ${name} banks once and settles into a hover at your shoulder.`,
|
|
126
|
+
ground: `The ${name} rolls back to your side and idles, motors ticking.`,
|
|
127
|
+
walker: `The ${name} picks its way back and settles at your feet.`,
|
|
128
|
+
anthro: `The ${name} walks back and falls in at your side.`,
|
|
129
|
+
missile: `The ${name} comes around and picks up station overhead.`,
|
|
130
|
+
});
|
|
131
|
+
const dents = item.droneDamage > 0
|
|
132
|
+
? ` The frame carries its dents (${item.droneSummary()})${hint(' -- rest repairs it')}.`
|
|
133
|
+
: '';
|
|
134
|
+
return `${back} It follows where you walk again.${dents}`;
|
|
135
|
+
}
|
|
136
|
+
/** Stow: the frame folds back into your pack -- hand-held frames only
|
|
137
|
+
* (droneHandHeld); a frame you cannot carry stays deployed. */
|
|
138
|
+
export function droneStowLine(item) {
|
|
139
|
+
const name = item.name;
|
|
140
|
+
const away = pick(item, {
|
|
141
|
+
'flier-palm': `The ${name} settles onto your palm, rotors stilling, and goes back in your pocket.`,
|
|
142
|
+
flier: `The ${name} settles back onto your hand, rotors folding, and goes into the pack.`,
|
|
143
|
+
ground: `The ${name} powers down and you sling it.`,
|
|
144
|
+
walker: `The ${name} folds its legs into your hand and goes into the pack.`,
|
|
145
|
+
anthro: `The ${name} climbs into the pack and goes still.`,
|
|
146
|
+
missile: `The ${name} settles onto its rail, turbine spooling down.`,
|
|
126
147
|
});
|
|
127
148
|
const dents = item.droneDamage > 0
|
|
128
149
|
? ` The frame carries its dents (${item.droneSummary()})${hint(' -- rest repairs it')}.`
|
|
129
150
|
: '';
|
|
130
|
-
return `${
|
|
151
|
+
return `${away}${dents}`;
|
|
131
152
|
}
|
|
132
153
|
/** Pre-travel: how the frame gets to the cab, or does not need one. */
|
|
133
154
|
export function droneFoldForTravelLine(item) {
|
|
@@ -212,7 +233,7 @@ export function droneJumpOutLine(item, frameRoom, bodyRoom) {
|
|
|
212
233
|
missile: 'holds its circle overhead',
|
|
213
234
|
});
|
|
214
235
|
const where = frameRoom === bodyRoom ? 'where you left it' : `in ${frameRoom.name}`;
|
|
215
|
-
return `You're back in your body in ${bodyRoom.name}; the ${item.name} ${holds} ${where} -- dog-brain on the stick.${hint(' ("recall" brings it
|
|
236
|
+
return `You're back in your body in ${bodyRoom.name}; the ${item.name} ${holds} ${where} -- dog-brain on the stick, holding until you "recall" it.${hint(' ("recall" brings it to your side; "stow" packs it away.)')}`;
|
|
216
237
|
}
|
|
217
238
|
/** The recon sweep ("drone"): nowhere to go, launch, and return. */
|
|
218
239
|
export function droneReconNowhereLine(item) {
|
|
@@ -298,7 +298,7 @@ export function enterDrone(scene, actor, drone, mode) {
|
|
|
298
298
|
: ` Cold-sim: hits on the hull sting through the link as stun, half strength.`;
|
|
299
299
|
return [
|
|
300
300
|
`The rig takes hold and the world snaps to sensor-sight -- you ARE the ${drone.name} now, ${droneJumpInFragment(drone)}, your body slumped and empty in ${actor.bodyRoom.name}.${hotWarning}${hullNote}`,
|
|
301
|
-
hint(`Move with "go" (you fly the meat world; locked doors still stop an airframe), "look" through the sensors, "jump" again to drop back into your body -- the frame stays out on its dog-brain where you leave it; "recall" brings it
|
|
301
|
+
hint(`Move with "go" (you fly the meat world; locked doors still stop an airframe), "look" through the sensors, "jump" again to drop back into your body -- the frame stays out on its dog-brain where you leave it; "recall" brings it to your side, "stow" packs it away.`),
|
|
302
302
|
].filter(l => l.length > 0);
|
|
303
303
|
}
|
|
304
304
|
/**
|
|
@@ -361,6 +361,12 @@ export function leaveDrone(scene, actor, opts) {
|
|
|
361
361
|
if (shell) {
|
|
362
362
|
shell.atSpot = frameSpot;
|
|
363
363
|
shell.atCell = frameCell;
|
|
364
|
+
// It HOLDS there (Game.companions.holding): a frame you left in
|
|
365
|
+
// the next room must not teleport to your heel on your next step
|
|
366
|
+
// -- "recall" fetches it.
|
|
367
|
+
const entry = game.companions.find(c => c.npc === shell);
|
|
368
|
+
if (entry)
|
|
369
|
+
entry.holding = true;
|
|
364
370
|
Logger.getInstance().write(`${drone.name} re-shelled on its dog-brain in ${frameRoom.name} after ${actor.name} jumped out.`);
|
|
365
371
|
}
|
|
366
372
|
lines.push(shell && drone
|
|
@@ -858,6 +858,15 @@ export function standingOccupants(room, mover) {
|
|
|
858
858
|
const offPlane = new Set(room.getActors()
|
|
859
859
|
.filter(a => !onTheFloor(a) && a.bodyRoom !== room)
|
|
860
860
|
.map(a => a.name));
|
|
861
|
+
// YOUR OWN SHELL IS NEVER IN YOUR WAY (transcript 2026-09-06: a drone
|
|
862
|
+
// at heel sat on its master's next square -- "You can't step east --
|
|
863
|
+
// S-K Direktionssekretar (autopilot)'s in the way", a dozen times --
|
|
864
|
+
// and read as the frame refusing to follow). A companion shell heels
|
|
865
|
+
// AT its master's spot by construction (go.ts), so of course it is
|
|
866
|
+
// standing where the master wants to go next; it yields instead
|
|
867
|
+
// (yieldSquareToMaster, called by the step). Hired crew are people
|
|
868
|
+
// (ruling 2026-08-25) and still hold their square.
|
|
869
|
+
const heels = new Set(mover ? room.getActors().filter(a => heelsTo(a, mover)).map(a => a.name) : []);
|
|
861
870
|
for (const [name, c] of seatingIn(room).entries()) {
|
|
862
871
|
if (mover && name === mover.name)
|
|
863
872
|
continue;
|
|
@@ -865,10 +874,40 @@ export function standingOccupants(room, mover) {
|
|
|
865
874
|
continue;
|
|
866
875
|
if (offPlane.has(name))
|
|
867
876
|
continue;
|
|
877
|
+
if (heels.has(name))
|
|
878
|
+
continue;
|
|
868
879
|
out.set(key(c), name);
|
|
869
880
|
}
|
|
870
881
|
return out;
|
|
871
882
|
}
|
|
883
|
+
/** Is `a` a companion SHELL of `master` -- a drone, spirit, sprite or
|
|
884
|
+
* agent riding their bond? Hired crew (kind 'ally') are people and
|
|
885
|
+
* answer no. */
|
|
886
|
+
export function heelsTo(a, master) {
|
|
887
|
+
const shell = a;
|
|
888
|
+
return shell.allyOf === master.name && shell.companionKind !== undefined && shell.companionKind !== 'ally';
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* After `master` steps onto a square one of their own shells was
|
|
892
|
+
* holding, the shell gives it up: its cell is released so seatingIn
|
|
893
|
+
* finds it the nearest free square at the master's spot. Returns the
|
|
894
|
+
* shells that moved. */
|
|
895
|
+
export function yieldSquareToMaster(room, master) {
|
|
896
|
+
const here = master.atCell ? key(master.atCell) : undefined;
|
|
897
|
+
if (!here)
|
|
898
|
+
return [];
|
|
899
|
+
const moved = [];
|
|
900
|
+
for (const a of room.getActors()) {
|
|
901
|
+
if (!heelsTo(a, master))
|
|
902
|
+
continue;
|
|
903
|
+
if (!a.atCell || key(a.atCell) !== here)
|
|
904
|
+
continue;
|
|
905
|
+
a.atCell = undefined;
|
|
906
|
+
a.atSpot = master.atSpot;
|
|
907
|
+
moved.push(a);
|
|
908
|
+
}
|
|
909
|
+
return moved;
|
|
910
|
+
}
|
|
872
911
|
/** The cell `mover` would end up in on arriving at `spotName` -- their
|
|
873
912
|
* seat around that fixture, with everyone else's seats already taken.
|
|
874
913
|
* This, not the fixture's anchor, is where movement actually paths to:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.133.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.",
|