create-procode 0.0.0-stage → 1.1.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.
Files changed (68) hide show
  1. package/README.md +266 -2
  2. package/bin/create-procode.mjs +9 -0
  3. package/package.json +30 -4
  4. package/src/args.mjs +145 -0
  5. package/src/cli.mjs +514 -0
  6. package/src/deps.mjs +208 -0
  7. package/src/log.mjs +23 -0
  8. package/src/multiLanguage.mjs +409 -0
  9. package/src/prompt.mjs +60 -0
  10. package/src/scaffold.mjs +104 -0
  11. package/templates/base/README.md.tmpl +185 -0
  12. package/templates/base/_dotenv.local.example.tmpl +8 -0
  13. package/templates/base/_dotenv.tmpl +41 -0
  14. package/templates/base/_doteslintrc.cjs.tmpl +125 -0
  15. package/templates/base/_dotgitignore.tmpl +32 -0
  16. package/templates/base/_dotnvmrc.tmpl +1 -0
  17. package/templates/base/_dotvscode/extensions.json +3 -0
  18. package/templates/base/index.html.tmpl +18 -0
  19. package/templates/base/package.json.tmpl +15 -0
  20. package/templates/base/public/favicon.svg +4 -0
  21. package/templates/base/src/AdapterProvider/AdapterFactory.ts.tmpl +21 -0
  22. package/templates/base/src/AdapterProvider/SampleContacts.ts.tmpl +136 -0
  23. package/templates/base/src/CustomAction.ts.tmpl +47 -0
  24. package/templates/base/src/CustomAuthorizationProvider.ts.tmpl +77 -0
  25. package/templates/base/src/Factory/Actions/myAction.ts.tmpl +51 -0
  26. package/templates/base/src/Factory/Actions/sampleContacts.ts.tmpl +244 -0
  27. package/templates/base/src/Factory/Actions/setField.ts.tmpl +20 -0
  28. package/templates/base/src/Factory/CustomRuleEffectFactory.ts.tmpl +30 -0
  29. package/templates/base/src/Factory/CustomValidationFactory.tsx.tmpl +24 -0
  30. package/templates/base/src/Factory/CustomWidgetFactory.tsx.tmpl +43 -0
  31. package/templates/base/src/Factory/RuleEffects/readOnlyEffect.ts.tmpl +27 -0
  32. package/templates/base/src/Factory/Validations/myValidation.ts.tmpl +52 -0
  33. package/templates/base/src/Factory/Widgets/MyWidget/index.tsx.tmpl +101 -0
  34. package/templates/base/src/Factory/Widgets/Notification/index.tsx.tmpl +53 -0
  35. package/templates/base/src/Factory/Widgets/Notification/notifiFunc.ts.tmpl +9 -0
  36. package/templates/base/src/Factory/Widgets/SectionNav/index.tsx +193 -0
  37. package/templates/base/src/Factory/Widgets/SectionNav/style.scss +139 -0
  38. package/templates/base/src/Module/Sample/Contacts.json.tmpl +965 -0
  39. package/templates/base/src/Module/index.ts.tmpl +26 -0
  40. package/templates/base/src/Screens/index.tsx.tmpl +45 -0
  41. package/templates/base/src/Utils/errorMessages.ts.tmpl +35 -0
  42. package/templates/base/src/Utils/fieldErrorMapper.ts.tmpl +46 -0
  43. package/templates/base/src/Utils/logger.ts.tmpl +21 -0
  44. package/templates/base/src/Utils/responseFormatter.ts.tmpl +39 -0
  45. package/templates/base/src/Utils/storages.ts.tmpl +35 -0
  46. package/templates/base/src/assets/styles/index.scss.tmpl +149 -0
  47. package/templates/base/src/constant.ts.tmpl +53 -0
  48. package/templates/base/src/icons.ts.tmpl +17 -0
  49. package/templates/base/src/localizationResolver.ts.tmpl +48 -0
  50. package/templates/base/src/main.tsx.tmpl +136 -0
  51. package/templates/base/src/routes.ts.tmpl +20 -0
  52. package/templates/base/src/vite-env.d.ts.tmpl +20 -0
  53. package/templates/base/tsconfig.json.tmpl +30 -0
  54. package/templates/base/tsconfig.node.json.tmpl +11 -0
  55. package/templates/base/vite.config.ts.tmpl +90 -0
  56. package/templates/multi-language/locales/fr/core.json +76 -0
  57. package/templates/multi-language/locales/fr/kendo.json +181 -0
  58. package/templates/multi-language/locales/fr/template.json +169 -0
  59. package/templates/multi-language/locales/fr/widget.json +65 -0
  60. package/templates/multi-language/locales/hi/core.json +76 -0
  61. package/templates/multi-language/locales/hi/kendo.json +181 -0
  62. package/templates/multi-language/locales/hi/template.json +169 -0
  63. package/templates/multi-language/locales/hi/widget.json +65 -0
  64. package/templates/multi-language/sample/fr.json +55 -0
  65. package/templates/multi-language/sample/hi.json +55 -0
  66. package/templates/tests/src/Module/schemas.test.ts +50 -0
  67. package/templates/tests-multi-language/src/multiLanguage/enText.ts +68 -0
  68. package/templates/tests-multi-language/src/multiLanguage/translations.test.ts +92 -0
