@tsplc/compiler 0.1.0 → 0.2.1

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/cli.js CHANGED
@@ -71,11 +71,11 @@ var buildServer = async (config, root) => {
71
71
 
72
72
  // packages/compiler/project/cli.ts
73
73
  import { parseArgs } from "util";
74
- import { resolve as resolve3 } from "path";
74
+ import { resolve as resolve6 } from "path";
75
75
  import {
76
76
  buildTargetFor,
77
- controllerFor,
78
- loadTsplcConfig as loadTsplcConfig2,
77
+ controllerFor as controllerFor2,
78
+ loadTsplcConfig as loadTsplcConfig3,
79
79
  rawBuildConfig,
80
80
  simulationConfig
81
81
  } from "@tsplc/config";
@@ -84,17 +84,30 @@ import {
84
84
  configureDocker,
85
85
  configureRuntime,
86
86
  inspectController,
87
- installLibraries as installLibraries2,
87
+ installLibraries as installLibraries3,
88
88
  loadSources,
89
89
  resolveConfig,
90
90
  runProject,
91
91
  updateController
92
92
  } from "@tsplc/build";
93
- import { checkHostConfig as checkHostConfig2 } from "@tsplc/compiler";
94
- import { formatDiagnostic, hasErrors as hasErrors2 } from "@tsplc/compiler/ir";
95
- import { deviceTree as deviceTree2 } from "@tsplc/typegen";
96
- import { loadHardware as loadHardware2, resolveIo as resolveIo2 } from "@tsplc/typegen/hardware";
93
+ import { checkHostConfig as checkHostConfig3 } from "@tsplc/compiler";
94
+ import { formatDiagnostic, hasErrors as hasErrors4 } from "@tsplc/compiler/ir";
95
+ import { deviceTree as deviceTree3 } from "@tsplc/typegen";
96
+ import { loadHardware as loadHardware3, resolveIo as resolveIo3 } from "@tsplc/typegen/hardware";
97
+ import { generateProject as generateProject2 } from "@tsplc/typegen";
98
+
99
+ // packages/compiler/project/plant.ts
100
+ import { basename, relative, resolve as resolve4, sep } from "node:path";
101
+ import { createInterface } from "node:readline/promises";
102
+ import { pathToFileURL } from "node:url";
103
+ import {
104
+ AgentOperationError,
105
+ createAgentClient
106
+ } from "@tsplc/agent/client";
107
+ import { controllerFor } from "@tsplc/config";
108
+ import { hasErrors as hasErrors2 } from "@tsplc/compiler/ir";
97
109
  import { generateProject } from "@tsplc/typegen";
110
+ import { z } from "zod";
98
111
 
99
112
  // packages/compiler/project/project.ts
100
113
  import { copyFile, mkdir as mkdir3, mkdtemp as mkdtemp2, realpath, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
@@ -402,6 +415,17 @@ async function checkProject(file = "tsplc.config.ts", options = {}) {
402
415
  ]
403
416
  };
404
417
  }
