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.
@@ -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: path === 'write' ? 'Type your prompt.' : 'Edit the prompt, or press Enter to run it.',
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 ?? '').trim() || draft.trim();
125
- return resolved.length > 0 ? undefined : 'Need a prompt to build.';
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
- const resolved = text.trim() || draft.trim();
132
- return resolved.length > 0 ? resolved : null;
155
+ return resolvePromptInput(text, draft);
133
156
  }
134
157
  function listKidFileSummaries(project) {
135
158
  const out = [];
@@ -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 loadProjectModules() {
99
+ async function loadStore() {
103
100
  try {
104
- const store = await import('../projects/store.js');
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
- * The full new-project flow: scaffold, theme, name (with collision help),
184
- * remix option, badges. Returns the created project, or null on cancel.
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
- void settings;
188
- const mods = await loadProjectModules();
189
- if (mods === null) {
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
- let prettyName = await pickProjectName(theme.label);
230
- if (prettyName === null) {
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 mods = await loadProjectModules();
449
- if (mods === null) {
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 = mods.store.listProjects();
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 project.',
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 = mods.store.openProject(pick);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termi-kids",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Termi is a friendly coding buddy for kids. Build browser games with AI prompts, Learn AI lessons, and strong local safety rails.",
5
5
  "keywords": [
6
6
  "kids",