package/src/cli.mjs ADDED
@@ -0,0 +1,514 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { parseArgs, helpText } from "./args.mjs";
7
+ import { ask, confirm, closePrompt, isInteractive } from "./prompt.mjs";
8
+ import { c, log, step, ok, warn, info } from "./log.mjs";
9
+ import { buildDependencies, buildDevDependencies, PINNED_SET, resolveDependencySet } from "./deps.mjs";
10
+ import {
11
+ BASE_DIR,
12
+ copyTree,
13
+ inspectTarget,
14
+ toPackageName,
15
+ toBasePath,
16
+ writeJson,
17
+ TEMPLATES_DIR,
18
+ } from "./scaffold.mjs";
19
+ import { applyMultiLanguage, applySampleTranslations, languagesProblem, parseLanguages } from "./multiLanguage.mjs";
20
+
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const PKG = JSON.parse(fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf8"));
23
+
24
+ const DEFAULTS = {
25
+ name: "procode-app",
26
+ appCode: "MYAPP",
27
+ apiUrl: "https://qa.example.com/lowcodev2/api/{APP_CODE}/",
28
+ authUrl: "https://localhost:7016/token",
29
+ toolUrl: "https://lowcodeapi-399313.uc.r.appspot.com/",
30
+ env: "InDraft",
31
+ languages: "en,fr",
32
+ };
33
+
34
+ function detectPackageManager() {
35
+ const ua = process.env.npm_config_user_agent ?? "";
36
+ for (const pm of ["pnpm", "yarn", "bun"]) if (ua.startsWith(pm)) return pm;
37
+ return "npm";
38
+ }
39
+
40
+ function runCommand(cmd, args, cwd) {
41
+ const res = spawnSync(cmd, args, {
42
+ cwd,
43
+ stdio: "inherit",
44
+ // On Windows npm/pnpm/yarn/git are .cmd shims, which spawn cannot exec directly.
45
+ shell: process.platform === "win32",
46
+ });
47
+ if (res.error) throw res.error;
48
+ return res.status === 0;
49
+ }
50
+
51
+ const VALID_APP_CODE = /^[A-Za-z][A-Za-z0-9_]*$/;
52
+
53
+ async function collectOptions(opts, positionals) {
54
+ const nonInteractive = opts.yes || !isInteractive();
55
+ const given = (key) => Object.prototype.hasOwnProperty.call(opts, key);
56
+
57
+ // Every question below is skippable: press Enter to take the shown default,
58
+ // or pass the matching flag and it is never asked at all.
59
+
60
+ // ---- project name -------------------------------------------------------
61
+ let name = opts.name ?? positionals[0];
62
+ if (!name) {
63
+ if (nonInteractive) {
64
+ name = DEFAULTS.name;
65
+ } else {
66
+ name = await ask("Project name?", DEFAULTS.name, (v) =>
67
+ v.includes("/") || v.includes("\\") ? "Use a plain directory name, not a path." : null,
68
+ );
69
+ }
70
+ }
71
+ const packageName = toPackageName(name);
72
+
73
+ // ---- application code ---------------------------------------------------
74
+ // The core resolves the adapter global by NAME as "getAdapter" +
75
+ // VITE_APPLICATION_CODE. If the two diverge every adapter silently falls back
76
+ // to a dummy and no screen ever has data - so this is validated, not free text.
77
+ const codeFromName = packageName.replace(/[^a-zA-Z0-9]/g, "").toUpperCase() || DEFAULTS.appCode;
78
+ let appCode = opts["app-code"];
79
+ if (!appCode) {
80
+ appCode = nonInteractive
81
+ ? codeFromName
82
+ : await ask("Application code (VITE_APPLICATION_CODE)?", codeFromName, (v) =>
83
+ VALID_APP_CODE.test(v)
84
+ ? null
85
+ : "Must start with a letter and hold only letters, digits and underscores - it becomes part of a JS identifier.",
86
+ );
87
+ }
88
+ if (!VALID_APP_CODE.test(appCode)) {
89
+ throw new Error(
90
+ "--app-code \"" +
91
+ appCode +
92
+ "\" is not usable: it becomes the global name getAdapter" +
93
+ appCode +
94
+ ", so it must start with a letter and hold only letters, digits and underscores.",
95
+ );
96
+ }
97
+
98
+ // ---- base path ----------------------------------------------------------
99
+ let basePath = opts.base;
100
+ if (!basePath) {
101
+ basePath = nonInteractive
102
+ ? "/" + packageName
103
+ : await ask("Base path (Vite base + VITE_HOMEPAGE)?", "/" + packageName);
104
+ }
105
+ basePath = toBasePath(basePath);
106
+
107
+ // ---- service URLs -------------------------------------------------------
108
+ const apiUrl = (opts["api-url"] ?? DEFAULTS.apiUrl).replace(/\{APP_CODE\}/g, appCode);
109
+ const authUrl = opts["auth-url"] ?? DEFAULTS.authUrl;
110
+ const toolUrl = opts["tool-url"] ?? DEFAULTS.toolUrl;
111
+ const projectEnv = opts.env ?? DEFAULTS.env;
112
+
113
+ // ---- booleans -----------------------------------------------------------
114
+ const pm = opts.pm ?? detectPackageManager();
115
+ const withSample = given("sample") ? opts.sample : true;
116
+ const withLint = given("lint") ? opts.lint : true;
117
+ const withTests = given("tests") ? opts.tests : true;
118
+ const latest = given("latest") ? opts.latest : true;
119
+
120
+ // ---- multi-language -----------------------------------------------------
121
+ // Off unless asked for: a root screen, a language switch and translation
122
+ // files are only worth carrying when the app is going to be translated.
123
+ // `--languages` alone implies it.
124
+ let multiLanguage = given("multi-language") ? opts["multi-language"] : given("languages") ? true : null;
125
+ if (multiLanguage === null) {
126
+ multiLanguage = nonInteractive ? false : await confirm("Enable multi-language (a language switch and translation files)?", false);
127
+ }
128
+ let languages = ["en"];
129
+ if (multiLanguage) {
130
+ let raw = opts.languages;
131
+ if (!raw) {
132
+ raw = nonInteractive
133
+ ? DEFAULTS.languages
134
+ : await ask("Languages (comma-separated codes, English is always included)?", DEFAULTS.languages, languagesProblem);
135
+ }
136
+ languages = parseLanguages(raw);
137
+ }
138
+
139
+ let install = given("install") ? opts.install : null;
140
+ if (install === null) {
141
+ install = nonInteractive ? true : await confirm("Run " + pm + " install now?", true);
142
+ }
143
+
144
+ let git = given("git") ? opts.git : null;
145
+ if (git === null) git = nonInteractive ? true : await confirm("Initialise a git repository?", true);
146
+
147
+ return {
148
+ name,
149
+ packageName,
150
+ appCode,
151
+ basePath,
152
+ apiUrl,
153
+ authUrl,
154
+ toolUrl,
155
+ projectEnv,
156
+ pm,
157
+ install,
158
+ git,
159
+ withSample,
160
+ withLint,
161
+ withTests,
162
+ latest,
163
+ multiLanguage,
164
+ languages,
165
+ force: Boolean(opts.force),
166
+ };
167
+ }
168
+
169
+ function buildTokens(o, set) {
170
+ return {
171
+ APP_NAME: o.name,
172
+ PKG_NAME: o.packageName,
173
+ APP_CODE: o.appCode,
174
+ BASE_PATH: o.basePath, // "/myapp"
175
+ BASE_PATH_SLASH: o.basePath.endsWith("/") ? o.basePath : o.basePath + "/", // "/myapp/"
176
+ API_BASE_URL: o.apiUrl,
177
+ AUTH_URL: o.authUrl,
178
+ TOOL_BASE_URL: o.toolUrl,
179
+ PROJECT_ENV: o.projectEnv,
180
+ GENERATOR_VERSION: PKG.version,
181
+ // README's Scripts table gains the test row only when tests are generated.
182
+ TEST_SCRIPT_ROW: o.withTests
183
+ ? "\n| `npm test` | vitest: every screen's slug is registered and its screenId unique" +
184
+ (o.multiLanguage ? "; every translation key exists in every language" : "") +
185
+ " (`src/**/*.test.ts`) |"
186
+ : "",
187
+ // Rendered from the same set package.json is written from, so the
188
+ // generated README cannot drift from the versions it pins.
189
+ REACT_RANGE: set.peers.react ?? "",
190
+ PROCODE_VERSIONS: Object.entries(set.procode)
191
+ .map(([name, range]) => "| `" + name + "` | " + range + " |")
192
+ .join("\n"),
193
+ };
194
+ }
195
+
196
+ /** Edit a generated file line by line, keeping its line endings. */
197
+ function stripLines(file, drop, map = (line) => line) {
198
+ const raw = fs.readFileSync(file, "utf8");
199
+ const eol = raw.includes("\r\n") ? "\r\n" : "\n";
200
+ const out = raw.split(/\r?\n/).filter((line) => !drop(line)).map(map);
201
+ fs.writeFileSync(file, out.join(eol), "utf8");
202
+ }
203
+
204
+ const TESTS_START = /\/\/ @tests-start\s*$/;
205
+ const TESTS_END = /\/\/ @tests-end\s*$/;
206
+ const TESTS_LINE = /\s*\/\/ @tests\s*$/;
207
+
208
+ /** --no-tests: take vitest's block and its type reference out of vite.config.ts. */
209
+ function stripTestBlock(file) {
210
+ let inBlock = false;
211
+ stripLines(file, (line) => {
212
+ if (TESTS_START.test(line)) return (inBlock = true);
213
+ if (TESTS_END.test(line)) {
214
+ inBlock = false;
215
+ return true;
216
+ }
217
+ return inBlock || TESTS_LINE.test(line);
218
+ });
219
+ }
220
+
221
+ /** With tests: keep the block, drop only the markers. */
222
+ function keepTestBlock(file) {
223
+ stripLines(
224
+ file,
225
+ (line) => TESTS_START.test(line) || TESTS_END.test(line),
226
+ (line) => line.replace(TESTS_LINE, ""),
227
+ );
228
+ }
229
+
230
+ /** Remove the sample screen and unwire it, when --no-sample was passed. */
231
+ function stripSample(root) {
232
+ fs.rmSync(path.join(root, "src", "Module", "Sample"), { recursive: true, force: true });
233
+ fs.rmSync(path.join(root, "src", "AdapterProvider", "SampleContacts.ts"), { force: true });
234
+ fs.rmSync(path.join(root, "src", "Factory", "Actions", "sampleContacts.ts"), { force: true });
235
+
236
+ // The registry keeps the two generic actions and the custom-widget pair;
237
+ // only the sample-screen stand-ins go.
238
+ fs.writeFileSync(
239
+ path.join(root, "src", "CustomAction.ts"),
240
+ [
241
+ 'import { setField } from "./Factory/Actions/setField";',
242
+ 'import { myAction, suggestReferenceCode } from "./Factory/Actions/myAction";',
243
+ "",
244
+ "/**",
245
+ " * Extension point 4 of 5: custom actions, resolved by a CUS_-prefixed key.",
246
+ " *",
247
+ " * Resolution is custom -> template -> standard. A key no tier knows is logged",
248
+ ' * and toasted as "This action is not available". A Map, not an object literal:',
249
+ ' * on a plain object, actionMap["constructor"] returns a function.',
250
+ " *",
251
+ " * Write a custom action only for logic none of the standard keys expresses,",
252
+ " * and NEVER to make an HTTP call.",
253
+ " */",
254
+ "class CustomActionFactory {",
255
+ " private actionMap = new Map<string, (props: any, ...args: any[]) => any>([",
256
+ ' ["CUS_SET_FIELD", setField],',
257
+ ' ["CUS_MY_ACTION", myAction],',
258
+ ' ["CUS_SUGGEST_CODE", suggestReferenceCode],',
259
+ " ]);",
260
+ "",
261
+ " getAction(key: string) {",
262
+ " return this.actionMap.get(key) ?? null; // never throw",
263
+ " }",
264
+ "}",
265
+ "",
266
+ "export default CustomActionFactory;",
267
+ "",
268
+ ].join("\n"),
269
+ "utf8",
270
+ );
271
+
272
+ fs.writeFileSync(
273
+ path.join(root, "src", "Module", "index.ts"),
274
+ [
275
+ "/**",
276
+ " * slug -> schema. The slug is the URL segment after the Vite base; the schema",
277
+ " * is the JSON the core renders. A slug with no branch here is page-not-found.",
278
+ " */",
279
+ "export const getSchemaBySlugService = (_slug: string, _appCode: string): any => {",
280
+ " return null;",
281
+ "};",
282
+ "",
283
+ ].join("\n"),
284
+ "utf8",
285
+ );
286
+
287
+ fs.writeFileSync(
288
+ path.join(root, "src", "AdapterProvider", "AdapterFactory.ts"),
289
+ [
290
+ "/**",
291
+ " * screenId -> adapter. Keyed by the `screenId` INSIDE the schema JSON, never by",
292
+ " * the slug. Read the id out of the file; never guess it.",
293
+ " *",
294
+ " * Registered onto window as `getAdapter<APPLICATION_CODE>` by main.tsx.",
295
+ " */",
296
+ "export const getAdapter = (_screenId: string): any => {",
297
+ " return null;",
298
+ "};",
299
+ "",
300
+ ].join("\n"),
301
+ "utf8",
302
+ );
303
+ }
304
+
305
+ export async function run(argv) {
306
+ const { opts, positionals } = parseArgs(argv);
307
+
308
+ if (opts.help) {
309
+ log(helpText(PKG.version));
310
+ return;
311
+ }
312
+ if (opts.version) {
313
+ log(PKG.version);
314
+ return;
315
+ }
316
+
317
+ log("");
318
+ log(" " + c.bold(c.magenta("ProCode")) + " " + c.dim("project generator") + " " + c.dim("v" + PKG.version));
319
+ log(" " + c.dim("React + TypeScript + Vite, wired to the four ProCode libraries"));
320
+ log("");
321
+
322
+ let o;
323
+ try {
324
+ o = await collectOptions(opts, positionals);
325
+ } finally {
326
+ closePrompt();
327
+ }
328
+
329
+ const targetRoot = path.resolve(process.cwd(), o.name);
330
+
331
+ // ---- target directory ---------------------------------------------------
332
+ const target = inspectTarget(targetRoot);
333
+ if (target.notADirectory) throw new Error(targetRoot + " exists and is not a directory.");
334
+ if (!target.empty && !o.force) {
335
+ const shown = target.conflicts.slice(0, 5).join(", ") + (target.conflicts.length > 5 ? ", ..." : "");
336
+ throw new Error(
337
+ targetRoot +
338
+ " is not empty (" +
339
+ shown +
340
+ ").\n Pick another name, empty the directory, or pass --force to scaffold into it anyway.",
341
+ );
342
+ }
343
+
344
+ log("");
345
+ step("Scaffolding into " + c.bold(targetRoot));
346
+ info("package " + o.packageName);
347
+ info("app code " + o.appCode + " " + c.dim("(global: getAdapter" + o.appCode + ")"));
348
+ info("base path " + o.basePath);
349
+ info("api " + o.apiUrl);
350
+ info("languages " + (o.multiLanguage ? o.languages.join(", ") : c.dim("English only (multi-language off)")));
351
+ log("");
352
+
353
+ // ---- library versions ---------------------------------------------------
354
+ // The newest published ProCode packages, with the peers and bundled packages
355
+ // those versions declare. Any failure (offline, a registry without the
356
+ // packages, a timeout) falls back to the set this generator was tested with.
357
+ let set = PINNED_SET;
358
+ if (o.latest) {
359
+ step("Resolving the latest ProCode library versions from the npm registry");
360
+ try {
361
+ set = await resolveDependencySet();
362
+ ok(
363
+ Object.entries(set.procode)
364
+ .map(([name, range]) => name.replace(/^procode-(lowcode-)?/, "") + " " + range.replace(/^\^/, ""))
365
+ .join(", "),
366
+ );
367
+ } catch (err) {
368
+ warn("Could not resolve the latest versions (" + (err?.message ?? err) + ").");
369
+ info("Using the versions this generator was tested with. Pass --no-latest to skip the lookup.");
370
+ }
371
+ } else {
372
+ info("using the tested library versions (--no-latest)");
373
+ }
374
+ log("");
375
+
376
+ // ---- files --------------------------------------------------------------
377
+ const tokens = buildTokens(o, set);
378
+ const created = copyTree(BASE_DIR, targetRoot, tokens);
379
+ ok(created.length + " project files written");
380
+
381
+ if (!o.withSample) {
382
+ stripSample(targetRoot);
383
+ info("sample module screen omitted (--no-sample)");
384
+ }
385
+
386
+ if (o.multiLanguage) {
387
+ const { translatedLibraries } = applyMultiLanguage(targetRoot, { languages: o.languages, withSample: o.withSample });
388
+ ok("multi-language: " + o.languages.join(", ") + " — root screen src/Module/Header.json, files in public/locales/");
389
+ const untranslated = o.languages.filter((l) => l !== "en" && !translatedLibraries.includes(l));
390
+ if (untranslated.length) {
391
+ info(
392
+ "the libraries' own text is included for " +
393
+ (translatedLibraries.join(", ") || "no language but English") +
394
+ "; for " + untranslated.join(", ") + " it shows in English until public/locales/<code>/{core,widget,kendo,template}.json are translated",
395
+ );
396
+ }
397
+ }
398
+
399
+ // ---- tests --------------------------------------------------------------
400
+ const viteConfig = path.join(targetRoot, "vite.config.ts");
401
+ if (o.withTests) {
402
+ copyTree(path.join(TEMPLATES_DIR, "tests"), targetRoot, tokens);
403
+ if (o.multiLanguage) copyTree(path.join(TEMPLATES_DIR, "tests-multi-language"), targetRoot, tokens);
404
+ // The markers only delimit the block for --no-tests; drop them.
405
+ keepTestBlock(viteConfig);
406
+ } else {
407
+ stripTestBlock(viteConfig);
408
+ }
409
+
410
+ if (!o.withLint) {
411
+ fs.rmSync(path.join(targetRoot, ".eslintrc.cjs"), { force: true });
412
+ // Its only recommendation is the ESLint extension.
413
+ fs.rmSync(path.join(targetRoot, ".vscode"), { recursive: true, force: true });
414
+ }
415
+
416
+ // ---- package.json -------------------------------------------------------
417
+ const pkgFile = path.join(targetRoot, "package.json");
418
+ const generated = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
419
+ generated.dependencies = buildDependencies(set);
420
+ generated.devDependencies = buildDevDependencies({ lint: o.withLint, tests: o.withTests, set });
421
+ if (!o.withLint) delete generated.scripts.lint;
422
+ if (o.withTests) generated.scripts.test = "vitest run";
423
+ generated._procodeBundled = {
424
+ note:
425
+ "Bundled INSIDE the four ProCode packages. Do not add them here and do not bump them independently - react-redux in particular must stay on 8.x, or the core's store becomes invisible and every screen renders blank.",
426
+ ...set.bundled,
427
+ };
428
+ writeJson(pkgFile, generated);
429
+ ok(
430
+ "package.json written (" + (set.source === "latest" ? "latest" : "tested") + " versions): " +
431
+ Object.keys(generated.dependencies).length +
432
+ " dependencies, " +
433
+ Object.keys(generated.devDependencies).length +
434
+ " dev",
435
+ );
436
+
437
+ // ---- git ----------------------------------------------------------------
438
+ if (o.git) {
439
+ if (fs.existsSync(path.join(targetRoot, ".git"))) {
440
+ info("git repository already present, skipping init");
441
+ } else if (runCommand("git", ["init", "-q"], targetRoot)) {
442
+ ok("git repository initialised");
443
+ } else {
444
+ warn("git init failed - carrying on without it");
445
+ }
446
+ }
447
+
448
+ // ---- install ------------------------------------------------------------
449
+ let installed = false;
450
+ if (o.install) {
451
+ log("");
452
+ step("Installing dependencies with " + o.pm + " - this takes a few minutes");
453
+ log("");
454
+ installed = runCommand(o.pm, ["install"], targetRoot);
455
+ log("");
456
+ if (installed) ok("dependencies installed");
457
+ else warn(o.pm + " install failed - run it yourself in " + o.name + " and read the error");
458
+ }
459
+
460
+ // ---- multi-language: move the sample screen's text into the files ------
461
+ // The same versioned tool an app uses later (it ships with
462
+ // procode-lowcode-core), so it needs the install.
463
+ let sampleExtracted = false;
464
+ if (o.multiLanguage && o.withSample) {
465
+ if (installed) {
466
+ step("Moving the sample screen's text into public/locales/en/sample.json");
467
+ sampleExtracted = runCommand("npx", ["procode-extract-text", "sample", "src/Module/Sample/Contacts.json"], targetRoot);
468
+ if (sampleExtracted) {
469
+ const filled = applySampleTranslations(targetRoot, o.languages);
470
+ const rest = o.languages.filter((l) => l !== "en" && !filled.includes(l));
471
+ ok(
472
+ "sample screen text extracted" +
473
+ (filled.length ? " — translated into " + filled.join(", ") : "") +
474
+ (rest.length ? "; " + rest.join(", ") + " start in English — translate public/locales/{" + rest.join(",") + "}/sample.json" : ""),
475
+ );
476
+ } else {
477
+ warn("procode-extract-text failed - the sample screen stays in English; run it yourself (below)");
478
+ }
479
+ }
480
+ }
481
+
482
+ // ---- what to do next ----------------------------------------------------
483
+ const runCmd = o.pm === "npm" ? "npm run" : o.pm;
484
+ log("");
485
+ log(" " + c.green(c.bold("Done.")) + " Next:");
486
+ log("");
487
+ log(" " + c.cyan("cd") + " " + o.name);
488
+ if (!installed) log(" " + c.cyan(o.pm) + " install");
489
+ log(" " + c.cyan(runCmd) + " dev");
490
+ log("");
491
+ if (o.withSample) {
492
+ log(" The sample screen is at " + c.bold("http://localhost:5173" + o.basePath + "/sample"));
493
+ log(" " + c.dim(o.basePath + " is the Vite base, /sample is the slug in src/Module/index.ts"));
494
+ log("");
495
+ }
496
+ if (o.multiLanguage) {
497
+ log(" Multi-language: " + c.bold(o.languages.join(", ")) + " — switch in the header; " + c.dim("?lang=<code> picks the start language"));
498
+ if (o.withSample && !sampleExtracted) {
499
+ log(" Move the sample screen's text into the translation files once installed:");
500
+ log(" " + c.cyan("npx procode-extract-text sample src/Module/Sample/Contacts.json"));
501
+ }
502
+ log("");
503
+ }
504
+ log(" " + c.yellow("Before it will load real data:"));
505
+ log(" 1. Point " + c.bold("VITE_CENTRAL_SERVICE_BASE_URL") + " in .env at your environment.");
506
+ log(
507
+ " 2. Put a dev token in " +
508
+ c.bold(".env.local") +
509
+ " as VITE_HARDCODED_TOKEN, or sign in for the AuthToken cookie.",
510
+ );
511
+ log(" " + c.dim(".env.local is gitignored. Never commit a token to .env."));
512
+ log(" 3. " + c.bold("README.md") + " holds the rules - schema first, no axios under src/ outside main.tsx.");
513
+ log("");
514
+ }