termi-kids 0.2.2 → 0.2.4
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/dist/surfaces/buildGame.js +31 -8
- package/dist/surfaces/home.js +14 -164
- package/package.json +1 -1
|
@@ -86,6 +86,14 @@ async function gatherHelpAnswers(idea) {
|
|
|
86
86
|
}
|
|
87
87
|
return answers;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Merge typed input with an optional draft. Empty typing keeps the draft so
|
|
91
|
+
* kids can run a suggested prompt without retyping it.
|
|
92
|
+
*/
|
|
93
|
+
export function resolvePromptInput(typed, draft) {
|
|
94
|
+
const resolved = typed.trim() || draft.trim();
|
|
95
|
+
return resolved.length > 0 ? resolved : null;
|
|
96
|
+
}
|
|
89
97
|
async function obtainPrompt(idea) {
|
|
90
98
|
const path = await p.select({
|
|
91
99
|
message: 'How do you want to tell Termi what to build?',
|
|
@@ -110,26 +118,41 @@ async function obtainPrompt(idea) {
|
|
|
110
118
|
return null;
|
|
111
119
|
}
|
|
112
120
|
draft = suggestPromptFromAnswers(idea, answers);
|
|
121
|
+
}
|
|
122
|
+
// When we already have a draft, do not force a long single-line text field.
|
|
123
|
+
// clack text() is unreliable for long initialValue (looks like a placeholder
|
|
124
|
+
// and Enter can fail validate). Offer a clear Run vs Edit choice first.
|
|
125
|
+
if (draft.length > 0) {
|
|
113
126
|
p.note(draft, 'Suggested prompt');
|
|
127
|
+
const action = await p.select({
|
|
128
|
+
message: 'Use this prompt?',
|
|
129
|
+
options: [
|
|
130
|
+
{ value: 'run', label: 'Run it' },
|
|
131
|
+
{ value: 'edit', label: 'Edit it first' },
|
|
132
|
+
],
|
|
133
|
+
initialValue: 'run',
|
|
134
|
+
});
|
|
135
|
+
if (p.isCancel(action)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
if (action === 'run') {
|
|
139
|
+
return draft;
|
|
140
|
+
}
|
|
114
141
|
}
|
|
115
|
-
// clack: initialValue pre-fills the field; defaultValue is only applied
|
|
116
|
-
// AFTER validate. Empty Enter with only defaultValue fails validate, so
|
|
117
|
-
// we pre-fill and also accept empty when a draft already exists.
|
|
118
142
|
const text = await p.text({
|
|
119
|
-
message:
|
|
143
|
+
message: draft.length > 0 ? 'Edit your prompt.' : 'Type your prompt.',
|
|
120
144
|
initialValue: draft.length > 0 ? draft : undefined,
|
|
121
145
|
defaultValue: draft.length > 0 ? draft : undefined,
|
|
122
146
|
placeholder: draft.length > 0 ? undefined : 'Make a fun browser game where...',
|
|
123
147
|
validate: (value) => {
|
|
124
|
-
const resolved = (value ?? ''
|
|
125
|
-
return resolved
|
|
148
|
+
const resolved = resolvePromptInput(value ?? '', draft);
|
|
149
|
+
return resolved !== null ? undefined : 'Need a prompt to build.';
|
|
126
150
|
},
|
|
127
151
|
});
|
|
128
152
|
if (p.isCancel(text)) {
|
|
129
153
|
return null;
|
|
130
154
|
}
|
|
131
|
-
|
|
132
|
-
return resolved.length > 0 ? resolved : null;
|
|
155
|
+
return resolvePromptInput(text, draft);
|
|
133
156
|
}
|
|
134
157
|
function listKidFileSummaries(project) {
|
|
135
158
|
const out = [];
|
package/dist/surfaces/home.js
CHANGED
|
@@ -12,9 +12,6 @@ import * as p from '@clack/prompts';
|
|
|
12
12
|
import { atomicWriteFileSync, termiHome } from '../config/paths.js';
|
|
13
13
|
import { saveSettings } from '../config/settings.js';
|
|
14
14
|
import { consumeGuardReadyNotice, guardFetchState, guardProgressBar, } from '../safety/guarddownload.js';
|
|
15
|
-
import { nameIsOkay } from '../safety/prefilter.js';
|
|
16
|
-
import { scaffolds, scaffoldById } from '../projects/scaffolds/index.js';
|
|
17
|
-
import { suggestProjectNames } from '../setup/wizard.js';
|
|
18
15
|
import { BADGES, celebrate, confetti } from '../ui/celebrate.js';
|
|
19
16
|
import { mascot } from '../ui/mascot.js';
|
|
20
17
|
import { style } from '../ui/theme.js';
|
|
@@ -99,174 +96,27 @@ export function recapFromTermiMd(text) {
|
|
|
99
96
|
}
|
|
100
97
|
return null;
|
|
101
98
|
}
|
|
102
|
-
async function
|
|
99
|
+
async function loadStore() {
|
|
103
100
|
try {
|
|
104
|
-
|
|
105
|
-
const create = await import('../projects/create.js');
|
|
106
|
-
return { store, create };
|
|
101
|
+
return await import('../projects/store.js');
|
|
107
102
|
}
|
|
108
103
|
catch {
|
|
109
104
|
return null;
|
|
110
105
|
}
|
|
111
106
|
}
|
|
112
|
-
const CUSTOM_NAME = '__custom__';
|
|
113
|
-
async function pickProjectName(themeLabel) {
|
|
114
|
-
const suggestions = suggestProjectNames(themeLabel);
|
|
115
|
-
const pick = await p.select({
|
|
116
|
-
message: 'Pick a name.',
|
|
117
|
-
options: [
|
|
118
|
-
...suggestions.map((name) => ({ value: name, label: name })),
|
|
119
|
-
{ value: CUSTOM_NAME, label: 'Type my own name' },
|
|
120
|
-
],
|
|
121
|
-
});
|
|
122
|
-
if (p.isCancel(pick)) {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
if (pick !== CUSTOM_NAME) {
|
|
126
|
-
return pick;
|
|
127
|
-
}
|
|
128
|
-
const typed = await p.text({
|
|
129
|
-
message: 'What is its name?',
|
|
130
|
-
validate: (value) => {
|
|
131
|
-
const trimmed = (value ?? '').trim();
|
|
132
|
-
if (trimmed.length === 0)
|
|
133
|
-
return 'It needs a name.';
|
|
134
|
-
if (!nameIsOkay(trimmed))
|
|
135
|
-
return 'That name will not work. Try another one.';
|
|
136
|
-
return undefined;
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
if (p.isCancel(typed)) {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
return typed.trim();
|
|
143
|
-
}
|
|
144
|
-
async function remixFlow(store, create) {
|
|
145
|
-
const metas = store.listProjects();
|
|
146
|
-
const sourceSlug = await p.select({
|
|
147
|
-
message: 'Which project do you want to remix?',
|
|
148
|
-
options: metas.map((m) => ({ value: m.slug, label: m.prettyName })),
|
|
149
|
-
});
|
|
150
|
-
if (p.isCancel(sourceSlug)) {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
const source = store.openProject(sourceSlug);
|
|
154
|
-
if (source === null) {
|
|
155
|
-
p.log.warn('I could not open that one.');
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
for (;;) {
|
|
159
|
-
const name = await p.text({
|
|
160
|
-
message: 'Name your remix.',
|
|
161
|
-
validate: (value) => ((value ?? '').trim().length > 0 ? undefined : 'It needs a name.'),
|
|
162
|
-
});
|
|
163
|
-
if (p.isCancel(name)) {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
const trimmed = name.trim();
|
|
167
|
-
if (create.slugifyName(trimmed).collision) {
|
|
168
|
-
p.log.warn('That name is taken. Try another one.');
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
const made = create.createProject(source.meta.scaffoldId, source.meta.themeId, trimmed);
|
|
172
|
-
for (const file of source.listKidFiles()) {
|
|
173
|
-
const content = source.readFile(file.relPath);
|
|
174
|
-
if (content !== null) {
|
|
175
|
-
made.project.writeFile(file.relPath, content);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
await awardBadge('remixer');
|
|
179
|
-
return made.project;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
107
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
108
|
+
* New project entry: always Build a game (blank shell + idea list).
|
|
109
|
+
* Multi-scaffold stock starters are no longer on the kid path.
|
|
185
110
|
*/
|
|
186
111
|
export async function runNewProject(settings) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
p.log.warn('Project tools are not ready yet.');
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
const { store, create } = mods;
|
|
194
|
-
const existing = store.listProjects();
|
|
195
|
-
const options = scaffolds.map((s) => ({
|
|
196
|
-
value: s.id,
|
|
197
|
-
label: `${s.emoji} ${s.label}`,
|
|
198
|
-
hint: s.ageNote,
|
|
199
|
-
}));
|
|
200
|
-
if (existing.length > 0) {
|
|
201
|
-
options.push({
|
|
202
|
-
value: '__remix__',
|
|
203
|
-
label: 'Remix one of your projects',
|
|
204
|
-
hint: 'Copy a project and make it new.',
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
const pick = await p.select({ message: 'What do you want to make?', options });
|
|
208
|
-
if (p.isCancel(pick)) {
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
if (pick === '__remix__') {
|
|
212
|
-
return remixFlow(store, create);
|
|
213
|
-
}
|
|
214
|
-
const scaffold = scaffoldById(pick);
|
|
215
|
-
if (scaffold === undefined) {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
const themeId = await p.select({
|
|
219
|
-
message: 'Pick a style.',
|
|
220
|
-
options: scaffold.themes.map((t) => ({ value: t.id, label: `${t.emoji} ${t.label}` })),
|
|
221
|
-
});
|
|
222
|
-
if (p.isCancel(themeId)) {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
const theme = scaffold.themes.find((t) => t.id === themeId);
|
|
226
|
-
if (theme === undefined) {
|
|
227
|
-
return null;
|
|
112
|
+
try {
|
|
113
|
+
const build = await import('./buildGame.js');
|
|
114
|
+
return await build.runBuildGame(settings);
|
|
228
115
|
}
|
|
229
|
-
|
|
230
|
-
|
|
116
|
+
catch {
|
|
117
|
+
p.log.warn('Build a game is taking a nap. Try again soon.');
|
|
231
118
|
return null;
|
|
232
119
|
}
|
|
233
|
-
const slugInfo = create.slugifyName(prettyName);
|
|
234
|
-
if (slugInfo.collision) {
|
|
235
|
-
const what = await p.select({
|
|
236
|
-
message: 'You already have a project with that name.',
|
|
237
|
-
options: [
|
|
238
|
-
{ value: 'open', label: 'Open it' },
|
|
239
|
-
{ value: 'suffix', label: `Call this one ${prettyName} 2` },
|
|
240
|
-
{ value: 'rename', label: 'Pick a new name' },
|
|
241
|
-
],
|
|
242
|
-
});
|
|
243
|
-
if (p.isCancel(what)) {
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
if (what === 'open') {
|
|
247
|
-
return store.openProject(slugInfo.slug);
|
|
248
|
-
}
|
|
249
|
-
if (what === 'suffix') {
|
|
250
|
-
prettyName = `${prettyName} 2`;
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
const again = await pickProjectName(theme.label);
|
|
254
|
-
if (again === null) {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
prettyName = again;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
const made = create.createProject(scaffold.id, theme.id, prettyName);
|
|
261
|
-
await awardBadge('first-project');
|
|
262
|
-
if (store.listProjects().length >= 5) {
|
|
263
|
-
await awardBadge('five-projects');
|
|
264
|
-
}
|
|
265
|
-
const starters = made.starterPrompts.slice(0, 2);
|
|
266
|
-
if (starters.length > 0) {
|
|
267
|
-
p.note(starters.map((line) => `- ${line}`).join('\n'), 'Try saying');
|
|
268
|
-
}
|
|
269
|
-
return made.project;
|
|
270
120
|
}
|
|
271
121
|
/**
|
|
272
122
|
* Opens the chat for a project and keeps going while the kid asks for
|
|
@@ -445,12 +295,12 @@ export async function showHome(settings) {
|
|
|
445
295
|
}
|
|
446
296
|
}
|
|
447
297
|
async function openPicked(settings) {
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
298
|
+
const store = await loadStore();
|
|
299
|
+
if (store === null) {
|
|
450
300
|
p.log.warn('Project tools are not ready yet.');
|
|
451
301
|
return;
|
|
452
302
|
}
|
|
453
|
-
const metas =
|
|
303
|
+
const metas = store.listProjects();
|
|
454
304
|
if (metas.length === 0) {
|
|
455
305
|
p.log.info('No games yet. Pick "Build a game"!');
|
|
456
306
|
return;
|
|
@@ -460,14 +310,14 @@ async function openPicked(settings) {
|
|
|
460
310
|
? settings.lastProjectSlug
|
|
461
311
|
: first?.slug;
|
|
462
312
|
const pick = await p.select({
|
|
463
|
-
message: 'Pick a
|
|
313
|
+
message: 'Pick a game.',
|
|
464
314
|
options: metas.map((m) => ({ value: m.slug, label: m.prettyName })),
|
|
465
315
|
...(initial !== undefined ? { initialValue: initial } : {}),
|
|
466
316
|
});
|
|
467
317
|
if (p.isCancel(pick)) {
|
|
468
318
|
return;
|
|
469
319
|
}
|
|
470
|
-
const project =
|
|
320
|
+
const project = store.openProject(pick);
|
|
471
321
|
if (project === null) {
|
|
472
322
|
p.log.warn('I could not open that one.');
|
|
473
323
|
return;
|
package/package.json
CHANGED