418
+ async function engineeringInputs(shared, configFile, bundle, frozen) {
419
+ const root = dirname3(configFile);
420
+ const tree = await deviceTree(shared, await loadHardware(shared, root));
421
+ return {
422
+ libraries: await installLibraries(shared.dependencies, root, { frozen }),
423
+ deviceTree: tree && {
424
+ ...tree,
425
+ mappings: bundle.ioMappings.map(({ address, variable }) => ({ address, variable }))
426
+ }
427
+ };
428
+ }
405
429
  async function buildProject(file = "tsplc.config.ts", options = {}) {
406
430
  let staging;
407
431
  try {
@@ -421,11 +445,14 @@ async function buildProject(file = "tsplc.config.ts", options = {}) {
421
445
  if (hasErrors(bundle.diagnostics)) {
422
446
  return { diagnostics: bundle.diagnostics };
423
447
  }
424
- const controllerConfig = checked.shared?.build ? codesysBuildConfig(checked.plan, bundle, {
448
+ const shared = checked.shared;
449
+ const engineering = shared && (!options.emitOnly || options.forController) ? await engineeringInputs(shared, configFile, bundle, options.frozenLockfile) : undefined;
450
+ const controllerConfig = shared?.build ? codesysBuildConfig(checked.plan, bundle, {
425
451
  configFile,
426
452
  sourceDir: join3(previous.output, "st"),
427
- outDir: resolve2(dirname3(configFile), checked.shared.build.outDir),
428
- controller: options.controller
453
+ outDir: resolve2(dirname3(configFile), shared.build.outDir),
454
+ controller: options.controller,
455
+ ...engineering
429
456
  }) : undefined;
430
457
  await mkdir3(previous.root, { recursive: true });
431
458
  staging = await mkdtemp2(join3(previous.root, "stage-"));
@@ -447,23 +474,15 @@ async function buildProject(file = "tsplc.config.ts", options = {}) {
447
474
  }
448
475
  let vendor;
449
476
  if (!options.emitOnly) {
450
- const engineering = checked.shared?.build ? resolve2(dirname3(configFile), checked.shared.build.outDir ?? "build") : join3(previous.root, "engineering");
451
- const libraries = checked.shared ? await installLibraries(checked.shared.dependencies, dirname3(configFile), {
452
- frozen: options.frozenLockfile
453
- }) : undefined;
454
- await engineeringDirectory(engineering, configFile);
455
- const tree = checked.shared && await deviceTree(checked.shared, await loadHardware(checked.shared, dirname3(configFile)));
477
+ const outDir = shared?.build ? resolve2(dirname3(configFile), shared.build.outDir ?? "build") : join3(previous.root, "engineering");
478
+ await engineeringDirectory(outDir, configFile);
456
479
  try {
457
480
  vendor = await buildCodesys(checked.plan, bundle, {
458
481
  configFile,
459
482
  sourceDir: join3(staging, "st"),
460
- outDir: engineering,
461
- libraries,
483
+ outDir,
462
484
  controller: options.controller,
463
- deviceTree: tree && {
464
- ...tree,
465
- mappings: bundle.ioMappings.map(({ address, variable }) => ({ address, variable }))
466
- }
485
+ ...engineering
467
486
  });
468
487
  } catch (cause) {
469
488
  return { diagnostics: diagnoseCodesys(cause, bundle) };
@@ -502,12 +521,627 @@ async function buildProject(file = "tsplc.config.ts", options = {}) {
502
521
  }
503
522
  }
504
523
  }
524
+
525
+ // packages/compiler/project/server.ts
526
+ import { existsSync as existsSync2 } from "node:fs";
527
+ import { mkdtemp as mkdtemp3, mkdir as mkdir4, rm as rm4, rename as rename3 } from "node:fs/promises";
528
+ import { dirname as dirname4, resolve as resolve3, join as join4 } from "node:path";
529
+ import { createRequire as createRequire2 } from "node:module";
530
+ import ts2 from "typescript";
531
+ import { typeCheckOnly as typeCheckOnly2 } from "@tsplc/compiler";
532
+ var resolveServerModule2 = (root, name) => name.startsWith(".") || name.startsWith("/") || existsSync2(resolve3(root, name)) ? resolve3(root, name) : createRequire2(join4(root, "package.json")).resolve(name);
533
+ var kebabCase2 = (name) => name.replace(/([a-z0-9])([A-Z])|([A-Z])([A-Z][a-z])/g, "$1$3-$2$4").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
534
+ var buildServer2 = async (config, root) => {
535
+ const settings = config.server;
536
+ if (!settings?.runtime)
537
+ throw new Error("Configure server.runtime to build an executable.");
538
+ const runtime = settings.runtime;
539
+ const entry = resolveServerModule2(root, runtime.entry);
540
+ const windows = runtime.target ? runtime.target.includes("windows") : process.platform === "win32";
541
+ const outFile = resolve3(root, "build/server", `${kebabCase2(config.name)}-services${windows ? ".exe" : ""}`);
542
+ const tsconfig = ts2.findConfigFile(dirname4(entry), ts2.sys.fileExists);
543
+ if (!tsconfig)
544
+ throw new Error("Server entry requires a tsconfig.json.");
545
+ const read = ts2.readConfigFile(tsconfig, ts2.sys.readFile);
546
+ if (read.error)
547
+ throw new Error(ts2.flattenDiagnosticMessageText(read.error.messageText, `
548
+ `));
549
+ const parsed = ts2.parseJsonConfigFileContent(read.config, ts2.sys, dirname4(tsconfig));
550
+ const program = ts2.createProgram({
551
+ rootNames: [entry, ...parsed.fileNames.filter((file) => /\.d\.[cm]?ts$/.test(file))],
552
+ options: { ...parsed.options, ...typeCheckOnly2 },
553
+ projectReferences: parsed.projectReferences
554
+ });
555
+ const diagnostics = [...parsed.errors, ...ts2.getPreEmitDiagnostics(program)];
556
+ if (diagnostics.length)
557
+ throw new Error(ts2.formatDiagnosticsWithColorAndContext(diagnostics, {
558
+ getCanonicalFileName: (path) => path,
559
+ getCurrentDirectory: () => root,
560
+ getNewLine: () => `
561
+ `
562
+ }));
563
+ await mkdir4(dirname4(outFile), { recursive: true });
564
+ const staging = await mkdtemp3(join4(dirname4(outFile), ".tsplc-server-"));
565
+ try {
566
+ const binary = join4(staging, "server");
567
+ const result = await Bun.build({
568
+ entrypoints: [entry],
569
+ env: "disable",
570
+ compile: {
571
+ outfile: binary,
572
+ ...runtime.target ? { target: runtime.target } : {},
573
+ autoloadDotenv: false,
574
+ autoloadBunfig: false,
575
+ autoloadTsconfig: false,
576
+ autoloadPackageJson: false
577
+ }
578
+ });
579
+ if (!result.success)
580
+ throw new AggregateError(result.logs, "Server executable build failed.");
581
+ await rename3(windows ? `${binary}.exe` : binary, outFile);
582
+ return {
583
+ server: {
584
+ executable: outFile,
585
+ target: runtime.target ?? `bun-${process.platform}-${process.arch}`
586
+ }
587
+ };
588
+ } finally {
589
+ await rm4(staging, { recursive: true, force: true });
590
+ }
591
+ };
592
+
593
+ // packages/compiler/project/plant.ts
594
+ var plantCommands = new Set([
595
+ "deploy",
596
+ "target",
597
+ "status",
598
+ "login",
599
+ "logout",
600
+ "vars",
601
+ "read",
602
+ "write",
603
+ "force",
604
+ "unforce",
605
+ "start",
606
+ "stop",
607
+ "reset"
608
+ ]);
609
+ var ask = async (question) => {
610
+ const lines = createInterface({ input: process.stdin, output: process.stdout });
611
+ try {
612
+ return (await lines.question(question)).trim();
613
+ } finally {
614
+ lines.close();
615
+ }
616
+ };
617
+ var askSecret = (question) => new Promise((accept, reject) => {
618
+ if (!process.stdin.isTTY) {
619
+ reject(new Error("The controller password needs an interactive terminal."));
620
+ return;
621
+ }
622
+ process.stdin.setRawMode(true);
623
+ process.stdin.setEncoding("utf8");
624
+ process.stdin.resume();
625
+ process.stdout.write(question);
626
+ let secret = "";
627
+ const finish = () => {
628
+ process.stdin.off("data", read);
629
+ process.stdin.setRawMode(false);
630
+ process.stdin.pause();
631
+ process.stdout.write(`
632
+ `);
633
+ };
634
+ const read = (text) => {
635
+ for (const character of text) {
636
+ if (character === "\r" || character === `
637
+ `) {
638
+ finish();
639
+ accept(secret);
640
+ return;
641
+ }
642
+ if (character === "\x03") {
643
+ finish();
644
+ reject(new Error("Cancelled."));
645
+ return;
646
+ }
647
+ secret = character === "" ? secret.slice(0, -1) : secret + character;
648
+ }
649
+ };
650
+ process.stdin.on("data", read);
651
+ });
652
+ var choose = async (question, items, label) => {
653
+ items.forEach((item, index) => console.log(` ${index + 1}. ${label(item)}`));
654
+ while (true) {
655
+ const item = items[Number(await ask(`${question} [1-${items.length}]: `)) - 1];
656
+ if (item !== undefined)
657
+ return item;
658
+ }
659
+ };
660
+ var confirm = async (question, flags) => flags.yes || /^y(es)?$/i.test(await ask(`${question} [y/N]: `));
661
+ var askCredentials = async () => ({
662
+ username: await ask("Controller username: "),
663
+ password: await askSecret("Controller password: ")
664
+ });
665
+ var progress = (message) => console.log(` ${message}`);
666
+ var connect = () => {
667
+ const environment = z.object({
668
+ TSPLC_AGENT_URL: z.url({
669
+ error: "Set TSPLC_AGENT_URL to the plant agent, such as http://line-pc:4850"
670
+ }),
671
+ TSPLC_AGENT_TOKEN: z.string({ error: "Set TSPLC_AGENT_TOKEN to the plant agent's token" })
672
+ }).parse(process.env);
673
+ return createAgentClient({
674
+ url: environment.TSPLC_AGENT_URL,
675
+ token: environment.TSPLC_AGENT_TOKEN
676
+ });
677
+ };
678
+ var selectTarget = async (agent) => {
679
+ const interfaces = await agent.interfaces();
680
+ if (!interfaces.length)
681
+ throw new Error("The plant PC has no IPv4 network.");
682
+ const network = interfaces.length === 1 && interfaces[0] ? interfaces[0] : await choose("Which network is the controller on?", interfaces, (item) => `${item.name} ${item.address}/${item.prefixLength}`);
683
+ console.log(`Scanning ${network.address}/${network.prefixLength} for CODESYS controllers…`);
684
+ const found = await agent.discover(network.name);
685
+ const manual = { address: "", port: 11740, hostname: null };
686
+ const choice = await choose("Which controller?", [...found, manual], (item) => item === manual ? "Enter an address" : `${item.address}${item.hostname ? ` (${item.hostname})` : ""}`);
687
+ const host = choice === manual ? await ask("Controller address: ") : choice.address;
688
+ return agent.setTarget({ host, port: choice.port });
689
+ };
690
+ var contractVariables = async (config, root) => {
691
+ if (!config.typegen)
692
+ return [];
693
+ const contract = z.object({
694
+ symbols: z.array(z.object({ name: z.string(), gvl: z.string(), type: z.string() })),
695
+ typeFields: z.record(z.string(), z.array(z.object({ field: z.string(), type: z.string() })))
696
+ }).parse(await import(pathToFileURL(resolve4(root, config.typegen.dir, "plc.ts")).href));
697
+ return contract.symbols.flatMap(({ name, gvl, type }) => {
698
+ const path = `${gvl}.${name}`;
699
+ const fields = contract.typeFields[type];
700
+ if (!fields)
701
+ return [{ path, type }];
702
+ return fields.map((field) => ({
703
+ path: field.field === "" ? path : `${path}${field.field.startsWith("[") ? "" : "."}${field.field}`,
704
+ type: field.type
705
+ }));
706
+ });
707
+ };
708
+ var prepareRelease = async (file, loaded, flags) => {
709
+ const { config, root } = loaded;
710
+ if (!config.compiler)
711
+ throw new Error("tsplc deploy deploys tsPLC programs; configure compiler.");
712
+ const emitted = await buildProject(file, {
713
+ emitOnly: true,
714
+ forController: true,
715
+ frozenLockfile: flags["frozen-lockfile"],
716
+ loaded,
717
+ buildTarget: "production",
718
+ controller: controllerFor(config, "deploy")
719
+ });
720
+ if (hasErrors2(emitted.diagnostics))
721
+ return { diagnostics: emitted.diagnostics };
722
+ const settings = emitted.controllerConfig;
723
+ if (!settings)
724
+ throw new Error("Configure build settings for deployment.");
725
+ await generateProject(file, { loaded, targets: await projectTargets(file, { loaded }) });
726
+ const portable = (path) => path.split(sep).join("/");
727
+ const libraries = (settings.libraries ?? []).map((library) => ({
728
+ ...library,
729
+ release: `libraries/${library.sha256}-${basename(library.path)}`
730
+ }));
731
+ const files = [
732
+ { path: "template.project", content: Bun.file(settings.template) },
733
+ ...libraries.map((library) => ({ path: library.release, content: Bun.file(library.path) }))
734
+ ];
735
+ for await (const source of new Bun.Glob("**/*").scan({ cwd: settings.sourceDir, dot: true }))
736
+ files.push({
737
+ path: `st/${portable(source)}`,
738
+ content: Bun.file(resolve4(settings.sourceDir, source))
739
+ });
740
+ let server;
741
+ if (config.server?.runtime) {
742
+ const { executable } = (await buildServer2(config, root)).server;
743
+ server = `server/${basename(executable)}`;
744
+ files.push({ path: server, content: Bun.file(executable) });
745
+ }
746
+ const manifest = {
747
+ config: {
748
+ io: {},
749
+ deviceTree: settings.deviceTree,
750
+ controller: settings.controller,
751
+ libraryReferences: settings.libraryReferences,
752
+ libraries: libraries.map(({ release, ...library }) => ({ ...library, path: release })),
753
+ task: settings.task,
754
+ tasks: settings.tasks,
755
+ removeObjects: settings.removeObjects,
756
+ symbols: settings.symbols,
757
+ timeouts: settings.timeouts,
758
+ template: "template.project",
759
+ sourceDir: "st",
760
+ entry: `st/${portable(relative(settings.sourceDir, settings.entry))}`
761
+ },
762
+ variables: await contractVariables(config, root),
763
+ server
764
+ };
765
+ return { release: { manifest, files } };
766
+ };
767
+ var deploy = async (agent, file, loaded, flags) => {
768
+ const status = await agent.status();
769
+ if (status.login)
770
+ throw new Error("A login is open; log out before deploying: tsplc logout");
771
+ const target = !status.target || flags.select ? await selectTarget(agent) : status.target;
772
+ console.log(`Building the release for ${target.host}…`);
773
+ const { release, diagnostics } = await prepareRelease(file, loaded, flags);
774
+ if (!release)
775
+ return { diagnostics };
776
+ if (flags.download && !await confirm("A full download stops the running application while it loads. Continue?", flags))
777
+ throw new Error("Cancelled.");
778
+ const credentials = await askCredentials();
779
+ const settings = {
780
+ credentials,
781
+ download: flags.download ?? false,
782
+ initializeUser: flags["initialize-user"] ?? false
783
+ };
784
+ console.log(release.manifest.server ? "Deploying the server and the program…" : "Deploying the program…");
785
+ try {
786
+ return await agent.deploy(release, settings, progress);
787
+ } catch (error) {
788
+ if (!(error instanceof AgentOperationError && error.downloadRequired) || !await confirm(`${error.message}. A full download stops the running application while it loads. Download now?`, flags))
789
+ throw error;
790
+ return agent.deploy(release, { ...settings, download: true }, progress);
791
+ }
792
+ };
793
+ var ensureLogin = async (agent) => {
794
+ if ((await agent.status()).login)
795
+ return;
796
+ const credentials = await askCredentials();
797
+ console.log("Logging in. CODESYS takes a few minutes to start; the login stays open until tsplc logout.");
798
+ await agent.login(credentials, progress);
799
+ };
800
+ var variableChildren = (variables, prefix) => {
801
+ const children = new Map;
802
+ const start = prefix.toLowerCase();
803
+ for (const { path, type } of variables) {
804
+ const lower = path.toLowerCase();
805
+ if (prefix && !(lower === start || lower.startsWith(`${start}.`) || lower.startsWith(`${start}[`)))
806
+ continue;
807
+ const rest = path.slice(prefix.length).replace(/^\./, "");
808
+ if (rest === "")
809
+ return [{ name: path, type, leaves: 1 }];
810
+ const name = /^(\[[^\]]*\]|[^.[]+)/.exec(rest)?.[0] ?? rest;
811
+ const child = children.get(name) ?? { leaves: 0 };
812
+ children.set(name, { type: rest === name ? type : undefined, leaves: child.leaves + 1 });
813
+ }
814
+ const indexes = [...children.keys()].filter((name) => /^\[\d+\]$/.test(name)).map((name) => Number(name.slice(1, -1)));
815
+ const listed = [...children].filter(([name]) => !/^\[\d+\]$/.test(name));
816
+ if (indexes.length) {
817
+ const first = children.get(`[${Math.min(...indexes)}]`);
818
+ listed.unshift([`[${Math.min(...indexes)}..${Math.max(...indexes)}]`, first ?? { leaves: 0 }]);
819
+ }
820
+ return listed.map(([name, child]) => ({ name, ...child }));
821
+ };
822
+ var printLogin = (result) => {
823
+ if ("values" in result) {
824
+ for (const [expression, value] of Object.entries(result.values))
825
+ console.log(`${expression} = ${value}`);
826
+ return;
827
+ }
828
+ console.log(`Application ${result.state}`);
829
+ if (result.forced.length)
830
+ console.log(`Forced: ${result.forced.join(", ")}`);
831
+ };
832
+ var runPlantCommand = async (command, args, flags, project) => {
833
+ const agent = connect();
834
+ const expect = (count, usage) => {
835
+ if (args.length !== count)
836
+ throw new Error(`Usage: tsplc ${command} ${usage}`);
837
+ };
838
+ const send = async (loginCommand) => {
839
+ await ensureLogin(agent);
840
+ printLogin(await agent.command(loginCommand));
841
+ };
842
+ switch (command) {
843
+ case "deploy": {
844
+ expect(0, "[--download] [--select] [--initialize-user]");
845
+ const deployed = await deploy(agent, project.file, await project.load(), flags);
846
+ if ("diagnostics" in deployed)
847
+ return deployed;
848
+ console.log(`Deployed release ${deployed.id} (${deployed.operation}).`);
849
+ return;
850
+ }
851
+ case "target": {
852
+ expect(0, "");
853
+ const target = await selectTarget(agent);
854
+ console.log(`Deployments now go to ${target.host}:${target.port}.`);
855
+ return;
856
+ }
857
+ case "status": {
858
+ expect(0, "");
859
+ const status = await agent.status();
860
+ console.log(`Controller ${status.target ? `${status.target.host}:${status.target.port}` : "not selected (tsplc target)"}`);
861
+ console.log(`Release ${status.release ? `${status.release.id}, ${status.release.operation} at ${status.release.deployedAt}` : "none"}`);
862
+ console.log(`Server ${status.server ? `${status.server.running ? "running" : "restarting"} (${status.server.executable})` : "none"}`);
863
+ console.log(`Login ${status.login ? `open; application ${status.login.state}${status.login.forced.length ? `; forced: ${status.login.forced.join(", ")}` : ""}` : "closed"}`);
864
+ return;
865
+ }
866
+ case "login":
867
+ expect(0, "");
868
+ await ensureLogin(agent);
869
+ printLogin(await agent.command({ command: "state" }));
870
+ return;
871
+ case "logout": {
872
+ expect(0, "");
873
+ const forced = (await agent.status()).login?.forced ?? [];
874
+ if (forced.length && !await confirm(`Forced: ${forced.join(", ")}. Forces stay on the controller after logout. Log out anyway?`, flags))
875
+ throw new Error("Cancelled.");
876
+ await agent.logout();
877
+ console.log("Logged out.");
878
+ return;
879
+ }
880
+ case "vars": {
881
+ if (args.length > 1)
882
+ throw new Error("Usage: tsplc vars [path]");
883
+ const variables = await agent.variables();
884
+ const children = variableChildren(variables, args[0] ?? "");
885
+ if (!children.length)
886
+ throw new Error(`No published variable ${args[0]}.`);
887
+ for (const child of children)
888
+ console.log(`${child.name.padEnd(32)} ${child.type ?? `${child.leaves} variables`}`);
889
+ return;
890
+ }
891
+ case "read": {
892
+ const [first, ...rest] = args;
893
+ if (!first)
894
+ throw new Error("Usage: tsplc read <expression>...");
895
+ return send({ command: "read", expressions: [first, ...rest] });
896
+ }
897
+ case "write":
898
+ case "force": {
899
+ const [expression, value, ...extra] = args;
900
+ if (!expression || !value || extra.length)
901
+ throw new Error(`Usage: tsplc ${command} <expression> <value>`);
902
+ return send({ command, values: { [expression]: value } });
903
+ }
904
+ case "unforce":
905
+ return send({ command: "unforce", expressions: args, restore: flags.restore ?? false });
906
+ case "start":
907
+ expect(0, "");
908
+ return send({ command: "start" });
909
+ case "stop":
910
+ expect(0, "");
911
+ if (!await confirm("Stop the application on the controller?", flags))
912
+ throw new Error("Cancelled.");
913
+ return send({ command: "stop" });
914
+ case "reset": {
915
+ expect(1, "warm|cold");
916
+ const kind = z.enum(["warm", "cold"]).parse(args[0]);
917
+ if (!await confirm(`Reset the application (${kind})? It stops and reinitializes its variables.`, flags))
918
+ throw new Error("Cancelled.");
919
+ return send({ command: "reset", kind: kind === "warm" ? "Warm" : "Cold" });
920
+ }
921
+ }
922
+ };
923
+
924
+ // packages/compiler/project/project.ts
925
+ import { copyFile as copyFile2, mkdir as mkdir5, mkdtemp as mkdtemp4, realpath as realpath2, rm as rm5, writeFile as writeFile3 } from "node:fs/promises";
926
+ import { dirname as dirname5, join as join5, resolve as resolve5 } from "node:path";
927
+ import {
928
+ buildCodesys as buildCodesys2,
929
+ codesysBuildConfig as codesysBuildConfig2,
930
+ diagnoseCodesys as diagnoseCodesys2,
931
+ emitCodesys as emitCodesys2,
932
+ ioTargets as ioTargets2,
933
+ publicationSelectors as publicationSelectors2,
934
+ planCodesys as planCodesys2
935
+ } from "@tsplc/compiler/codesys";
936
+ import { analyze as analyze2, checkHostConfig as checkHostConfig2 } from "@tsplc/compiler";
937
+ import {
938
+ compilerConfig as compilerConfig2,
939
+ loadTsplcConfig as loadTsplcConfig2
940
+ } from "@tsplc/config";
941
+ import { deviceTree as deviceTree2, loadHardware as loadHardware2, resolveIo as resolveIo2 } from "@tsplc/typegen";
942
+ import { installLibraries as installLibraries2 } from "@tsplc/build";
943
+ import { hasErrors as hasErrors3, normalizeModule as normalizeModule2 } from "@tsplc/compiler/ir";
944
+ var failure2 = (origin, code, cause) => ({
945
+ origin,
946
+ code,
947
+ severity: "error",
948
+ message: cause instanceof Error ? cause.message : String(cause)
949
+ });
950
+ async function loadProject2(file, loaded, buildTarget) {
951
+ let configFile = resolve5(file);
952
+ try {
953
+ configFile = await realpath2(configFile);
954
+ const diagnostics = checkHostConfig2(configFile);
955
+ if (hasErrors3(diagnostics))
956
+ return { configFile, diagnostics };
957
+ const shared = (loaded ?? await loadTsplcConfig2(configFile)).config;
958
+ const root = dirname5(configFile);
959
+ const config = compilerConfig2(shared, resolveIo2(shared, await loadHardware2(shared, root)), buildTarget);
960
+ return {
961
+ configFile,
962
+ shared,
963
+ config: { ...config, ...sourceInput2(config, root) },
964
+ diagnostics
965
+ };
966
+ } catch (cause) {
967
+ return { configFile, diagnostics: [failure2("config", "PLC5003", cause)] };
968
+ }
969
+ }
970
+ function sourceInput2(compiler, root) {
971
+ return {
972
+ tsconfig: resolve5(root, compiler.tsconfig),
973
+ programs: Object.fromEntries(Object.entries(compiler.programs).map(([name, entry]) => [
974
+ name,
975
+ { ...entry, module: resolve5(root, entry.module) }
976
+ ]))
977
+ };
978
+ }
979
+ async function projectTargets2(file = "tsplc.config.ts", options = {}) {
980
+ const configFile = await realpath2(resolve5(file));
981
+ const { config } = options.loaded ?? await loadTsplcConfig2(configFile);
982
+ if (!config.compiler)
983
+ return;
984
+ const source = analyze2({
985
+ ...sourceInput2(config.compiler, dirname5(configFile)),
986
+ services: config.server
987
+ });
988
+ const core = source.module && normalizeModule2(source.module).core;
989
+ return core ? { io: ioTargets2(core), publication: publicationSelectors2(core) } : undefined;
990
+ }
991
+ async function checkProject2(file = "tsplc.config.ts", options = {}) {
992
+ const loaded = await loadProject2(file, options.loaded, options.buildTarget);
993
+ if (!loaded.config) {
994
+ return loaded;
995
+ }
996
+ const config = loaded.config;
997
+ if (config.target.kind !== "codesys") {
998
+ return {
999
+ ...loaded,
1000
+ diagnostics: [failure2("backend", "PLC7001", `Unsupported target: ${config.target.kind}`)]
1001
+ };
1002
+ }
1003
+ const source = analyze2({
1004
+ tsconfig: config.tsconfig,
1005
+ programs: config.programs,
1006
+ services: loaded.shared?.server
1007
+ });
1008
+ if (!source.module) {
1009
+ return { ...loaded, diagnostics: source.diagnostics };
1010
+ }
1011
+ const normalized = normalizeModule2(source.module);
1012
+ if (!normalized.core) {
1013
+ return { ...loaded, diagnostics: normalized.diagnostics };
1014
+ }
1015
+ const tasks = config.tasks.map((task) => ({
1016
+ name: task.name,
1017
+ intervalNanoseconds: task.schedule.interval.nanoseconds,
1018
+ priority: task.priority,
1019
+ programs: task.programs
1020
+ }));
1021
+ const planned = planCodesys2(normalized.core, { tasks }, config.target.options);
1022
+ return {
1023
+ ...loaded,
1024
+ ...planned,
1025
+ diagnostics: [
1026
+ ...loaded.diagnostics,
1027
+ ...source.diagnostics,
1028
+ ...normalized.diagnostics,
1029
+ ...planned.diagnostics
1030
+ ]
1031
+ };
1032
+ }
1033
+ async function engineeringInputs2(shared, configFile, bundle, frozen) {
1034
+ const root = dirname5(configFile);
1035
+ const tree = await deviceTree2(shared, await loadHardware2(shared, root));
1036
+ return {
1037
+ libraries: await installLibraries2(shared.dependencies, root, { frozen }),
1038
+ deviceTree: tree && {
1039
+ ...tree,
1040
+ mappings: bundle.ioMappings.map(({ address, variable }) => ({ address, variable }))
1041
+ }
1042
+ };
1043
+ }
1044
+ async function buildProject2(file = "tsplc.config.ts", options = {}) {
1045
+ let staging;
1046
+ try {
1047
+ const configFile = await realpath2(resolve5(file));
1048
+ const previous = await owned(configFile, true);
1049
+ if (previous.unknown.length) {
1050
+ throw new Error(`Output contains unrelated files: ${previous.unknown.join(", ")}`);
1051
+ }
1052
+ const checked = await checkProject2(configFile, {
1053
+ loaded: options.loaded,
1054
+ buildTarget: options.buildTarget
1055
+ });
1056
+ if (!checked.plan || !checked.config) {
1057
+ return { diagnostics: checked.diagnostics };
1058
+ }
1059
+ const bundle = emitCodesys2(checked.plan);
1060
+ if (hasErrors3(bundle.diagnostics)) {
1061
+ return { diagnostics: bundle.diagnostics };
1062
+ }
1063
+ const shared = checked.shared;
1064
+ const engineering = shared && (!options.emitOnly || options.forController) ? await engineeringInputs2(shared, configFile, bundle, options.frozenLockfile) : undefined;
1065
+ const controllerConfig = shared?.build ? codesysBuildConfig2(checked.plan, bundle, {
1066
+ configFile,
1067
+ sourceDir: join5(previous.output, "st"),
1068
+ outDir: resolve5(dirname5(configFile), shared.build.outDir),
1069
+ controller: options.controller,
1070
+ ...engineering
1071
+ }) : undefined;
1072
+ await mkdir5(previous.root, { recursive: true });
1073
+ staging = await mkdtemp4(join5(previous.root, "stage-"));
1074
+ const artifacts = [
1075
+ ...bundle.artifacts,
1076
+ { path: "mappings.json", content: `${JSON.stringify(bundle.mappings, null, 2)}
1077
+ ` },
1078
+ { path: "io-symbols.json", content: `${JSON.stringify(bundle.ioSymbols, null, 2)}
1079
+ ` },
1080
+ { path: "publication.json", content: `${JSON.stringify(bundle.publication, null, 2)}
1081
+ ` },
1082
+ { path: "config.json", content: `${JSON.stringify(checked.config, null, 2)}
1083
+ ` }
1084
+ ];
1085
+ for (const artifact of artifacts) {
1086
+ const path = join5(staging, "st", artifact.path);
1087
+ await mkdir5(dirname5(path), { recursive: true });
1088
+ await writeFile3(path, artifact.content);
1089
+ }
1090
+ let vendor;
1091
+ if (!options.emitOnly) {
1092
+ const outDir = shared?.build ? resolve5(dirname5(configFile), shared.build.outDir ?? "build") : join5(previous.root, "engineering");
1093
+ await engineeringDirectory(outDir, configFile);
1094
+ try {
1095
+ vendor = await buildCodesys2(checked.plan, bundle, {
1096
+ configFile,
1097
+ sourceDir: join5(staging, "st"),
1098
+ outDir,
1099
+ controller: options.controller,
1100
+ ...engineering
1101
+ });
1102
+ } catch (cause) {
1103
+ return { diagnostics: diagnoseCodesys2(cause, bundle) };
1104
+ }
1105
+ const vendorArtifacts = [
1106
+ ["Application.project", vendor.project],
1107
+ ["Application.app", vendor.output],
1108
+ ["Application.crc", vendor.checksum]
1109
+ ];
1110
+ for (const [name, path] of vendorArtifacts) {
1111
+ if (!path)
1112
+ continue;
1113
+ const destination = join5(staging, "vendor", name);
1114
+ await mkdir5(dirname5(destination), { recursive: true });
1115
+ await copyFile2(path, destination);
1116
+ }
1117
+ }
1118
+ const status = vendor ? "vendor-compiled" : "emitted";
1119
+ await publishArtifacts(staging, previous, {
1120
+ configFile,
1121
+ status,
1122
+ ...vendor && { vendor: { application: vendor.application, tasks: vendor.tasks } }
1123
+ });
1124
+ staging = undefined;
1125
+ return {
1126
+ directory: previous.output,
1127
+ status,
1128
+ ...controllerConfig && { controllerConfig },
1129
+ diagnostics: [...checked.diagnostics, ...bundle.diagnostics]
1130
+ };
1131
+ } catch (cause) {
1132
+ return { diagnostics: [failure2("config", "PLC5004", cause)] };
1133
+ } finally {
1134
+ if (staging) {
1135
+ await rm5(staging, { recursive: true, force: true });
1136
+ }
1137
+ }
1138
+ }
505
1139
  async function cleanProject(file = "tsplc.config.ts") {
506
1140
  try {
507
- await cleanArtifacts(await realpath(resolve2(file)));
1141
+ await cleanArtifacts(await realpath2(resolve5(file)));
508
1142
  return { diagnostics: [] };
509
1143
  } catch (cause) {
510
- return { diagnostics: [failure("config", "PLC5004", cause)] };
1144
+ return { diagnostics: [failure2("config", "PLC5004", cause)] };
511
1145
  }
512
1146
  }
513
1147
 
@@ -527,14 +1161,32 @@ async function main() {
527
1161
  plc: { type: "boolean" },
528
1162
  server: { type: "boolean" },
529
1163
  simulate: { type: "boolean" },
530
- runtime: { type: "boolean" }
1164
+ runtime: { type: "boolean" },
1165
+ download: { type: "boolean" },
1166
+ restore: { type: "boolean" },
1167
+ select: { type: "boolean" },
1168
+ yes: { type: "boolean", short: "y" }
531
1169
  }
532
1170
  });
533
1171
  const command = positionals[0], file = values.config ?? "tsplc.config.ts";
534
1172
  if (!command || values.help) {
535
- console.log("tsplc install|typegen|check|build|run|download|online-change|inspect|clean|configure [--config tsplc.config.ts] [--plc|--server] [--simulate] [--runtime] [--emit-only] [--format json] [--read symbol]");
1173
+ console.log(`tsplc install|typegen|check|build|run|download|online-change|inspect|clean|configure [--config tsplc.config.ts] [--plc|--server] [--simulate] [--runtime] [--emit-only] [--format json] [--read symbol]
1174
+
1175
+ Plant agent (TSPLC_AGENT_URL, TSPLC_AGENT_TOKEN):
1176
+ tsplc deploy [--download] [--select] [--initialize-user]
1177
+ tsplc target | status | login | logout
1178
+ tsplc vars [path] | read <expression>... | write <expression> <value>
1179
+ tsplc force <expression> <value> | unforce [expression]... [--restore]
1180
+ tsplc start | stop | reset warm|cold (-y skips confirmations)`);
536
1181
  return 0;
537
1182
  }
1183
+ if (plantCommands.has(command)) {
1184
+ const failed = await runPlantCommand(command, positionals.slice(1), values, {
1185
+ file,
1186
+ load: () => loadTsplcConfig3(file)
1187
+ });
1188
+ return failed ? report(failed, values.format) : 0;
1189
+ }
538
1190
  if (positionals.length > 1)
539
1191
  throw new Error("Specify one command.");
540
1192
  if (![
@@ -578,51 +1230,51 @@ async function main() {
578
1230
  result = await cleanProject(file);
579
1231
  } else {
580
1232
  if (["check", "build", "run", "download", "online-change"].includes(command)) {
581
- const diagnostics = checkHostConfig2(resolve3(file));
582
- if (hasErrors2(diagnostics))
1233
+ const diagnostics = checkHostConfig3(resolve6(file));
1234
+ if (hasErrors4(diagnostics))
583
1235
  throw new DiagnosticFailure({ diagnostics });
584
1236
  }
585
- const loaded = await loadTsplcConfig2(file);
1237
+ const loaded = await loadTsplcConfig3(file);
586
1238
  const { config, configFile, root } = loaded;
587
1239
  const target = values.simulate ? { ...loaded, config: simulationConfig(config) } : loaded;
588
1240
  if (command === "install")
589
1241
  result = {
590
- libraries: await installLibraries2(config.dependencies, root, {
1242
+ libraries: await installLibraries3(config.dependencies, root, {
591
1243
  frozen: values["frozen-lockfile"]
592
1244
  })
593
1245
  };
594
1246
  else if (command === "typegen") {
595
- const targets = await projectTargets(file, { loaded });
1247
+ const targets = await projectTargets2(file, { loaded });
596
1248
  if (config.compiler && !values.check) {
597
- await generateProject(file, { hardwareOnly: true, loaded, targets });
598
- const emitted = await buildProject(file, { emitOnly: true, loaded });
599
- if (hasErrors2(emitted.diagnostics))
1249
+ await generateProject2(file, { hardwareOnly: true, loaded, targets });
1250
+ const emitted = await buildProject2(file, { emitOnly: true, loaded });
1251
+ if (hasErrors4(emitted.diagnostics))
600
1252
  throw new DiagnosticFailure(emitted);
601
1253
  }
602
- result = await generateProject(file, { check: values.check, loaded, targets });
1254
+ result = await generateProject2(file, { check: values.check, loaded, targets });
603
1255
  } else if (command === "check") {
604
- result = config.compiler ? await checkProject(file, { loaded }) : {
1256
+ result = config.compiler ? await checkProject2(file, { loaded }) : {
605
1257
  objects: (await loadSources(resolveConfig({
606
1258
  ...rawBuildConfig(config),
607
- io: resolveIo2(config, await loadHardware2(config, root))
1259
+ io: resolveIo3(config, await loadHardware3(config, root))
608
1260
  }, configFile))).objects.length
609
1261
  };
610
1262
  } else if (command === "build" && values.server) {
611
1263
  result = await buildServer(config, root);
612
1264
  } else if (command === "build" || command === "run") {
613
1265
  if (config.compiler) {
614
- const built = await buildProject(file, {
1266
+ const built = await buildProject2(file, {
615
1267
  emitOnly: values["emit-only"],
616
1268
  frozenLockfile: values["frozen-lockfile"],
617
1269
  loaded: target,
618
- controller: controllerFor(config, command),
1270
+ controller: controllerFor2(config, command),
619
1271
  buildTarget: buildTargetFor(command)
620
1272
  });
621
- if (hasErrors2(built.diagnostics))
1273
+ if (hasErrors4(built.diagnostics))
622
1274
  throw new DiagnosticFailure(built);
623
- await generateProject(file, {
1275
+ await generateProject2(file, {
624
1276
  loaded,
625
- targets: await projectTargets(file, { loaded })
1277
+ targets: await projectTargets2(file, { loaded })
626
1278
  });
627
1279
  result = built;
628
1280
  } else {
@@ -631,18 +1283,18 @@ async function main() {
631
1283
  if (!config.build)
632
1284
  throw new Error("Configure build settings.");
633
1285
  const build = rawBuildConfig(config);
634
- const libraries = await installLibraries2(config.dependencies, root, {
1286
+ const libraries = await installLibraries3(config.dependencies, root, {
635
1287
  frozen: values["frozen-lockfile"]
636
1288
  });
637
- const hardware = await loadHardware2(target.config, root);
1289
+ const hardware = await loadHardware3(target.config, root);
638
1290
  result = await buildSt(resolveConfig({
639
1291
  ...build,
640
1292
  libraries,
641
- io: resolveIo2(target.config, hardware),
642
- deviceTree: await deviceTree2(target.config, hardware),
643
- controller: controllerFor(config, command)
1293
+ io: resolveIo3(target.config, hardware),
1294
+ deviceTree: await deviceTree3(target.config, hardware),
1295
+ controller: controllerFor2(config, command)
644
1296
  }, configFile));
645
- await generateProject(file, { hardwareOnly: !config.typegen, loaded });
1297
+ await generateProject2(file, { hardwareOnly: !config.typegen, loaded });
646
1298
  }
647
1299
  if (command === "build" && !values.plc && !values["emit-only"] && config.server?.runtime) {
648
1300
  const backend = await buildServer(config, root);
@@ -656,31 +1308,35 @@ async function main() {
656
1308
  } else if (command === "inspect") {
657
1309
  if (!config.build)
658
1310
  throw new Error("Configure build settings and controller connection.");
659
- result = await inspectController(resolveConfig({ ...config.build, controller: controllerFor(config, command) }, configFile), values.read);
1311
+ result = await inspectController(resolveConfig({ ...config.build, controller: controllerFor2(config, command) }, configFile), values.read);
660
1312
  } else if (["download", "online-change"].includes(command)) {
661
1313
  if (!config.build)
662
1314
  throw new Error("Configure build settings and controller connection.");
663
1315
  let settings;
664
1316
  if (config.compiler) {
665
- const emitted = await buildProject(file, {
1317
+ const emitted = await buildProject2(file, {
666
1318
  emitOnly: true,
1319
+ forController: true,
1320
+ frozenLockfile: values["frozen-lockfile"],
667
1321
  loaded: target,
668
1322
  buildTarget: buildTargetFor(command)
669
1323
  });
670
- if (hasErrors2(emitted.diagnostics))
1324
+ if (hasErrors4(emitted.diagnostics))
671
1325
  throw new DiagnosticFailure(emitted);
672
1326
  if (!emitted.controllerConfig)
673
1327
  throw new Error("Configure build settings for deployment.");
674
1328
  settings = { ...emitted.controllerConfig, io: {} };
675
1329
  } else
676
- settings = resolveConfig({
677
- ...rawBuildConfig(config),
678
- io: resolveIo2(target.config, await loadHardware2(target.config, root))
679
- }, configFile);
680
- const libraries = await installLibraries2(config.dependencies, root, {
681
- frozen: values["frozen-lockfile"]
682
- });
683
- result = await updateController({ ...settings, libraries, controller: controllerFor(config, command) }, command, {
1330
+ settings = {
1331
+ ...resolveConfig({
1332
+ ...rawBuildConfig(config),
1333
+ io: resolveIo3(target.config, await loadHardware3(target.config, root))
1334
+ }, configFile),
1335
+ libraries: await installLibraries3(config.dependencies, root, {
1336
+ frozen: values["frozen-lockfile"]
1337
+ })
1338
+ };
1339
+ result = await updateController({ ...settings, controller: controllerFor2(config, command) }, command, {
684
1340
  initializeUser: values["initialize-user"],
685
1341
  read: values.read
686
1342
  });
@@ -706,7 +1362,7 @@ class DiagnosticFailure extends Error {
706
1362
  }
707
1363
  function report(result, format) {
708
1364
  const diagnostics = result && typeof result === "object" && "diagnostics" in result ? result.diagnostics : [];
709
- const ok = !hasErrors2(diagnostics);
1365
+ const ok = !hasErrors4(diagnostics);
710
1366
  if (format === "json")
711
1367
  console.log(JSON.stringify({ ok, ...result && typeof result === "object" ? result : { result } }));
712
1368
  else {
@@ -736,5 +1392,5 @@ try {
736
1392
  }
737
1393
  }
738
1394
 
739
- //# debugId=0A41ED9B311EA7A064756E2164756E21
1395
+ //# debugId=2A9AAF31224302AE64756E2164756E21
740
1396
  //# sourceMappingURL=cli.js.map