@multiplatform.one/cli 2.1.6 → 2.1.8

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.
@@ -1,10 +1,10 @@
1
- /**
1
+ /*
2
2
  * File: /src/bin/multiplatformOne.ts
3
3
  * Project: @multiplatform.one/cli
4
- * File Created: 01-01-1970 00:00:00
4
+ * File Created: 10-01-2025 21:02:36
5
5
  * Author: Clay Risser
6
6
  * -----
7
- * BitSpur (c) Copyright 2021 - 2024
7
+ * BitSpur (c) Copyright 2021 - 2025
8
8
  *
9
9
  * Licensed under the Apache License, Version 2.0 (the "License");
10
10
  * you may not use this file except in compliance with the License.
@@ -24,23 +24,34 @@ import fs from "node:fs/promises";
24
24
  import os from "node:os";
25
25
  import path from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
- import axios from "axios";
27
+ import {
28
+ formatServiceList,
29
+ projectRoot,
30
+ waitForApi,
31
+ waitForFrappe,
32
+ waitForKeycloak,
33
+ waitForPostgres,
34
+ waitServices,
35
+ } from "@multiplatform.one/utils/dev";
28
36
  import { program } from "commander";
29
37
  import dotenv from "dotenv";
30
38
  import { execa } from "execa";
31
39
  import inquirer from "inquirer";
32
40
  import ora from "ora";
33
- import pg from "pg";
34
41
  import type { CookieCutterConfig } from "../types";
35
42
 
36
- const availableBackends = ["api", "frappe"];
43
+ const availableServices = ["api", "frappe", "solana", "ethereum", "sui"];
44
+ const defaultDotenvPath = path.resolve(projectRoot, ".env");
37
45
  const availablePlatforms = [
38
46
  "electron",
39
47
  "expo",
40
48
  "keycloak",
41
- "next",
49
+ "one",
42
50
  "storybook",
43
51
  "storybook-expo",
52
+ "vocs",
53
+ "vscode",
54
+ "webext",
44
55
  ];
45
56
 
46
57
  process.env.COOKIECUTTER = `sh ${path.resolve(
@@ -72,13 +83,12 @@ program
72
83
  "branch, tag or commit to checkout",
73
84
  "main",
74
85
  )
75
- .option("-p, --platforms <platforms>", "platforms to keep")
76
- .option("-b, --backends <backends>", "backends to keep")
86
+ .option("-p, --platforms <platforms>", "platforms to use")
87
+ .option("-s, --services <services>", "services to use")
77
88
  .argument("[name]", "the name of the project", "")
78
89
  .description("init multiplatform.one")
79
90
  .action(async (name, options) => {
80
- let { backends, platforms } = options;
81
-
91
+ let { services, platforms } = options;
82
92
  if (
83
93
  (
84
94
  await execa("git", ["rev-parse", "--is-inside-work-tree"], {
@@ -101,19 +111,18 @@ program
101
111
  ])
102
112
  ).name;
103
113
  }
104
- if (!backends) {
105
- const backendsResult = (
114
+ if (!services) {
115
+ const servicesResult = (
106
116
  await inquirer.prompt([
107
117
  {
108
- message: "What backends are you using?",
109
- name: "backends",
118
+ message: "What services are you using?",
119
+ name: "services",
110
120
  type: "checkbox",
111
- choices: availableBackends.map((name) => ({ name })),
121
+ choices: availableServices.map((name) => ({ name })),
112
122
  },
113
123
  ])
114
- ).backends;
115
-
116
- backends = backendsResult.join(",");
124
+ ).services;
125
+ services = servicesResult.join(",");
117
126
  }
118
127
  if (!platforms) {
119
128
  const platformsResult = (
@@ -129,7 +138,7 @@ program
129
138
  platforms = platformsResult.join(",");
130
139
  }
131
140
  const cookieCutterConfig: CookieCutterConfig = {
132
- default_context: { name, platforms, backends },
141
+ default_context: { name, platforms, services },
133
142
  };
134
143
  const cookieCutterConfigFile = path.join(
135
144
  await fs.mkdtemp(path.join(os.tmpdir(), "multiplatform-")),
@@ -177,20 +186,20 @@ program
177
186
  "main",
178
187
  )
179
188
  .option("-p, --platforms <platforms>", "platforms to keep")
180
- .option("-b, --backends <backends>", "backends to keep")
189
+ .option("-s, --services <services>", "services to keep")
181
190
  .description("update multiplatform.one")
182
191
  .action(async (options) => {
183
- let { backends, platforms } = options;
184
- if (!backends) {
185
- const backendsResult = await inquirer.prompt([
192
+ let { services, platforms } = options;
193
+ if (!services) {
194
+ const servicesResult = await inquirer.prompt([
186
195
  {
187
- message: "What backends are you using?",
188
- name: "backends",
196
+ message: "What services are you using?",
197
+ name: "services",
189
198
  type: "checkbox",
190
- choices: availableBackends.map((name) => ({ name })),
199
+ choices: availableServices.map((name) => ({ name })),
191
200
  },
192
201
  ]);
193
- backends = backendsResult.backends.join(",");
202
+ services = servicesResult.services.join(",");
194
203
  }
195
204
  if (!platforms) {
196
205
  const platformsResult = await inquirer.prompt([
@@ -243,7 +252,9 @@ program
243
252
  await fs.readFile(path.resolve(projectRoot, "package.json"), "utf8"),
244
253
  )?.name;
245
254
  if (name) {
246
- cookieCutterConfig = { default_context: { name, backends, platforms } };
255
+ cookieCutterConfig = {
256
+ default_context: { name, services, platforms },
257
+ };
247
258
  }
248
259
  }
249
260
  if (!cookieCutterConfig) throw new Error("not a multiplatform.one project");
@@ -281,7 +292,6 @@ program
281
292
  }
282
293
  });
283
294
 
284
- const waitServices = ["api", "frappe", "postgres", "keycloak"];
285
295
  program
286
296
  .command("wait")
287
297
  .description("wait for a service to be ready")
@@ -293,136 +303,391 @@ program
293
303
  `the services to wait for (${waitServices.join(", ")})`,
294
304
  )
295
305
  .action(async (servicesString, options) => {
296
- dotenv.config({ path: options.dotenv });
306
+ dotenv.config({ path: options.dotenv || defaultDotenvPath });
297
307
  const interval = Number.parseInt(options.interval);
298
308
  const timeout = Number.parseInt(options.timeout);
299
309
  const services: string[] = servicesString.split(",");
300
- const unreadyServices = [...services];
301
- const spinner = ora(
302
- `waiting for ${formatServiceList(unreadyServices)}`,
303
- ).start();
304
- function updateSpinner(readyService: string) {
305
- unreadyServices.splice(unreadyServices.indexOf(readyService), 1);
306
- spinner.stop();
307
- spinner.succeed(`${readyService} is ready`);
308
- if (!unreadyServices.length) return;
309
- spinner.start(`waiting for ${formatServiceList(unreadyServices)}`);
310
- }
311
- let timeoutId: NodeJS.Timeout;
312
310
  try {
313
- await Promise.race([
314
- Promise.all(
315
- services.map(async (service) => {
316
- switch (service) {
317
- case "api": {
318
- await waitForApi(interval);
319
- updateSpinner("api");
320
- return;
321
- }
322
- case "frappe": {
323
- await waitForFrappe(interval);
324
- updateSpinner("frappe");
325
- return;
326
- }
327
- case "postgres": {
328
- await waitForPostgres(interval);
329
- updateSpinner("postgres");
330
- return;
331
- }
332
- case "keycloak": {
333
- await waitForKeycloak(interval);
334
- updateSpinner("keycloak");
335
- return;
336
- }
337
- }
338
- ora(
339
- `available services are ${formatServiceList(waitServices)}`,
340
- ).fail();
341
- }),
342
- ),
343
- new Promise((_, reject) => {
344
- timeoutId = setTimeout(() => reject(new Error("Timeout")), timeout);
345
- }),
346
- ]);
311
+ await waitWithSpinner(services, { interval, timeout });
347
312
  } catch (err) {
348
- if (err instanceof Error && err.message === "Timeout") {
349
- spinner.fail(
350
- `${formatServiceList(unreadyServices)} timed out after ${timeout}ms`,
351
- );
352
- } else {
353
- spinner.fail(err);
354
- }
355
313
  process.exit(1);
356
- } finally {
357
- clearTimeout(timeoutId);
358
314
  }
359
315
  });
360
316
 
361
- function formatServiceList(services: string[]): string {
362
- if (services.length === 1) return services[0];
363
- if (services.length === 2) return `${services[0]} and ${services[1]}`;
364
- return `${services.slice(0, -1).join(", ")} and ${services[services.length - 1]}`;
365
- }
317
+ program
318
+ .command("mesh")
319
+ .description("start the mesh server")
320
+ .option("-a, --api", "use api", false)
321
+ .option("-e, --dotenv <dotenv>", "dotenv file path", ".env")
322
+ .option("-f, --frappe", "use frappe", false)
323
+ .option("-i, --interval <interval>", "interval to wait for", "1000")
324
+ .option("-p, --port <port>", "port to run mesh on", "5002")
325
+ .option("-t, --timeout <timeout>", "timeout to wait for", "600000")
326
+ .action(async (options) => {
327
+ dotenv.config({ path: options.dotenv || defaultDotenvPath });
328
+ process.env.UWS_HTTP_MAX_HEADERS_SIZE = "16384";
329
+ if (options.frappe || options.api) {
330
+ process.env.MESH_API = options.api ? "1" : "0";
331
+ process.env.MESH_FRAPPE = options.frappe ? "1" : "0";
332
+ }
333
+ if (
334
+ !(await fs
335
+ .stat(path.resolve(projectRoot, "app/main.ts"))
336
+ .catch(() => false))
337
+ ) {
338
+ process.env.MESH_APP = "0";
339
+ }
340
+ if (
341
+ !(await fs
342
+ .stat(path.resolve(projectRoot, "frappe/package.json"))
343
+ .catch(() => false))
344
+ ) {
345
+ process.env.MESH_FRAPPE = "0";
346
+ }
347
+ const services = [
348
+ ...(process.env.MESH_API === "1" ? ["api"] : []),
349
+ ...(process.env.MESH_FRAPPE === "1" ? ["frappe"] : []),
350
+ ];
351
+ if (services.length > 0) {
352
+ const interval = Number.parseInt(options.interval);
353
+ const timeout = Number.parseInt(options.timeout);
354
+ try {
355
+ await waitWithSpinner(services, { interval, timeout });
356
+ } catch (err) {
357
+ process.exit(1);
358
+ }
359
+ }
360
+ await execa(
361
+ "mesh",
362
+ [
363
+ "dev",
364
+ "--port",
365
+ Number(options.port || process.env.MESH_PORT || 5002).toString(),
366
+ ],
367
+ {
368
+ stdio: "inherit",
369
+ },
370
+ );
371
+ });
366
372
 
367
- program.parse(process.argv);
373
+ program
374
+ .command("build")
375
+ .argument(
376
+ "[args...]",
377
+ `build to run: ${[...availableServices, "packages"].join(", ")} (default: all)`,
378
+ )
379
+ .description("run build command")
380
+ .action(async (args: string[]) => {
381
+ args = args.flatMap((arg) => arg.split(","));
382
+ const packages = args.includes("packages");
383
+ const projects = args.filter((arg) => arg !== "packages");
384
+ if ((!projects.length && !packages) || packages) {
385
+ await execa(
386
+ "turbo",
387
+ [
388
+ "run",
389
+ "build",
390
+ "--filter",
391
+ "'!./platforms/*'",
392
+ "--filter",
393
+ "'!./api'",
394
+ "--filter",
395
+ "'!./solana'",
396
+ "--filter",
397
+ "'!./ethereum'",
398
+ "--filter",
399
+ "'!./sui'",
400
+ ],
401
+ {
402
+ stdio: "inherit",
403
+ shell: true,
404
+ cwd: projectRoot,
405
+ },
406
+ );
407
+ }
408
+ for (const platform of await fs.readdir("platforms")) {
409
+ if (
410
+ ((!projects.length && !packages) || projects.includes(platform)) &&
411
+ (await fs
412
+ .access(path.join(projectRoot, "platforms", platform, "package.json"))
413
+ .then(
414
+ () =>
415
+ JSON.parse(
416
+ fsSync.readFileSync(
417
+ path.join(projectRoot, "platforms", platform, "package.json"),
418
+ "utf8",
419
+ ),
420
+ ).scripts?.build,
421
+ )
422
+ .catch(() => false))
423
+ ) {
424
+ await execa("./mkpm", [`${platform}/build`], {
425
+ stdio: "inherit",
426
+ shell: true,
427
+ cwd: projectRoot,
428
+ });
429
+ }
430
+ }
431
+ for (const service of availableServices) {
432
+ if (
433
+ ((!projects.length && !packages) || projects.includes(service)) &&
434
+ (await fs
435
+ .access(path.join(projectRoot, service, "package.json"))
436
+ .then(
437
+ () =>
438
+ JSON.parse(
439
+ fsSync.readFileSync(
440
+ path.join(projectRoot, service, "package.json"),
441
+ "utf8",
442
+ ),
443
+ ).scripts?.build,
444
+ )
445
+ .catch(() => false))
446
+ ) {
447
+ await execa("./mkpm", [`${service}/build`], {
448
+ stdio: "inherit",
449
+ shell: true,
450
+ cwd: projectRoot,
451
+ });
452
+ }
453
+ }
454
+ });
368
455
 
369
- async function waitForApi(interval: number) {
370
- try {
371
- const res = await axios.get(
372
- `http://localhost:${process.env.API_PORT || "5001"}/healthz`,
373
- );
374
- if ((res?.status || 500) < 300) {
375
- return;
456
+ program
457
+ .command("test")
458
+ .argument(
459
+ "[args...]",
460
+ `test to run: ${[...availableServices, "packages"].join(", ")} (default: all)`,
461
+ )
462
+ .description("run test command")
463
+ .action(async (args: string[]) => {
464
+ args = args.flatMap((arg) => arg.split(","));
465
+ const packages = args.includes("packages");
466
+ const projects = args.filter((arg) => arg !== "packages");
467
+ if ((!projects.length && !packages) || packages) {
468
+ await execa(
469
+ "turbo",
470
+ [
471
+ "run",
472
+ "test",
473
+ "--filter",
474
+ "'!./platforms/*'",
475
+ "--filter",
476
+ "'!./api'",
477
+ "--filter",
478
+ "'!./solana'",
479
+ "--filter",
480
+ "'!./ethereum'",
481
+ "--filter",
482
+ "'!./sui'",
483
+ ],
484
+ {
485
+ stdio: "inherit",
486
+ shell: true,
487
+ cwd: projectRoot,
488
+ },
489
+ );
376
490
  }
377
- } catch (err) {}
378
- await new Promise((resolve) => setTimeout(resolve, interval));
379
- return waitForApi(interval);
380
- }
491
+ for (const platform of await fs.readdir("platforms")) {
492
+ if (
493
+ ((!projects.length && !packages) || projects.includes(platform)) &&
494
+ (await fs
495
+ .access(path.join(projectRoot, "platforms", platform, "package.json"))
496
+ .then(
497
+ () =>
498
+ JSON.parse(
499
+ fsSync.readFileSync(
500
+ path.join(projectRoot, "platforms", platform, "package.json"),
501
+ "utf8",
502
+ ),
503
+ ).scripts?.test,
504
+ )
505
+ .catch(() => false))
506
+ ) {
507
+ await execa("./mkpm", [`${platform}/test`], {
508
+ stdio: "inherit",
509
+ shell: true,
510
+ cwd: projectRoot,
511
+ });
512
+ }
513
+ }
514
+ for (const service of availableServices) {
515
+ if (
516
+ ((!projects.length && !packages) || projects.includes(service)) &&
517
+ (await fs
518
+ .access(path.join(projectRoot, service, "package.json"))
519
+ .then(
520
+ () =>
521
+ JSON.parse(
522
+ fsSync.readFileSync(
523
+ path.join(projectRoot, service, "package.json"),
524
+ "utf8",
525
+ ),
526
+ ).scripts?.test,
527
+ )
528
+ .catch(() => false))
529
+ ) {
530
+ await execa("./mkpm", [`${service}/test`], {
531
+ stdio: "inherit",
532
+ shell: true,
533
+ cwd: projectRoot,
534
+ });
535
+ }
536
+ }
537
+ });
381
538
 
382
- async function waitForFrappe(interval: number) {
383
- try {
384
- const res = await axios.get(
385
- `${process.env.FRAPPE_BASE_URL || "http://frappe.localhost"}/api/method/ping`,
386
- );
387
- if ((res?.status || 500) < 300) {
388
- return;
539
+ program
540
+ .command("check")
541
+ .argument("[args...]", "checks to run: spelling, types, lint (default: all)")
542
+ .description("run check command")
543
+ .action(async (args: string[]) => {
544
+ args = args.flatMap((arg) => arg.split(","));
545
+ const spelling = !args.length || args.includes("spelling");
546
+ const types = !args.length || args.includes("types");
547
+ const lint = !args.length || args.includes("lint");
548
+ let exitCode = 0;
549
+ if (spelling) {
550
+ try {
551
+ await execa(
552
+ "cspell",
553
+ [
554
+ "--unique",
555
+ "`(git ls-files && (git lfs ls-files | cut -d' ' -f3))`",
556
+ ],
557
+ {
558
+ stdio: "inherit",
559
+ shell: true,
560
+ cwd: projectRoot,
561
+ },
562
+ );
563
+ } catch (err) {
564
+ if (err instanceof Error && "exitCode" in err) {
565
+ exitCode = (err as { exitCode: number }).exitCode;
566
+ } else {
567
+ exitCode = 1;
568
+ }
569
+ }
389
570
  }
390
- } catch (err) {}
391
- await new Promise((resolve) => setTimeout(resolve, interval));
392
- return waitForFrappe(interval);
393
- }
571
+ if (types) {
572
+ try {
573
+ await execa("turbo", ["run", "typecheck"], {
574
+ stdio: "inherit",
575
+ shell: true,
576
+ cwd: projectRoot,
577
+ });
578
+ } catch (err) {
579
+ if (err instanceof Error && "exitCode" in err) {
580
+ exitCode = (err as { exitCode: number }).exitCode;
581
+ } else {
582
+ exitCode = 1;
583
+ }
584
+ }
585
+ }
586
+ if (lint) {
587
+ try {
588
+ await execa(
589
+ "biome",
590
+ [
591
+ "check",
592
+ "--fix",
593
+ "--unsafe",
594
+ "`(git ls-files && (git lfs ls-files | cut -d' ' -f3)) | sort | uniq -u | grep -E '(html)|(s?css)|(md)|(json)|(yaml)|([jt]sx?)$'`",
595
+ ],
596
+ {
597
+ stdio: "inherit",
598
+ shell: true,
599
+ cwd: projectRoot,
600
+ },
601
+ );
602
+ } catch (err) {
603
+ if (err instanceof Error && "exitCode" in err) {
604
+ exitCode = (err as { exitCode: number }).exitCode;
605
+ } else {
606
+ exitCode = 1;
607
+ }
608
+ }
609
+ }
610
+ process.exit(exitCode);
611
+ });
394
612
 
395
- async function waitForPostgres(interval: number) {
396
- const client = new pg.Client({
397
- database: process.env.POSTGRES_DATABASE || "postgres",
398
- host: process.env.POSTGRES_HOSTNAME || "localhost",
399
- password: process.env.POSTGRES_PASSWORD || "postgres",
400
- port: Number.parseInt(process.env.POSTGRES_PORT || "5432"),
401
- user: process.env.POSTGRES_USERNAME || "postgres",
613
+ program
614
+ .command("count")
615
+ .description("count lines of code")
616
+ .action(async () => {
617
+ await execa(
618
+ "cloc",
619
+ [
620
+ "`(git ls-files && (git lfs ls-files | cut -d' ' -f3)) | sort | uniq -u | grep -E '(html)|(s?css)|(md)|(json)|(yaml)|([jt]sx?)$'`",
621
+ ],
622
+ {
623
+ stdio: "inherit",
624
+ shell: true,
625
+ cwd: projectRoot,
626
+ },
627
+ );
402
628
  });
403
- try {
404
- await client.connect();
405
- while (true) {
406
- try {
407
- await client.query("SELECT 1");
408
- return;
409
- } catch (error) {}
410
- await new Promise((resolve) => setTimeout(resolve, interval));
629
+
630
+ program
631
+ .command("generate")
632
+ .description("run generate command")
633
+ .action(async () => {
634
+ await execa("turbo", ["run", "generate"], {
635
+ stdio: "inherit",
636
+ shell: true,
637
+ cwd: projectRoot,
638
+ });
639
+ });
640
+
641
+ program.parse(process.argv);
642
+
643
+ async function waitWithSpinner(
644
+ services: string[],
645
+ options: { interval?: number; timeout?: number } = {},
646
+ ) {
647
+ const { interval = 1000, timeout = 600000 } = options;
648
+ const waitFunctions = {
649
+ api: waitForApi,
650
+ frappe: waitForFrappe,
651
+ postgres: waitForPostgres,
652
+ keycloak: waitForKeycloak,
653
+ };
654
+ const unreadyServices = [...services];
655
+ const spinner = ora(
656
+ `waiting for ${formatServiceList(unreadyServices)}`,
657
+ ).start();
658
+ function updateSpinner(readyService: string) {
659
+ unreadyServices.splice(unreadyServices.indexOf(readyService), 1);
660
+ spinner.succeed(`${readyService} is ready`);
661
+ if (unreadyServices.length) {
662
+ spinner.start(`waiting for ${formatServiceList(unreadyServices)}`);
411
663
  }
412
- } finally {
413
- await client.end();
414
664
  }
415
- }
416
-
417
- async function waitForKeycloak(interval: number) {
665
+ let timeoutId: NodeJS.Timeout | undefined;
418
666
  try {
419
- const res = await axios.get(
420
- `${process.env.KEYCLOAK_BASE_URL || "http://localhost:8080"}/realms/master/.well-known/openid-configuration`,
421
- );
422
- if ((res?.status || 500) < 300) {
423
- return;
667
+ await Promise.race([
668
+ Promise.all(
669
+ services.map(async (service) => {
670
+ const waitFn = waitFunctions[service];
671
+ if (!waitFn) throw new Error(`Unknown service: ${service}`);
672
+ await waitFn(interval);
673
+ updateSpinner(service);
674
+ }),
675
+ ),
676
+ new Promise((_, reject) => {
677
+ timeoutId = setTimeout(() => reject(new Error("Timeout")), timeout);
678
+ }),
679
+ ]);
680
+ } catch (err) {
681
+ const error = err as Error;
682
+ if (error.message === "Timeout") {
683
+ spinner.fail(
684
+ `${formatServiceList(unreadyServices)} timed out after ${timeout}ms`,
685
+ );
686
+ } else {
687
+ spinner.fail(error.message);
424
688
  }
425
- } catch (err) {}
426
- await new Promise((resolve) => setTimeout(resolve, interval));
427
- return waitForKeycloak(interval);
689
+ throw error;
690
+ } finally {
691
+ clearTimeout(timeoutId);
692
+ }
428
693
  }
package/src/types.ts CHANGED
@@ -1,10 +1,10 @@
1
- /*
1
+ /**
2
2
  * File: /src/types.ts
3
3
  * Project: @multiplatform.one/cli
4
- * File Created: 23-06-2024 10:13:38
4
+ * File Created: 10-01-2025 21:02:36
5
5
  * Author: Clay Risser
6
6
  * -----
7
- * BitSpur (c) Copyright 2021 - 2024
7
+ * BitSpur (c) Copyright 2021 - 2025
8
8
  *
9
9
  * Licensed under the Apache License, Version 2.0 (the "License");
10
10
  * you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@
21
21
 
22
22
  export interface CookieCutterConfig {
23
23
  default_context: {
24
- backends: string;
24
+ services: string;
25
25
  name: string;
26
26
  platforms: string;
27
27
  };