twin-cli 0.3.1 → 0.4.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/README.md +29 -19
- package/bin/twin.js +4 -0
- package/package.json +1 -1
- package/src/build.js +130 -7
- package/src/plan.js +2 -1
- package/src/steer.js +37 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Old TDD meant write the tests first. New TDD means write the twin first.
|
|
4
4
|
|
|
5
|
-
A `.twin` file
|
|
5
|
+
A `.twin` file is the `.env` for your taste. Drop it into any project. Your AI does not wait for instructions. It decides what to build next and builds it. Not human in the loop. Twin in the loop.
|
|
6
6
|
|
|
7
7
|
## The Problem
|
|
8
8
|
|
|
@@ -18,8 +18,8 @@ The root cause: agents do not know what you would build next. They can execute.
|
|
|
18
18
|
|
|
19
19
|
Three commands.
|
|
20
20
|
|
|
21
|
-
```
|
|
22
|
-
twin init #
|
|
21
|
+
```bash
|
|
22
|
+
twin init # your taste, in a file
|
|
23
23
|
twin plan # your twin decides what to build next
|
|
24
24
|
twin build # your twin builds it without you
|
|
25
25
|
```
|
|
@@ -33,6 +33,8 @@ Run `twin plan` once and you get the next batch of features you would have chose
|
|
|
33
33
|
- **Node.js 18+** — [nodejs.org](https://nodejs.org)
|
|
34
34
|
- **Claude Code** — [docs.anthropic.com](https://docs.anthropic.com/en/docs/claude-code) (powers all three commands)
|
|
35
35
|
|
|
36
|
+
Run everything with `npx twin-cli` — no global install needed. This keeps you on the latest version automatically.
|
|
37
|
+
|
|
36
38
|
## Quick Start
|
|
37
39
|
|
|
38
40
|
Each twin project starts in its own folder. Create one, then run from inside it.
|
|
@@ -82,13 +84,13 @@ npx twin-cli build
|
|
|
82
84
|
# → Picks the next story, builds it, marks it done
|
|
83
85
|
# → You watch it happen in real time
|
|
84
86
|
|
|
85
|
-
# Want more
|
|
87
|
+
# Want more? Plan again.
|
|
86
88
|
npx twin-cli plan
|
|
87
89
|
# → Sees what is done, generates the next batch
|
|
88
90
|
npx twin-cli build
|
|
89
91
|
```
|
|
90
92
|
|
|
91
|
-
Your `.twin` file is portable. Copy it into any new project and run `twin plan` to start.
|
|
93
|
+
Your `.twin` file is portable. Copy it into any new project and run `npx twin-cli plan` to start.
|
|
92
94
|
|
|
93
95
|
## Project Ideas
|
|
94
96
|
|
|
@@ -106,12 +108,12 @@ Twin works best on new projects. Some things to try:
|
|
|
106
108
|
|
|
107
109
|
Your twin drives the whole cycle.
|
|
108
110
|
|
|
109
|
-
1. **`twin init`** —
|
|
110
|
-
2. **`twin plan`** — your twin generates
|
|
111
|
+
1. **`twin init`** — your taste, in a file (once)
|
|
112
|
+
2. **`twin plan`** — your twin generates the next batch of stories, matched to how you prioritize
|
|
111
113
|
3. **`twin build`** — your twin builds on its own, updating `prd.json` as stories complete
|
|
112
114
|
4. **`twin plan` again** — your twin reads what shipped and decides what comes next
|
|
113
115
|
|
|
114
|
-
You did not write a task list.
|
|
116
|
+
You did not write a task list. Your twin wrote it. You did not pick the next feature. Your twin picked it.
|
|
115
117
|
|
|
116
118
|
Each iteration, Claude Code starts fresh but reads the files on disk. Your twin. The PRD. A progress log. The files are the memory. Your taste stays consistent across runs.
|
|
117
119
|
|
|
@@ -119,7 +121,7 @@ Each iteration, Claude Code starts fresh but reads the files on disk. Your twin.
|
|
|
119
121
|
|
|
120
122
|
### `twin init`
|
|
121
123
|
|
|
122
|
-
Asks your name, then 5 questions about how you build
|
|
124
|
+
Asks your name, then 5 questions about how you build. Generates `yourname.twin`.
|
|
123
125
|
|
|
124
126
|
### `twin plan`
|
|
125
127
|
|
|
@@ -136,17 +138,29 @@ Runs an autonomous build loop using Claude Code. Each iteration:
|
|
|
136
138
|
- Picks the next story to build (guided by your taste)
|
|
137
139
|
- Builds it, commits, and marks it done in `prd.json`
|
|
138
140
|
- Appends learnings to `progress.md`
|
|
139
|
-
- Repeats until all stories are done or
|
|
141
|
+
- Repeats until all stories are done or the story limit is hit
|
|
140
142
|
|
|
141
143
|
```bash
|
|
142
|
-
twin build # build the current stories (default: 3)
|
|
143
|
-
twin build --loop # build, plan, build — fully autonomous
|
|
144
|
-
twin build --loop --stories 20 # stop after 20 stories
|
|
145
|
-
twin build --loop --minutes 30 # stop after 30 minutes
|
|
144
|
+
npx twin-cli build # build the current stories (default: 3)
|
|
145
|
+
npx twin-cli build --loop # build, plan, build — fully autonomous
|
|
146
|
+
npx twin-cli build --loop --stories 20 # stop after 20 stories
|
|
147
|
+
npx twin-cli build --loop --minutes 30 # stop after 30 minutes
|
|
146
148
|
```
|
|
147
149
|
|
|
148
150
|
Requires Claude Code installed and available in your PATH.
|
|
149
151
|
|
|
152
|
+
### `twin steer`
|
|
153
|
+
|
|
154
|
+
You have something to say but don't want to write a user story yourself. Just say it:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
twin steer "I want users to be able to share their progress on Twitter"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Or run `twin steer` with no arguments to type or dictate a longer message.
|
|
161
|
+
|
|
162
|
+
Your twin turns it into stories in `prd.json` and updates your `.twin` file with anything it can infer about your taste. Works any time — before a build, between runs, or from a second terminal while a build is running.
|
|
163
|
+
|
|
150
164
|
## What Goes in a `.twin` File
|
|
151
165
|
|
|
152
166
|
- **Execution bias** — do you plan first or build first? Ship rough or wait for polish?
|
|
@@ -161,11 +175,7 @@ Requires Claude Code installed and available in your PATH.
|
|
|
161
175
|
- Your tech stack (that goes in project config)
|
|
162
176
|
- Your roadmap (that goes in a PRD)
|
|
163
177
|
|
|
164
|
-
The twin
|
|
165
|
-
|
|
166
|
-
## What Comes Next
|
|
167
|
-
|
|
168
|
-
`twin tweak` for natural language updates. `twin share` for publishing your taste publicly.
|
|
178
|
+
The twin holds your taste. The project files hold what you are building. The twin is the founder. The project files are the company.
|
|
169
179
|
|
|
170
180
|
## Notes
|
|
171
181
|
|
package/bin/twin.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { init } from '../src/init.js';
|
|
4
4
|
import { plan } from '../src/plan.js';
|
|
5
5
|
import { build } from '../src/build.js';
|
|
6
|
+
import { steer } from '../src/steer.js';
|
|
6
7
|
|
|
7
8
|
const command = process.argv[2];
|
|
8
9
|
|
|
@@ -23,6 +24,8 @@ if (!command || command === 'init') {
|
|
|
23
24
|
const maxStories = storiesFlag ? parseInt(storiesFlag, 10) : (loop ? Infinity : 3);
|
|
24
25
|
const maxMinutes = minutesFlag ? parseInt(minutesFlag, 10) : null;
|
|
25
26
|
build({ maxStories, loop, maxMinutes });
|
|
27
|
+
} else if (command === 'steer') {
|
|
28
|
+
steer();
|
|
26
29
|
} else if (command === '--help' || command === '-h') {
|
|
27
30
|
console.log(`
|
|
28
31
|
twin - your twin builds while you sleep
|
|
@@ -34,6 +37,7 @@ Usage:
|
|
|
34
37
|
twin build --loop Build, plan, build — fully autonomous
|
|
35
38
|
twin build --loop --stories 20 Stop after 20 stories
|
|
36
39
|
twin build --loop --minutes 30 Stop after 30 minutes
|
|
40
|
+
twin steer [message] Tell your twin what to build next
|
|
37
41
|
twin --help Show this message
|
|
38
42
|
`);
|
|
39
43
|
} else {
|
package/package.json
CHANGED
package/src/build.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { readFile, writeFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { unlinkSync } from 'node:fs';
|
|
3
4
|
import { resolve } from 'node:path';
|
|
4
5
|
import { runPlan } from './plan.js';
|
|
6
|
+
import { callLLM } from './llm.js';
|
|
5
7
|
|
|
6
8
|
const COMPLETION_SIGNAL = '<twin>STORY_COMPLETE</twin>';
|
|
7
9
|
const ALL_DONE_SIGNAL = '<twin>ALL_COMPLETE</twin>';
|
|
@@ -218,6 +220,84 @@ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
|
218
220
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
219
221
|
const bar = dim('─'.repeat(60));
|
|
220
222
|
|
|
223
|
+
async function processSteer(cwd, twinPath, prdPath) {
|
|
224
|
+
const steerPath = resolve(cwd, 'steer.md');
|
|
225
|
+
const steerContent = await readIfExists(steerPath);
|
|
226
|
+
if (!steerContent?.trim()) return;
|
|
227
|
+
|
|
228
|
+
const twinContent = await readFile(twinPath, 'utf-8');
|
|
229
|
+
const twinFilename = twinPath.split('/').pop();
|
|
230
|
+
const prdContent = await readFile(prdPath, 'utf-8');
|
|
231
|
+
const prd = JSON.parse(prdContent);
|
|
232
|
+
|
|
233
|
+
const maxNum = prd.userStories.reduce((max, s) => {
|
|
234
|
+
const m = s.id?.match(/\d+/);
|
|
235
|
+
return m ? Math.max(max, parseInt(m[0], 10)) : max;
|
|
236
|
+
}, 0);
|
|
237
|
+
const nextId = `US-${String(maxNum + 1).padStart(3, '0')}`;
|
|
238
|
+
|
|
239
|
+
// Send trimmed prd (id + title + status only) to reduce token count
|
|
240
|
+
const prdSummary = prd.userStories
|
|
241
|
+
.map((s) => `${s.id}: ${s.title} (${s.status})`)
|
|
242
|
+
.join('\n');
|
|
243
|
+
|
|
244
|
+
const result = await callLLM(
|
|
245
|
+
'You process developer steering input for a build loop. Output valid JSON only — no prose, no markdown fences.',
|
|
246
|
+
[
|
|
247
|
+
'Steering input from developer:',
|
|
248
|
+
steerContent,
|
|
249
|
+
'',
|
|
250
|
+
`Existing stories:\n${prdSummary}`,
|
|
251
|
+
'',
|
|
252
|
+
`${twinFilename}:\n${twinContent}`,
|
|
253
|
+
'',
|
|
254
|
+
`Next available story id: ${nextId} (increment for each additional story)`,
|
|
255
|
+
'',
|
|
256
|
+
'Output JSON with this exact shape:',
|
|
257
|
+
'{',
|
|
258
|
+
' "newStories": [], // stories to add to prd.json (id, title, userStory, acceptanceCriteria array, status:"open"), empty array if none',
|
|
259
|
+
' "twinAppend": null // text to append to the twin file if this input reveals something clear about the developer\'s taste, or null if nothing clear can be inferred',
|
|
260
|
+
'}',
|
|
261
|
+
].join('\n')
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
let json;
|
|
265
|
+
try {
|
|
266
|
+
json = JSON.parse(result.replace(/^```(?:json)?\n?/m, '').replace(/\n?```$/m, '').trim());
|
|
267
|
+
} catch {
|
|
268
|
+
console.log(dim(' steer: could not parse response, skipping.'));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (json.newStories?.length > 0) {
|
|
273
|
+
prd.userStories.push(...json.newStories);
|
|
274
|
+
await writeFile(prdPath, JSON.stringify(prd, null, 2), 'utf-8');
|
|
275
|
+
console.log('');
|
|
276
|
+
for (const s of json.newStories) {
|
|
277
|
+
console.log(dim(' + ') + `${s.id} ${s.title}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (json.twinAppend) {
|
|
282
|
+
const current = await readFile(twinPath, 'utf-8');
|
|
283
|
+
await writeFile(twinPath, current.trimEnd() + '\n\n' + json.twinAppend + '\n', 'utf-8');
|
|
284
|
+
console.log(dim(` Updated ${twinFilename}`));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Clear steer.md so it isn't re-processed next cycle
|
|
288
|
+
await writeFile(steerPath, '', 'utf-8');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function logPriorityJustification(twinContent, prdContent, storyNum, synthesisPath) {
|
|
292
|
+
const justification = await callLLM(
|
|
293
|
+
'You are a prioritization oracle. Answer in 1-3 sentences only. No preamble.',
|
|
294
|
+
`Twin file:\n${twinContent}\n\nprd.json:\n${prdContent}\n\nIf you could only ship one story this cycle, which would it be and why? Reference the twin file reasoning explicitly.`
|
|
295
|
+
);
|
|
296
|
+
const existing = await readIfExists(synthesisPath) || '';
|
|
297
|
+
const entry = `\n## Story ${storyNum} — ${new Date().toISOString()}\n\n**Priority justification:** ${justification}\n`;
|
|
298
|
+
await writeFile(synthesisPath, existing + entry, 'utf-8');
|
|
299
|
+
}
|
|
300
|
+
|
|
221
301
|
const STORY_RETRIES = 2;
|
|
222
302
|
const STORY_RETRY_DELAYS = [10_000, 30_000]; // 10s, then 30s
|
|
223
303
|
|
|
@@ -225,6 +305,28 @@ function sleep(ms) {
|
|
|
225
305
|
return new Promise((r) => setTimeout(r, ms));
|
|
226
306
|
}
|
|
227
307
|
|
|
308
|
+
// Show an overwriting status line while an async operation runs, then clear it.
|
|
309
|
+
// msg can be a string, or an array of [afterSeconds, message] milestones.
|
|
310
|
+
async function withStatus(msg, fn) {
|
|
311
|
+
const milestones = Array.isArray(msg)
|
|
312
|
+
? msg.slice().sort((a, b) => a[0] - b[0])
|
|
313
|
+
: [[0, msg]];
|
|
314
|
+
const start = Date.now();
|
|
315
|
+
let shown = false;
|
|
316
|
+
const timer = setInterval(() => {
|
|
317
|
+
const elapsed = Math.round((Date.now() - start) / 1000);
|
|
318
|
+
const current = milestones.filter(([s]) => elapsed >= s).pop()[1];
|
|
319
|
+
process.stdout.write(`\r${dim(`${current} (${elapsed}s)`)}`);
|
|
320
|
+
shown = true;
|
|
321
|
+
}, 3_000);
|
|
322
|
+
try {
|
|
323
|
+
return await fn();
|
|
324
|
+
} finally {
|
|
325
|
+
clearInterval(timer);
|
|
326
|
+
if (shown) process.stdout.write('\r\x1b[K');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
228
330
|
function summary(totalBuilt, cycles, elapsed) {
|
|
229
331
|
const mins = Math.round(elapsed / 60000);
|
|
230
332
|
const time = mins > 0 ? ` in ${mins}m` : '';
|
|
@@ -237,6 +339,11 @@ export async function build({ maxStories = 3, loop = false, maxMinutes = null }
|
|
|
237
339
|
const startTime = Date.now();
|
|
238
340
|
const timeLimitMs = maxMinutes ? maxMinutes * 60 * 1000 : null;
|
|
239
341
|
|
|
342
|
+
// Write lock file so twin steer knows a build is running
|
|
343
|
+
const lockPath = resolve(cwd, '.twin-lock');
|
|
344
|
+
await writeFile(lockPath, String(process.pid), 'utf-8');
|
|
345
|
+
process.on('exit', () => { try { unlinkSync(lockPath); } catch {} });
|
|
346
|
+
|
|
240
347
|
// Clean exit on Ctrl+C — finish current story, then stop
|
|
241
348
|
let stopping = false;
|
|
242
349
|
process.on('SIGINT', () => {
|
|
@@ -261,7 +368,9 @@ export async function build({ maxStories = 3, loop = false, maxMinutes = null }
|
|
|
261
368
|
const prd = JSON.parse(prdContent);
|
|
262
369
|
const openStories = prd.userStories.filter((s) => s.status !== 'done');
|
|
263
370
|
if (openStories.length === 0 && !loop) {
|
|
264
|
-
console.log('All stories are done
|
|
371
|
+
console.log('All stories are done.\n');
|
|
372
|
+
console.log(' npx twin-cli plan # plan the next batch, then build');
|
|
373
|
+
console.log(' npx twin-cli build --loop # plan and build automatically\n');
|
|
265
374
|
process.exit(0);
|
|
266
375
|
}
|
|
267
376
|
// In loop mode with no open stories, the while loop will trigger planning
|
|
@@ -299,7 +408,15 @@ export async function build({ maxStories = 3, loop = false, maxMinutes = null }
|
|
|
299
408
|
// Re-read twin each cycle (user may tweak mid-run)
|
|
300
409
|
const twinContent = await readFile(twinPath, 'utf-8');
|
|
301
410
|
|
|
302
|
-
//
|
|
411
|
+
// Process any steering input before deciding what to build next
|
|
412
|
+
await withStatus([
|
|
413
|
+
[0, 'Applying your steer...'],
|
|
414
|
+
[20, 'Still working...'],
|
|
415
|
+
[60, 'Your message is detailed — taking a moment...'],
|
|
416
|
+
[120, 'Almost there...'],
|
|
417
|
+
], () => processSteer(cwd, twinPath, prdPath)).catch(() => {});
|
|
418
|
+
|
|
419
|
+
// Re-read prd.json (steer or previous story may have updated it)
|
|
303
420
|
const currentPrdContent = await readFile(prdPath, 'utf-8');
|
|
304
421
|
const currentPrd = JSON.parse(currentPrdContent);
|
|
305
422
|
const remaining = currentPrd.userStories.filter((s) => s.status !== 'done');
|
|
@@ -312,8 +429,8 @@ export async function build({ maxStories = 3, loop = false, maxMinutes = null }
|
|
|
312
429
|
console.log(bold(' All stories complete'));
|
|
313
430
|
console.log(dim(` ${summary(totalBuilt, cycle, Date.now() - startTime)}`));
|
|
314
431
|
console.log(bar);
|
|
315
|
-
console.log('\
|
|
316
|
-
console.log(' npx twin-cli plan\n');
|
|
432
|
+
console.log('\n npx twin-cli plan # plan the next batch, then build');
|
|
433
|
+
console.log(' npx twin-cli build --loop # plan and build automatically\n');
|
|
317
434
|
break;
|
|
318
435
|
}
|
|
319
436
|
|
|
@@ -321,11 +438,10 @@ export async function build({ maxStories = 3, loop = false, maxMinutes = null }
|
|
|
321
438
|
console.log('');
|
|
322
439
|
console.log(bar);
|
|
323
440
|
console.log(bold(` Cycle ${cycle} complete`));
|
|
324
|
-
console.log(dim(' Your twin is planning the next batch...'));
|
|
325
441
|
console.log(bar);
|
|
326
442
|
console.log('');
|
|
327
443
|
|
|
328
|
-
const newStories = await runPlan(cwd);
|
|
444
|
+
const newStories = await withStatus('Planning...', () => runPlan(cwd));
|
|
329
445
|
|
|
330
446
|
if (newStories.length === 0) {
|
|
331
447
|
console.log(bar);
|
|
@@ -370,6 +486,12 @@ export async function build({ maxStories = 3, loop = false, maxMinutes = null }
|
|
|
370
486
|
console.log('');
|
|
371
487
|
|
|
372
488
|
const progressContent = await readIfExists(resolve(cwd, 'progress.md'));
|
|
489
|
+
|
|
490
|
+
// Log priority justification to synthesis.md before building
|
|
491
|
+
await withStatus('Thinking...', () =>
|
|
492
|
+
logPriorityJustification(twinContent, currentPrdContent, storyNum, resolve(cwd, 'synthesis.md'))
|
|
493
|
+
).catch(() => {});
|
|
494
|
+
|
|
373
495
|
const prompt = buildPrompt(twinContent, twinFilename, currentPrdContent, progressContent);
|
|
374
496
|
|
|
375
497
|
let succeeded = false;
|
|
@@ -413,7 +535,8 @@ export async function build({ maxStories = 3, loop = false, maxMinutes = null }
|
|
|
413
535
|
console.log(dim(` ${summary(totalBuilt, cycle, Date.now() - startTime)}`));
|
|
414
536
|
console.log(bar);
|
|
415
537
|
if (!loop) {
|
|
416
|
-
console.log('\
|
|
538
|
+
console.log('\n npx twin-cli build # keep building');
|
|
539
|
+
console.log(' npx twin-cli build --loop # build and plan automatically\n');
|
|
417
540
|
}
|
|
418
541
|
console.log('');
|
|
419
542
|
}
|
package/src/plan.js
CHANGED
|
@@ -204,5 +204,6 @@ export async function plan() {
|
|
|
204
204
|
console.log(`---`);
|
|
205
205
|
console.log(`Wrote ${prdPath}`);
|
|
206
206
|
console.log(`\nNext step — let your twin build it:`);
|
|
207
|
-
console.log(` npx twin-cli build`);
|
|
207
|
+
console.log(` npx twin-cli build # build the next 3 stories`);
|
|
208
|
+
console.log(` npx twin-cli build --loop # build and plan automatically`);
|
|
208
209
|
}
|
package/src/steer.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { createPrompter } from './prompt.js';
|
|
5
|
+
import { build } from './build.js';
|
|
6
|
+
|
|
7
|
+
export async function steer() {
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
const steerPath = resolve(cwd, 'steer.md');
|
|
10
|
+
|
|
11
|
+
// Accept message as inline args or fall back to interactive prompt
|
|
12
|
+
const inline = process.argv.slice(3).join(' ').trim();
|
|
13
|
+
let message;
|
|
14
|
+
|
|
15
|
+
if (inline) {
|
|
16
|
+
message = inline;
|
|
17
|
+
} else {
|
|
18
|
+
const prompter = createPrompter();
|
|
19
|
+
message = await prompter.ask('What do you want to tell your twin?');
|
|
20
|
+
prompter.close();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!message) {
|
|
24
|
+
console.log('No message provided. Nothing written.');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await writeFile(steerPath, message, 'utf-8');
|
|
29
|
+
|
|
30
|
+
const lockPath = resolve(cwd, '.twin-lock');
|
|
31
|
+
if (existsSync(lockPath)) {
|
|
32
|
+
console.log('\nGot it. Your twin will pick this up at the next story boundary.\n');
|
|
33
|
+
} else {
|
|
34
|
+
console.log('');
|
|
35
|
+
await build({ maxStories: 1 });
|
|
36
|
+
}
|
|
37
|
+
}
|