@enerlence/suntropy-cli 0.3.0 → 0.4.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.
@@ -388,9 +388,709 @@ function registerAuthCommands(program2) {
388
388
  });
389
389
  }
390
390
 
391
+ // src/commands/config/shared.ts
392
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, unlinkSync } from "fs";
393
+ import { tmpdir } from "os";
394
+ import { join as join2 } from "path";
395
+ import { spawnSync } from "child_process";
396
+ function getGlobalOpts2(cmd) {
397
+ let root = cmd;
398
+ while (root.parent) root = root.parent;
399
+ return root.opts();
400
+ }
401
+ function coerceValue(raw) {
402
+ if (raw === "true") return true;
403
+ if (raw === "false") return false;
404
+ if (raw === "null") return null;
405
+ if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
406
+ if (/^-?\d*\.\d+$/.test(raw)) return parseFloat(raw);
407
+ if (raw.startsWith("{") && raw.endsWith("}") || raw.startsWith("[") && raw.endsWith("]")) {
408
+ try {
409
+ return JSON.parse(raw);
410
+ } catch {
411
+ }
412
+ }
413
+ return raw;
414
+ }
415
+ function parseSetFlags(entries) {
416
+ if (!entries || entries.length === 0) return {};
417
+ const out = {};
418
+ for (const entry of entries) {
419
+ const eq = entry.indexOf("=");
420
+ if (eq < 0) throw new Error(`Invalid --set value "${entry}" (expected key=value)`);
421
+ const key = entry.slice(0, eq).trim();
422
+ const rawVal = entry.slice(eq + 1);
423
+ if (!key) throw new Error(`Invalid --set value "${entry}" (empty key)`);
424
+ out[key] = coerceValue(rawVal);
425
+ }
426
+ return out;
427
+ }
428
+ function loadFromFile(path) {
429
+ const raw = readFileSync2(path, "utf-8");
430
+ const parsed = JSON.parse(raw);
431
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
432
+ throw new Error(`File ${path} must contain a JSON object at the top level`);
433
+ }
434
+ return parsed;
435
+ }
436
+ function pickKeys(obj, keys) {
437
+ const out = {};
438
+ if (!obj) return out;
439
+ for (const k of keys) {
440
+ if (k in obj) out[k] = obj[k];
441
+ }
442
+ return out;
443
+ }
444
+ function buildPayload(flagValues, setEntries, fromFile, allowedKeys) {
445
+ const payload = {};
446
+ if (fromFile) Object.assign(payload, loadFromFile(fromFile));
447
+ if (setEntries) Object.assign(payload, parseSetFlags(setEntries));
448
+ for (const [k, v] of Object.entries(flagValues)) {
449
+ if (v !== void 0) payload[k] = v;
450
+ }
451
+ if (allowedKeys) {
452
+ for (const k of Object.keys(payload)) {
453
+ if (!allowedKeys.includes(k)) {
454
+ throw new Error(`Field "${k}" does not belong to this section. Allowed: ${allowedKeys.join(", ")}`);
455
+ }
456
+ }
457
+ }
458
+ return payload;
459
+ }
460
+ function editJson(initial, filenameHint = "config") {
461
+ const editor = process.env.EDITOR || process.env.VISUAL || "vi";
462
+ const tmp = join2(tmpdir(), `suntropy-${filenameHint}-${Date.now()}.json`);
463
+ writeFileSync3(tmp, JSON.stringify(initial ?? {}, null, 2));
464
+ try {
465
+ const res = spawnSync(editor, [tmp], { stdio: "inherit" });
466
+ if (res.status !== 0) throw new Error(`Editor "${editor}" exited with status ${res.status}`);
467
+ const updated = readFileSync2(tmp, "utf-8");
468
+ const parsed = JSON.parse(updated);
469
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
470
+ throw new Error("Edited content must be a JSON object at the top level");
471
+ }
472
+ return parsed;
473
+ } finally {
474
+ try {
475
+ unlinkSync(tmp);
476
+ } catch {
477
+ }
478
+ }
479
+ }
480
+
481
+ // src/commands/config/theme.ts
482
+ var THEME_KEYS = [
483
+ "idClientThemeConfig",
484
+ "primary",
485
+ "btnPrimary",
486
+ "btnSecondary",
487
+ "background",
488
+ "navbarBackgroundColor",
489
+ "graph1",
490
+ "graph2",
491
+ "graph3",
492
+ "graph4",
493
+ "graph5",
494
+ "graph6",
495
+ "logoUrl",
496
+ "faviconUrl",
497
+ "clientAppTitle",
498
+ "carouselLinks",
499
+ "enableCustomTheme"
500
+ ];
501
+ function registerThemeCommands(configRoot) {
502
+ const theme = configRoot.command("theme").description(
503
+ "Client branding & theme (security service).\nColours, logo, favicon and app title applied across Suntropy.\nEndpoints: GET /clients/config/clientThemeByClientUID/:uid (public)\n POST /clients/config/updateTheme (requires admin role)\n\nFields:\n primary, btnPrimary, btnSecondary Core palette (hex).\n background, navbarBackgroundColor Surface colours.\n graph1..graph6 Chart palette (6 slots).\n logoUrl, faviconUrl Asset URLs.\n clientAppTitle Visible app title.\n carouselLinks JSON-serialised carousel config.\n enableCustomTheme Toggles the custom theme on/off."
504
+ );
505
+ theme.command("get").description("Fetch the theme of a client (public endpoint, no auth required for reads).").requiredOption("--client-uid <uid>", "Client UID whose theme should be fetched").action(async (opts) => {
506
+ try {
507
+ const global = getGlobalOpts2(theme);
508
+ const client = createServiceClient("security", global);
509
+ const res = await client.get(`/clients/config/clientThemeByClientUID/${opts.clientUid}`);
510
+ output(res.data, global);
511
+ } catch (err) {
512
+ outputError(handleApiError(err));
513
+ }
514
+ });
515
+ theme.command("set").description(
516
+ 'Update the theme of the authenticated client (requires admin role).\nCombine individual flags, repeatable --set key=value, and/or --from-file.\nExample:\n suntropy config theme set --primary "#0066ff" --logo-url https://cdn/logo.svg\n suntropy config theme set --set graph1=#ff0000 --set graph2=#00ff00\n suntropy config theme set --from-file theme.json'
517
+ ).option("--primary <hex>", "Primary colour").option("--btn-primary <hex>", "Primary button colour").option("--btn-secondary <hex>", "Secondary button colour").option("--background <hex>", "Background colour").option("--navbar-background-color <hex>", "Navbar background colour").option("--logo-url <url>", "Logo URL").option("--favicon-url <url>", "Favicon URL").option("--client-app-title <title>", "Visible app title").option("--enable-custom-theme [bool]", "Toggle the custom theme (true/false)").option("--set <entries...>", "Additional field assignments as key=value (repeatable)").option("--from-file <path>", "Load a partial payload from a JSON file").action(async (opts) => {
518
+ try {
519
+ const global = getGlobalOpts2(theme);
520
+ const client = createServiceClient("security", global);
521
+ const flagValues = {
522
+ primary: opts.primary,
523
+ btnPrimary: opts.btnPrimary,
524
+ btnSecondary: opts.btnSecondary,
525
+ background: opts.background,
526
+ navbarBackgroundColor: opts.navbarBackgroundColor,
527
+ logoUrl: opts.logoUrl,
528
+ faviconUrl: opts.faviconUrl,
529
+ clientAppTitle: opts.clientAppTitle,
530
+ enableCustomTheme: opts.enableCustomTheme === void 0 ? void 0 : opts.enableCustomTheme === true || opts.enableCustomTheme === "true"
531
+ };
532
+ const payload = buildPayload(flagValues, opts.set, opts.fromFile, THEME_KEYS);
533
+ if (Object.keys(payload).length === 0) {
534
+ outputError(new Error("No fields provided. Use flags, --set, or --from-file."));
535
+ return;
536
+ }
537
+ const res = await client.post("/clients/config/updateTheme", payload);
538
+ output(res.data, global);
539
+ } catch (err) {
540
+ outputError(handleApiError(err));
541
+ }
542
+ });
543
+ theme.command("edit").description("Open $EDITOR with the theme JSON and PUT the edited result.").requiredOption("--client-uid <uid>", "Client UID to fetch the current theme from").action(async (opts) => {
544
+ try {
545
+ const global = getGlobalOpts2(theme);
546
+ const client = createServiceClient("security", global);
547
+ const current = await client.get(`/clients/config/clientThemeByClientUID/${opts.clientUid}`);
548
+ const subset = pickKeys(current.data, THEME_KEYS);
549
+ const edited = editJson(subset, "theme");
550
+ const res = await client.post("/clients/config/updateTheme", edited);
551
+ output(res.data, global);
552
+ } catch (err) {
553
+ outputError(handleApiError(err));
554
+ }
555
+ });
556
+ }
557
+
558
+ // src/commands/config/advanced.ts
559
+ function kebab(key) {
560
+ return key.replace(/[A-Z0-9]+/g, (m, i) => i === 0 ? m.toLowerCase() : "-" + m.toLowerCase());
561
+ }
562
+ var GENERAL = {
563
+ name: "general",
564
+ title: "General configuration (branding, language, tracking, base aesthetics).",
565
+ summary: 'Applies to the entire widget: navbar, favicon, global theme, analytics/pixels\nand page-wide injected scripts. Equivalent to the "General Configuration"\npanel in the Suntropy admin.',
566
+ fields: [
567
+ "logoUrl",
568
+ "faviconUrl",
569
+ "tabTitle",
570
+ "onAddressButtonClick",
571
+ "onSendButtonClick",
572
+ "googleConversionId",
573
+ "googleConversionLabelOnAddress",
574
+ "googleConversionLabelOnSend",
575
+ "metaConversionId",
576
+ "customTrackingHTML",
577
+ "defaultLanguage",
578
+ "enableLanguageMenu",
579
+ "customColorOfElements",
580
+ "steticVariant",
581
+ "themeColor",
582
+ "navbarColor",
583
+ "addNumberToSteps",
584
+ "showFooter"
585
+ ],
586
+ fieldDocs: {
587
+ logoUrl: "Logo displayed in the navbar (and on the cover if includeLogoInCover=true). Empty = no logo.",
588
+ faviconUrl: "Browser tab favicon.",
589
+ tabTitle: "Browser tab title (HTML <title>).",
590
+ onAddressButtonClick: 'JavaScript evaluated (eval) when the user hits "Continue" on the address step. \u26A0 XSS risk; never accept untrusted input.',
591
+ onSendButtonClick: "JavaScript evaluated (eval) when the final form is submitted. Same XSS risk as above.",
592
+ googleConversionId: "Google Ads ID; injects gtag.js in <head>.",
593
+ googleConversionLabelOnAddress: "Google conversion label fired after the address step.",
594
+ googleConversionLabelOnSend: "Google conversion label fired on submit.",
595
+ metaConversionId: 'Meta/Facebook pixel ID; fires a "Lead" event on submit.',
596
+ customTrackingHTML: "Arbitrary HTML/JS injected in <head> (Hotjar, Mixpanel, GTM\u2026).",
597
+ defaultLanguage: 'Initial language: en|es|fr|it|pt|cat. Falls back to browser pref, then "en".',
598
+ enableLanguageMenu: "Shows the language picker in the navbar. false pins the user to defaultLanguage.",
599
+ customColorOfElements: "Accent colour for buttons / active borders.",
600
+ steticVariant: "Visual variant: default (rounded) | sharped (hard corners) | simple (minimalist, no shadows).",
601
+ themeColor: "Primary colour (navbar, buttons, focus ring). Default #575757.",
602
+ navbarColor: "Overrides themeColor for the navbar only.",
603
+ addNumberToSteps: 'Prefix step names with their number ("1. Address", \u2026).',
604
+ showFooter: "Pins the footer to the bottom instead of inlining it in the flow."
605
+ }
606
+ };
607
+ var COVER = {
608
+ name: "cover",
609
+ title: "Cover / hero screen shown before the address step.",
610
+ summary: "Configures the first screen the user sees: title, subtitle, carousel background, colours and logo placement.",
611
+ fields: [
612
+ "coverTitle",
613
+ "coverSubtitle",
614
+ "coverBackgroundImageUrl1",
615
+ "coverBackgroundImageUrl2",
616
+ "coverBackgroundImageUrl3",
617
+ "coverBackgroundImageUrl4",
618
+ "staticCoverImages",
619
+ "coverTextColor",
620
+ "coverFilterColor",
621
+ "includeLogoInCover"
622
+ ],
623
+ fieldDocs: {
624
+ coverTitle: "Hero title.",
625
+ coverSubtitle: "Hero subtitle below the title.",
626
+ coverBackgroundImageUrl1: "Background image #1 (also first frame of the carousel).",
627
+ coverBackgroundImageUrl2: "Background image #2.",
628
+ coverBackgroundImageUrl3: "Background image #3.",
629
+ coverBackgroundImageUrl4: "Background image #4.",
630
+ staticCoverImages: "true = no rotation (always shows the first image). false = carousel auto-rotates.",
631
+ coverTextColor: "Colour applied to coverTitle and coverSubtitle.",
632
+ coverFilterColor: "Overlay colour on top of the image. Default rgba(75,55,30,0.7); improves text contrast.",
633
+ includeLogoInCover: "true repeats the navbar logo on the cover. false keeps it only in the navbar."
634
+ }
635
+ };
636
+ var SURFACES = {
637
+ name: "surfaces",
638
+ title: "Surfaces step: the map where the user draws the roof polygon(s).",
639
+ summary: "Controls whether the step runs, how many surfaces are allowed, and the per-device help messages.",
640
+ fields: ["surfaceStepEnabled", "multipleSurfaces", "enableMapMarker", "messageOnDesktopDevices", "messageOnMobileDevices"],
641
+ fieldDocs: {
642
+ surfaceStepEnabled: "Enables/disables the whole step. false = the lead is sized without any polygon input.",
643
+ multipleSurfaces: "Allows drawing more than one roof surface per lead.",
644
+ enableMapMarker: "Drops a pin on the installation location over the satellite view.",
645
+ messageOnDesktopDevices: "Instruction text shown on desktop during the step.",
646
+ messageOnMobileDevices: 'Instruction text shown on mobile (e.g. "Tap to draw").'
647
+ }
648
+ };
649
+ var CONSUMER = {
650
+ name: "consumer",
651
+ title: "Consumer step: residential / commercial / community picker.",
652
+ summary: "Each subtype has its own visibility flag, image and label. defaultConsumptionPattern feeds the calculation engine.",
653
+ fields: [
654
+ "consumerStepEnabled",
655
+ "residentialConsumerTypeEnabled",
656
+ "residentialConsumerImageUrl",
657
+ "residentialConsumerTitle",
658
+ "commercialConsumerTypeEnabled",
659
+ "commercialConsumerImageUrl",
660
+ "commercialConsumerTitle",
661
+ "communityConsumerTypeEnabled",
662
+ "communityConsumerImageUrl",
663
+ "communityConsumerTitle",
664
+ "defaultConsumptionPattern"
665
+ ],
666
+ fieldDocs: {
667
+ consumerStepEnabled: "Shows/hides the whole step.",
668
+ residentialConsumerTypeEnabled: 'Makes the "Residential" option visible.',
669
+ residentialConsumerImageUrl: "Icon/photo for the Residential option.",
670
+ residentialConsumerTitle: 'Residential label (e.g. "Home").',
671
+ commercialConsumerTypeEnabled: 'Makes the "Commercial" option visible.',
672
+ commercialConsumerImageUrl: "Icon/photo for the Commercial option.",
673
+ commercialConsumerTitle: 'Commercial label (e.g. "Business").',
674
+ communityConsumerTypeEnabled: 'Makes the "Community" option visible.',
675
+ communityConsumerImageUrl: "Icon/photo for the Community option.",
676
+ communityConsumerTitle: "Community label.",
677
+ defaultConsumptionPattern: "Preselected pattern: Balance | Nightly | Morning | Afternoon | Domestic | Commercial | Community. Feeds the hourly curve used by the engine."
678
+ }
679
+ };
680
+ var INCLINATION = {
681
+ name: "inclination",
682
+ title: "Roof inclination step (flat / inclined / very inclined).",
683
+ summary: "Each variant carries its own image and label. defaultInclination is also used when the step is disabled.",
684
+ fields: [
685
+ "inclinationStepEnabled",
686
+ "flatRoofImageUrl",
687
+ "textFlatRoof",
688
+ "inclinedRoofImageUrl",
689
+ "textInclinedRoof",
690
+ "veryInclinedRoofImageUrl",
691
+ "textVeryInclinedRoof",
692
+ "defaultInclination",
693
+ "inclinationStepTitle"
694
+ ],
695
+ fieldDocs: {
696
+ inclinationStepEnabled: "Shows/hides the step. When false, defaultInclination is fed directly into the calculation.",
697
+ flatRoofImageUrl: "Image for the flat-roof option (0-15\xB0).",
698
+ textFlatRoof: 'Label for flat roof. Default "0-15\xB0".',
699
+ inclinedRoofImageUrl: "Image for the inclined-roof option (15-30\xB0).",
700
+ textInclinedRoof: 'Label for inclined roof. Default "15-30\xB0".',
701
+ veryInclinedRoofImageUrl: "Image for the very-inclined option (>30\xB0).",
702
+ textVeryInclinedRoof: 'Label for very inclined roof. Default ">30\xB0".',
703
+ defaultInclination: "Degrees (0-90). Preselected value and fallback when the step is disabled.",
704
+ inclinationStepTitle: "Title for the step."
705
+ }
706
+ };
707
+ var ORIENTATION = {
708
+ name: "orientation",
709
+ title: "Roof orientation (azimuth) step.",
710
+ summary: "Directly impacts yield estimates.",
711
+ fields: ["orientationStepEnabled", "defaultOrientation"],
712
+ fieldDocs: {
713
+ orientationStepEnabled: "Shows/hides the step. When false, defaultOrientation is fed to the engine.",
714
+ defaultOrientation: "Degrees 0-360 (0=N, 90=E, 180=S, 270=W)."
715
+ }
716
+ };
717
+ var PANELS = {
718
+ name: "panels",
719
+ title: "Solar panel & kit-category selection step.",
720
+ summary: "Drives which panel + kit category the lead is sized with.",
721
+ fields: ["panelStepEnabled", "defaultSolarPanel", "kitCategories", "defaultKitCategoryId", "panelSectionTitle", "panelTitle"],
722
+ fieldDocs: {
723
+ panelStepEnabled: "Shows/hides the step. When false, the backend uses defaultSolarPanel + defaultKitCategoryId unattended.",
724
+ defaultSolarPanel: "ID of the preselected SolarPanel (reference to the client inventory).",
725
+ kitCategories: "Array of kit categories offered to the lead. Each item: { id, name, description, priority }. Use --from-file for multi-item payloads.",
726
+ defaultKitCategoryId: "ID of the preselected kit category.",
727
+ panelSectionTitle: "Navbar/breadcrumb label for the panel section.",
728
+ panelTitle: "Step title."
729
+ }
730
+ };
731
+ var CONSUMPTION = {
732
+ name: "consumption",
733
+ title: "Electric-consumption input step.",
734
+ summary: "Controls the input mode and display density of the consumption step.",
735
+ fields: ["showOnlyOneConsumptionFieldAtATime", "defaultConsumptionIntroductionMode"],
736
+ fieldDocs: {
737
+ showOnlyOneConsumptionFieldAtATime: "true = guided mode (one field per screen). false = all fields visible at once.",
738
+ defaultConsumptionIntroductionMode: "monthlyConsumption (kWh/month) | monthlySpending (EUR/month). User can toggle if both are enabled."
739
+ }
740
+ };
741
+ var RESULTS = {
742
+ name: "results",
743
+ title: "Results screen, contact form and calculation-engine hooks.",
744
+ summary: "Largest section: visual layout, form fields for lead capture, confirmation modal, and engine toggles (batteries, PPA, alternative endpoint).",
745
+ fields: [
746
+ "resultsMode",
747
+ "resultsBackgroundImageUrl",
748
+ "hideBackgroundImageInResults",
749
+ "colorOfBackgroundInResults",
750
+ "includeLogoInResults",
751
+ "includeMapInResults",
752
+ "hideResults",
753
+ "hideROI",
754
+ "formTitle",
755
+ "formSubtitle",
756
+ "resultsWhenSent",
757
+ "colorOfResultsPanelBackground",
758
+ "showDniFieldOnResults",
759
+ "showPhonePrefixFieldOnResults",
760
+ "showTypeOfDocumentFieldOnResults",
761
+ "showDniValidationFieldOnResults",
762
+ "showPhoneNumberValidationFieldOnResults",
763
+ "showIdentifierValidationFieldOnResults",
764
+ "showSurnameValidationFieldOnResults",
765
+ "showTypeOfClientSelectorOnResults",
766
+ "titleModalOfConfirm",
767
+ "textModalOfConfirm",
768
+ "businessName",
769
+ "enablePeakPowerLimitation",
770
+ "includeBatteries",
771
+ "enabledPPACalculation",
772
+ "hideCommentField",
773
+ "alternativeCalculationEndpoint",
774
+ "alternativeCalculationLoadingMessage",
775
+ "redirectToShareable"
776
+ ],
777
+ fieldDocs: {
778
+ resultsMode: "Default (ROI, payback, savings) | SolarResource (irradiance map + technical data).",
779
+ resultsBackgroundImageUrl: "Background image behind the results panel.",
780
+ hideBackgroundImageInResults: "Hides the background image even when set.",
781
+ colorOfBackgroundInResults: "Overlay colour on top of the background image. Default rgba(75,55,30,0.7).",
782
+ colorOfResultsPanelBackground: "Background of the metrics card. Default rgba(0,0,0,0.5).",
783
+ includeLogoInResults: "Shows the logo on the results screen.",
784
+ includeMapInResults: "Shows the interactive map with the drawn panels.",
785
+ hideResults: "true = no metrics shown, only the contact form.",
786
+ hideROI: "Hides the payback/ROI widget.",
787
+ formTitle: "Contact form title.",
788
+ formSubtitle: "Contact form subtitle.",
789
+ resultsWhenSent: "true = results shown before submit. false = form must be filled first.",
790
+ showDniFieldOnResults: "Adds a DNI/CIF input to the form.",
791
+ showPhonePrefixFieldOnResults: "Adds a phone-prefix selector.",
792
+ showTypeOfDocumentFieldOnResults: "Adds a document-type selector.",
793
+ showDniValidationFieldOnResults: "Visual DNI validation (green check).",
794
+ showPhoneNumberValidationFieldOnResults: "Visual phone validation.",
795
+ showIdentifierValidationFieldOnResults: "Visual identifier validation.",
796
+ showSurnameValidationFieldOnResults: "Visual surname validation.",
797
+ showTypeOfClientSelectorOnResults: "Selector for Individual / Company / Community.",
798
+ titleModalOfConfirm: "Title of the confirmation modal shown before submit.",
799
+ textModalOfConfirm: "Body of the confirmation modal.",
800
+ businessName: 'Adds a "Business name" field to the form.',
801
+ enablePeakPowerLimitation: "Adds a peak-power limiter to the consumption section. Affects engine sizing.",
802
+ includeBatteries: "Includes battery sizing in the results.",
803
+ enabledPPACalculation: "Enables PPA (Power Purchase Agreement) modelling.",
804
+ hideCommentField: "Hides the free-text comment field.",
805
+ alternativeCalculationEndpoint: "URL to which calculation data is POSTed instead of the default engine endpoint. For white-label integrations.",
806
+ alternativeCalculationLoadingMessage: "Loading text while awaiting the alternative endpoint response.",
807
+ redirectToShareable: "After calculation, redirect to the shareable-study URL instead of rendering results in-place."
808
+ }
809
+ };
810
+ var CUSTOM_FIELDS = {
811
+ name: "custom-fields",
812
+ title: "Extra lead fields persisted on the SolarStudy (plugin-gated).",
813
+ summary: 'Only available when the client has the "studyCustomFields" plugin active.',
814
+ fields: ["solarStudyCustomFields"],
815
+ fieldDocs: {
816
+ solarStudyCustomFields: "Array of custom-field definitions, serialised. Each item: { id, label, type, required, options? }. Types: text|number|select|boolean. Use --from-file to avoid quoting headaches."
817
+ }
818
+ };
819
+ var SECTIONS = [
820
+ GENERAL,
821
+ COVER,
822
+ SURFACES,
823
+ CONSUMER,
824
+ INCLINATION,
825
+ ORIENTATION,
826
+ PANELS,
827
+ CONSUMPTION,
828
+ RESULTS,
829
+ CUSTOM_FIELDS
830
+ ];
831
+ function getAdvanced(client) {
832
+ return client.get("/solar-form/config/advanced").then((r) => r.data);
833
+ }
834
+ function putAdvanced(client, payload) {
835
+ return client.put("/solar-form/config/advanced", payload).then((r) => r.data);
836
+ }
837
+ function formatFieldsHelp(section) {
838
+ const longest = Math.max(...section.fields.map((f) => f.length));
839
+ return [
840
+ section.title,
841
+ "",
842
+ section.summary,
843
+ "",
844
+ "Fields:",
845
+ ...section.fields.map((f) => ` ${f.padEnd(longest)} ${section.fieldDocs[f] ?? ""}`),
846
+ "",
847
+ "Operations: get | set [--set key=value] [--from-file] [--<field> <value>] | edit"
848
+ ].join("\n");
849
+ }
850
+ function attachSectionCommand(advanced, section) {
851
+ const sec = advanced.command(section.name).description(formatFieldsHelp(section));
852
+ sec.command("get").description(`Fetch only the ${section.name} fields of the advanced config.`).action(async () => {
853
+ try {
854
+ const global = getGlobalOpts2(sec);
855
+ const client = createServiceClient("solar", global);
856
+ const doc = await getAdvanced(client);
857
+ output(pickKeys(doc, section.fields), global);
858
+ } catch (err) {
859
+ outputError(handleApiError(err));
860
+ }
861
+ });
862
+ const setCmd = sec.command("set").description(
863
+ `Update the ${section.name} fields of the advanced config.
864
+ Combine per-field flags, repeatable --set key=value, and/or --from-file.
865
+ Internally GETs the current config, merges this section, and PUTs the whole thing.`
866
+ ).option("--set <entries...>", "Field assignments as key=value (repeatable)").option("--from-file <path>", "Load a partial payload from a JSON file");
867
+ for (const field of section.fields) {
868
+ setCmd.option(`--${kebab(field)} <value>`, section.fieldDocs[field] ?? field);
869
+ }
870
+ setCmd.action(async (opts) => {
871
+ try {
872
+ const global = getGlobalOpts2(sec);
873
+ const client = createServiceClient("solar", global);
874
+ const flagValues = {};
875
+ for (const field of section.fields) {
876
+ const val = opts[field];
877
+ if (val !== void 0) flagValues[field] = coerceValue(String(val));
878
+ }
879
+ const partial = buildPayload(flagValues, opts.set, opts.fromFile, section.fields);
880
+ if (Object.keys(partial).length === 0) {
881
+ outputError(new Error("No fields provided. Use flags, --set, or --from-file."));
882
+ return;
883
+ }
884
+ const current = await getAdvanced(client);
885
+ const merged = { ...current, ...partial };
886
+ const res = await putAdvanced(client, merged);
887
+ output(pickKeys(res, section.fields), global);
888
+ } catch (err) {
889
+ outputError(handleApiError(err));
890
+ }
891
+ });
892
+ sec.command("edit").description(`Open $EDITOR on the ${section.name} subset of the advanced config, then PUT the whole config.`).action(async () => {
893
+ try {
894
+ const global = getGlobalOpts2(sec);
895
+ const client = createServiceClient("solar", global);
896
+ const current = await getAdvanced(client);
897
+ const subset = pickKeys(current, section.fields);
898
+ const edited = editJson(subset, `advanced-${section.name}`);
899
+ const merged = { ...current, ...edited };
900
+ const res = await putAdvanced(client, merged);
901
+ output(pickKeys(res, section.fields), global);
902
+ } catch (err) {
903
+ outputError(handleApiError(err));
904
+ }
905
+ });
906
+ }
907
+ function registerAdvancedCommands(solarform) {
908
+ const advanced = solarform.command("advanced").description(
909
+ "SolarForm Advanced configuration (white-label microsite).\nEach subcommand maps 1:1 with a section of the admin accordion and with a\nsubset of fields in the AdvancedSolarFormConfig DTO.\n\nWhole-payload operations:\n get Fetch the full advanced config.\n update Merge a partial payload (--set / --from-file).\n delete Delete the authenticated client's advanced config.\n init-default Create the default config for a clientUID (superadmin / localhost).\n\nSections (each: get / set / edit):\n" + SECTIONS.map((s) => ` ${s.name.padEnd(14)} ${s.title}`).join("\n") + "\n\nConventions:\n --set <field>=<value> Assign a single field (repeatable).\n --from-file <path> Load a partial payload from JSON.\n edit Open $EDITOR on the section's current state."
910
+ );
911
+ advanced.command("get").description("Fetch the full AdvancedSolarFormConfig for the authenticated client.").action(async () => {
912
+ try {
913
+ const global = getGlobalOpts2(advanced);
914
+ const client = createServiceClient("solar", global);
915
+ const data = await getAdvanced(client);
916
+ output(data, global);
917
+ } catch (err) {
918
+ outputError(handleApiError(err));
919
+ }
920
+ });
921
+ advanced.command("update").description("Merge a partial AdvancedSolarFormConfig from --set / --from-file and PUT.").option("--set <entries...>", "Field assignments as key=value (repeatable)").option("--from-file <path>", "Load a partial payload from a JSON file").action(async (opts) => {
922
+ try {
923
+ const global = getGlobalOpts2(advanced);
924
+ const client = createServiceClient("solar", global);
925
+ const partial = buildPayload({}, opts.set, opts.fromFile);
926
+ if (Object.keys(partial).length === 0) {
927
+ outputError(new Error("No fields provided. Use --set or --from-file."));
928
+ return;
929
+ }
930
+ const current = await getAdvanced(client);
931
+ const res = await putAdvanced(client, { ...current, ...partial });
932
+ output(res, global);
933
+ } catch (err) {
934
+ outputError(handleApiError(err));
935
+ }
936
+ });
937
+ advanced.command("delete").description("Delete the authenticated client's advanced config.").action(async () => {
938
+ try {
939
+ const global = getGlobalOpts2(advanced);
940
+ const client = createServiceClient("solar", global);
941
+ const res = await client.delete("/solar-form/config/advanced");
942
+ output(res.data, global);
943
+ } catch (err) {
944
+ outputError(handleApiError(err));
945
+ }
946
+ });
947
+ advanced.command("init-default").description(
948
+ 'Create the default advanced config for a clientUID.\nRestricted: requires the "suntropy-auth: <clientUID>" header or localhost execution.'
949
+ ).requiredOption("--client-uid <uid>", "Client UID to initialise").option("--email <email>", "Operator email (optional)").option("--url <url>", "Site URL (optional)").action(async (opts) => {
950
+ try {
951
+ const global = getGlobalOpts2(advanced);
952
+ const client = createServiceClient("solar", global);
953
+ const params = {};
954
+ if (opts.email) params.email = opts.email;
955
+ if (opts.url) params.url = opts.url;
956
+ const res = await client.post(
957
+ `/solar-form/config/default/advanced/${opts.clientUid}`,
958
+ {},
959
+ { params, headers: { "suntropy-auth": opts.clientUid } }
960
+ );
961
+ output(res.data, global);
962
+ } catch (err) {
963
+ outputError(handleApiError(err));
964
+ }
965
+ });
966
+ for (const section of SECTIONS) attachSectionCommand(advanced, section);
967
+ }
968
+
969
+ // src/commands/config/solarform.ts
970
+ var SOLARFORM_KEYS = [
971
+ "idSolarFormConfig",
972
+ "clientUID",
973
+ "solarFormUrl",
974
+ "enabled",
975
+ "notificationEmailAddress",
976
+ "confirmationReplytoEmailAdress",
977
+ "notificationEmailSubject",
978
+ "enableNotificationEmail",
979
+ "enableSendConfirmationEmailToClient",
980
+ "confirmationEmailSubject",
981
+ "confirmationEmailBody",
982
+ "enableConfirmationEmailBody",
983
+ "formTitle",
984
+ "formSubtitle",
985
+ "formBackgroundColor",
986
+ "formBackgroundImageURL",
987
+ "formFaviconUrl",
988
+ "locationMode",
989
+ "hideFinalProjectPrice",
990
+ "enableRequiredPhoneNumberField",
991
+ "enableRequiredNameField",
992
+ "callToActionButtonText",
993
+ "renderPDF",
994
+ "defaultSolarStudyTemplateId",
995
+ "redirectUrl",
996
+ "privacyPolicy",
997
+ "advertisingPolicy",
998
+ "generateShareable",
999
+ "incentiveTemplateGroup",
1000
+ "disabledSolarForm",
1001
+ "redirectOnClose"
1002
+ ];
1003
+ function registerSolarformConfigCommands(configRoot) {
1004
+ const solarform = configRoot.command("solarform").description(
1005
+ 'SolarForm (basic) and SolarForm Advanced configuration (solar service).\nThe basic config controls the public lead-capture form (URL, notifications,\nappearance, mandatory fields). The advanced subtree configures the modern\nwhite-label widget rendered by the "advanced-solar-form" microsite.\n\nEndpoints:\n GET /solar-form/get-solar-form-config?getParameters=\n GET /solar-form/solar-form-config (public, resolved by Origin)\n POST /solar-form/solar-form-config?getParameters=\n PUT /solar-form/solar-form-config/:id'
1006
+ );
1007
+ solarform.command("get").description("Fetch the current SolarForm configuration for the authenticated client.").option("--with-parameters", "Include the associated SolarFormParameters in the response").action(async (opts) => {
1008
+ try {
1009
+ const global = getGlobalOpts2(solarform);
1010
+ const client = createServiceClient("solar", global);
1011
+ const params = {};
1012
+ if (opts.withParameters) params.getParameters = "true";
1013
+ const res = await client.get("/solar-form/get-solar-form-config", { params });
1014
+ output(res.data, global);
1015
+ } catch (err) {
1016
+ outputError(handleApiError(err));
1017
+ }
1018
+ });
1019
+ solarform.command("create").description(
1020
+ "Create a new SolarFormConfig for the authenticated client.\n--url is the public slug/URL of the form. Other fields may come from flags,\n--set key=value, or --from-file. Accepted keys:\n " + SOLARFORM_KEYS.join(", ")
1021
+ ).requiredOption("--url <slug>", "Public URL/slug of the form (solarFormUrl)").option("--enabled [bool]", "Whether the form is publicly enabled", "true").option("--form-title <text>", "Form title displayed to the end user").option("--form-subtitle <text>", "Form subtitle").option("--form-background-color <color>", "Background colour of the form container").option("--form-background-image-url <url>", "Background image URL").option("--form-favicon-url <url>", "Favicon URL").option("--call-to-action-button-text <text>", "Primary CTA button label").option("--notification-email-address <email>", "Where new lead notifications go").option("--location-mode <mode>", "fullSurface | locationOnly").option("--with-parameters", "Include SolarFormParameters in the response").option("--set <entries...>", "Additional field assignments as key=value (repeatable)").option("--from-file <path>", "Load a payload from a JSON file").action(async (opts) => {
1022
+ try {
1023
+ const global = getGlobalOpts2(solarform);
1024
+ const client = createServiceClient("solar", global);
1025
+ const flagValues = {
1026
+ solarFormUrl: opts.url,
1027
+ enabled: opts.enabled === void 0 ? void 0 : opts.enabled === true || opts.enabled === "true",
1028
+ formTitle: opts.formTitle,
1029
+ formSubtitle: opts.formSubtitle,
1030
+ formBackgroundColor: opts.formBackgroundColor,
1031
+ formBackgroundImageURL: opts.formBackgroundImageUrl,
1032
+ formFaviconUrl: opts.formFaviconUrl,
1033
+ callToActionButtonText: opts.callToActionButtonText,
1034
+ notificationEmailAddress: opts.notificationEmailAddress,
1035
+ locationMode: opts.locationMode
1036
+ };
1037
+ const payload = buildPayload(flagValues, opts.set, opts.fromFile, SOLARFORM_KEYS);
1038
+ const params = {};
1039
+ if (opts.withParameters) params.getParameters = "true";
1040
+ const res = await client.post("/solar-form/solar-form-config", payload, { params });
1041
+ output(res.data, global);
1042
+ } catch (err) {
1043
+ outputError(handleApiError(err));
1044
+ }
1045
+ });
1046
+ solarform.command("update <idSolarFormConfig>").description("Update an existing SolarFormConfig by numeric ID.").option("--set <entries...>", "Additional field assignments as key=value (repeatable)").option("--from-file <path>", "Load a payload from a JSON file").option("--form-title <text>", "Form title").option("--form-subtitle <text>", "Form subtitle").option("--enabled [bool]", "Whether the form is publicly enabled").option("--disabled-solar-form [bool]", "Explicit disable flag").action(async (idSolarFormConfig, opts) => {
1047
+ try {
1048
+ const global = getGlobalOpts2(solarform);
1049
+ const client = createServiceClient("solar", global);
1050
+ const flagValues = {
1051
+ formTitle: opts.formTitle,
1052
+ formSubtitle: opts.formSubtitle,
1053
+ enabled: opts.enabled === void 0 ? void 0 : opts.enabled === true || opts.enabled === "true",
1054
+ disabledSolarForm: opts.disabledSolarForm === void 0 ? void 0 : opts.disabledSolarForm === true || opts.disabledSolarForm === "true"
1055
+ };
1056
+ const payload = buildPayload(flagValues, opts.set, opts.fromFile, SOLARFORM_KEYS);
1057
+ if (Object.keys(payload).length === 0) {
1058
+ outputError(new Error("No fields to update."));
1059
+ return;
1060
+ }
1061
+ const res = await client.put(`/solar-form/solar-form-config/${idSolarFormConfig}`, payload);
1062
+ output(res.data, global);
1063
+ } catch (err) {
1064
+ outputError(handleApiError(err));
1065
+ }
1066
+ });
1067
+ solarform.command("edit").description("Fetch the current SolarForm config, open $EDITOR, and PUT the edited result.").action(async () => {
1068
+ try {
1069
+ const global = getGlobalOpts2(solarform);
1070
+ const client = createServiceClient("solar", global);
1071
+ const current = await client.get("/solar-form/get-solar-form-config");
1072
+ const doc = current.data;
1073
+ const id = doc?.idSolarFormConfig;
1074
+ if (!id) {
1075
+ outputError(new Error('No existing SolarFormConfig found to edit. Use "create" first.'));
1076
+ return;
1077
+ }
1078
+ const subset = pickKeys(doc, SOLARFORM_KEYS);
1079
+ const edited = editJson(subset, "solarform");
1080
+ const res = await client.put(`/solar-form/solar-form-config/${id}`, edited);
1081
+ output(res.data, global);
1082
+ } catch (err) {
1083
+ outputError(handleApiError(err));
1084
+ }
1085
+ });
1086
+ registerAdvancedCommands(solarform);
1087
+ }
1088
+
391
1089
  // src/commands/config.ts
392
1090
  function registerConfigCommands(program2) {
393
- const cfg = program2.command("config").description("CLI configuration management");
1091
+ const cfg = program2.command("config").description(
1092
+ "CLI configuration (profiles, server, token) plus tenant configuration\n(theme & SolarForm / SolarForm Advanced) served by the security and solar backends."
1093
+ );
394
1094
  cfg.command("set <key> <value>").description("Set a configuration value. Keys: server, token, activeProfile").option("--profile <name>", "Profile to modify").action((key, value, opts) => {
395
1095
  try {
396
1096
  if (key === "activeProfile") {
@@ -458,11 +1158,13 @@ function registerConfigCommands(program2) {
458
1158
  outputError(err);
459
1159
  }
460
1160
  });
1161
+ registerThemeCommands(cfg);
1162
+ registerSolarformConfigCommands(cfg);
461
1163
  }
462
1164
 
463
1165
  // src/commands/inventory/factory.ts
464
1166
  import { Command } from "commander";
465
- function getGlobalOpts2(cmd) {
1167
+ function getGlobalOpts3(cmd) {
466
1168
  let root = cmd;
467
1169
  while (root.parent) root = root.parent;
468
1170
  return root.opts();
@@ -470,8 +1172,8 @@ function getGlobalOpts2(cmd) {
470
1172
  function parseData(data) {
471
1173
  if (!data) return void 0;
472
1174
  if (data === "-") {
473
- const { readFileSync: readFileSync5 } = __require("fs");
474
- const input = readFileSync5(0, "utf-8");
1175
+ const { readFileSync: readFileSync6 } = __require("fs");
1176
+ const input = readFileSync6(0, "utf-8");
475
1177
  return JSON.parse(input);
476
1178
  }
477
1179
  return JSON.parse(data);
@@ -481,7 +1183,7 @@ function createResourceCommands(cfg) {
481
1183
  const service = cfg.service || "solar";
482
1184
  cmd.command("list").description(`List ${cfg.singular}s with pagination. Fields: ${cfg.listFields.join(", ")}`).option("--limit <n>", "Max results", "20").option("--offset <n>", "Skip results", "0").option("--active-only", "Only active items (exclude inactive)").action(async (opts) => {
483
1185
  try {
484
- const global = getGlobalOpts2(cmd);
1186
+ const global = getGlobalOpts3(cmd);
485
1187
  const client = createServiceClient(service, global);
486
1188
  const params = {
487
1189
  limit: parseInt(opts.limit),
@@ -516,7 +1218,7 @@ function createResourceCommands(cfg) {
516
1218
  });
517
1219
  cmd.command("get <id>").description(`Get a ${cfg.singular} by ID. All fields returned by default.`).action(async (id) => {
518
1220
  try {
519
- const global = getGlobalOpts2(cmd);
1221
+ const global = getGlobalOpts3(cmd);
520
1222
  const client = createServiceClient(service, global);
521
1223
  if (cfg.getViaFilter) {
522
1224
  const res = await client.get(cfg.basePath, { params: { unactive: true } });
@@ -539,7 +1241,7 @@ function createResourceCommands(cfg) {
539
1241
  });
540
1242
  cmd.command("create").description(`Create a new ${cfg.singular}. Pass JSON via --data or stdin (--data -)`).requiredOption("--data <json>", "JSON data (or - for stdin)").action(async (opts) => {
541
1243
  try {
542
- const global = getGlobalOpts2(cmd);
1244
+ const global = getGlobalOpts3(cmd);
543
1245
  const client = createServiceClient(service, global);
544
1246
  const body = parseData(opts.data);
545
1247
  const res = await client.post(cfg.basePath, body);
@@ -550,7 +1252,7 @@ function createResourceCommands(cfg) {
550
1252
  });
551
1253
  cmd.command("update <id>").description(`Update a ${cfg.singular}`).requiredOption("--data <json>", "JSON data with fields to update (or - for stdin)").action(async (id, opts) => {
552
1254
  try {
553
- const global = getGlobalOpts2(cmd);
1255
+ const global = getGlobalOpts3(cmd);
554
1256
  const client = createServiceClient(service, global);
555
1257
  const body = parseData(opts.data);
556
1258
  if (cfg.putBodyOnly) {
@@ -566,7 +1268,7 @@ function createResourceCommands(cfg) {
566
1268
  });
567
1269
  cmd.command("delete <id>").description(`Delete a ${cfg.singular}`).action(async (id) => {
568
1270
  try {
569
- const global = getGlobalOpts2(cmd);
1271
+ const global = getGlobalOpts3(cmd);
570
1272
  const client = createServiceClient(service, global);
571
1273
  const res = await client.delete(`${cfg.basePath}/${id}`);
572
1274
  output(res.data ?? { success: true, deleted: id }, global);
@@ -577,7 +1279,7 @@ function createResourceCommands(cfg) {
577
1279
  if (cfg.batchDeletePath) {
578
1280
  cmd.command("delete-batch").description(`Batch delete ${cfg.singular}s`).requiredOption("--ids <ids>", "Comma-separated IDs").action(async (opts) => {
579
1281
  try {
580
- const global = getGlobalOpts2(cmd);
1282
+ const global = getGlobalOpts3(cmd);
581
1283
  const client = createServiceClient(service, global);
582
1284
  const ids = opts.ids.split(",").map((s) => s.trim());
583
1285
  const res = await client.post(`${cfg.basePath}/${cfg.batchDeletePath}`, ids);
@@ -589,7 +1291,7 @@ function createResourceCommands(cfg) {
589
1291
  }
590
1292
  cmd.command("filter").description(`Advanced filter for ${cfg.singular}s. Pass filter query as JSON.`).requiredOption("--query <json>", "Filter query JSON (or - for stdin)").option("--limit <n>", "Max results", "20").option("--offset <n>", "Skip results", "0").action(async (opts) => {
591
1293
  try {
592
- const global = getGlobalOpts2(cmd);
1294
+ const global = getGlobalOpts3(cmd);
593
1295
  const client = createServiceClient(service, global);
594
1296
  const query = parseData(opts.query);
595
1297
  const filterUrl = cfg.filterPath || `${cfg.basePath}/filter`;
@@ -609,15 +1311,15 @@ function createResourceCommands(cfg) {
609
1311
  }
610
1312
 
611
1313
  // src/commands/inventory/kits.ts
612
- function getGlobalOpts3(cmd) {
1314
+ function getGlobalOpts4(cmd) {
613
1315
  let root = cmd;
614
1316
  while (root.parent) root = root.parent;
615
1317
  return root.opts();
616
1318
  }
617
1319
  function parseData2(data) {
618
1320
  if (data === "-") {
619
- const { readFileSync: readFileSync5 } = __require("fs");
620
- return JSON.parse(readFileSync5(0, "utf-8"));
1321
+ const { readFileSync: readFileSync6 } = __require("fs");
1322
+ return JSON.parse(readFileSync6(0, "utf-8"));
621
1323
  }
622
1324
  return JSON.parse(data);
623
1325
  }
@@ -636,7 +1338,7 @@ function registerKitsCommands(inventory) {
636
1338
  if (kitsCreateIdx !== -1) kits.commands.splice(kitsCreateIdx, 1);
637
1339
  kits.command("archive <kitId>").description("Archive a solar kit (soft-disable)").action(async (kitId) => {
638
1340
  try {
639
- const global = getGlobalOpts3(kits);
1341
+ const global = getGlobalOpts4(kits);
640
1342
  const client = createServiceClient("solar", global);
641
1343
  const res = await client.put(`/solar-kits/archive/${kitId}`);
642
1344
  output(res.data ?? { success: true, archived: kitId }, global);
@@ -647,7 +1349,7 @@ function registerKitsCommands(inventory) {
647
1349
  const kitPanels = kits.command("panels").description("Manage kit solar panels");
648
1350
  kitPanels.command("list").description("List all kit solar panels. Fields: idKitSolarPanel, name, manufacturer, peakPower, efficiency, costPerUnit").option("--limit <n>", "Max results", "50").option("--offset <n>", "Skip results", "0").action(async (opts) => {
649
1351
  try {
650
- const global = getGlobalOpts3(kits);
1352
+ const global = getGlobalOpts4(kits);
651
1353
  const client = createServiceClient("solar", global);
652
1354
  const res = await client.get("/solar-kits/solar-panels", { params: { limit: opts.limit, offset: opts.offset } });
653
1355
  output(res.data, global);
@@ -657,7 +1359,7 @@ function registerKitsCommands(inventory) {
657
1359
  });
658
1360
  kitPanels.command("get <id>").description("Get a kit solar panel by ID").action(async (id) => {
659
1361
  try {
660
- const global = getGlobalOpts3(kits);
1362
+ const global = getGlobalOpts4(kits);
661
1363
  const client = createServiceClient("solar", global);
662
1364
  const res = await client.post("/solar-kits/solar-panels/filter", { idKitSolarPanel: id });
663
1365
  const data = Array.isArray(res.data) ? res.data[0] : res.data?.data?.[0] || res.data;
@@ -668,7 +1370,7 @@ function registerKitsCommands(inventory) {
668
1370
  });
669
1371
  kitPanels.command("create").description("Create a kit solar panel").requiredOption("--data <json>", "JSON data").action(async (opts) => {
670
1372
  try {
671
- const global = getGlobalOpts3(kits);
1373
+ const global = getGlobalOpts4(kits);
672
1374
  const client = createServiceClient("solar", global);
673
1375
  const res = await client.post("/solar-kits/solar-panels", parseData2(opts.data));
674
1376
  output(res.data, global);
@@ -678,7 +1380,7 @@ function registerKitsCommands(inventory) {
678
1380
  });
679
1381
  kitPanels.command("update <id>").description("Update a kit solar panel").requiredOption("--data <json>", "JSON data").action(async (id, opts) => {
680
1382
  try {
681
- const global = getGlobalOpts3(kits);
1383
+ const global = getGlobalOpts4(kits);
682
1384
  const client = createServiceClient("solar", global);
683
1385
  const res = await client.put(`/solar-kits/solar-panels/${id}`, parseData2(opts.data));
684
1386
  output(res.data, global);
@@ -688,7 +1390,7 @@ function registerKitsCommands(inventory) {
688
1390
  });
689
1391
  kitPanels.command("delete <id>").description("Delete a kit solar panel").action(async (id) => {
690
1392
  try {
691
- const global = getGlobalOpts3(kits);
1393
+ const global = getGlobalOpts4(kits);
692
1394
  const client = createServiceClient("solar", global);
693
1395
  const res = await client.delete(`/solar-kits/solar-panels/${id}`);
694
1396
  output(res.data ?? { success: true, deleted: id }, global);
@@ -698,7 +1400,7 @@ function registerKitsCommands(inventory) {
698
1400
  });
699
1401
  kitPanels.command("featured <kitPanelId>").description("List solar kits that feature this kit panel").action(async (kitPanelId) => {
700
1402
  try {
701
- const global = getGlobalOpts3(kits);
1403
+ const global = getGlobalOpts4(kits);
702
1404
  const client = createServiceClient("solar", global);
703
1405
  const res = await client.get(`/solar-kits/solar-panels/findFeaturedSolarKits/${kitPanelId}`);
704
1406
  output(res.data, global);
@@ -709,7 +1411,7 @@ function registerKitsCommands(inventory) {
709
1411
  const kitInverters = kits.command("inverters").description("Manage kit inverters");
710
1412
  kitInverters.command("list").description("List all kit inverters. Fields: idKitInverter, name, manufacturer, nominalPower, efficiency, costPerUnit").option("--limit <n>", "Max results", "50").option("--offset <n>", "Skip results", "0").action(async (opts) => {
711
1413
  try {
712
- const global = getGlobalOpts3(kits);
1414
+ const global = getGlobalOpts4(kits);
713
1415
  const client = createServiceClient("solar", global);
714
1416
  const res = await client.get("/solar-kits/inverters", { params: { limit: opts.limit, offset: opts.offset } });
715
1417
  output(res.data, global);
@@ -719,7 +1421,7 @@ function registerKitsCommands(inventory) {
719
1421
  });
720
1422
  kitInverters.command("create").description("Create a kit inverter").requiredOption("--data <json>", "JSON data").action(async (opts) => {
721
1423
  try {
722
- const global = getGlobalOpts3(kits);
1424
+ const global = getGlobalOpts4(kits);
723
1425
  const client = createServiceClient("solar", global);
724
1426
  const res = await client.post("/solar-kits/inverters", parseData2(opts.data));
725
1427
  output(res.data, global);
@@ -729,7 +1431,7 @@ function registerKitsCommands(inventory) {
729
1431
  });
730
1432
  kitInverters.command("update <id>").description("Update a kit inverter").requiredOption("--data <json>", "JSON data").action(async (id, opts) => {
731
1433
  try {
732
- const global = getGlobalOpts3(kits);
1434
+ const global = getGlobalOpts4(kits);
733
1435
  const client = createServiceClient("solar", global);
734
1436
  const res = await client.put(`/solar-kits/inverters/${id}`, parseData2(opts.data));
735
1437
  output(res.data, global);
@@ -739,7 +1441,7 @@ function registerKitsCommands(inventory) {
739
1441
  });
740
1442
  kitInverters.command("delete <id>").description("Delete a kit inverter").action(async (id) => {
741
1443
  try {
742
- const global = getGlobalOpts3(kits);
1444
+ const global = getGlobalOpts4(kits);
743
1445
  const client = createServiceClient("solar", global);
744
1446
  const res = await client.delete(`/solar-kits/inverters/${id}`);
745
1447
  output(res.data ?? { success: true, deleted: id }, global);
@@ -749,7 +1451,7 @@ function registerKitsCommands(inventory) {
749
1451
  });
750
1452
  kitInverters.command("featured <kitInverterId>").description("List solar kits that feature this kit inverter").action(async (kitInverterId) => {
751
1453
  try {
752
- const global = getGlobalOpts3(kits);
1454
+ const global = getGlobalOpts4(kits);
753
1455
  const client = createServiceClient("solar", global);
754
1456
  const res = await client.get(`/solar-kits/inverters/findFeaturedSolarKits/${kitInverterId}`);
755
1457
  output(res.data, global);
@@ -760,7 +1462,7 @@ function registerKitsCommands(inventory) {
760
1462
  const kitBatteries = kits.command("batteries").description("Manage kit batteries");
761
1463
  kitBatteries.command("list").description("List all kit batteries. Fields: idKitBattery, name, manufacturer, capacity, costPerUnit").option("--limit <n>", "Max results", "50").option("--offset <n>", "Skip results", "0").action(async (opts) => {
762
1464
  try {
763
- const global = getGlobalOpts3(kits);
1465
+ const global = getGlobalOpts4(kits);
764
1466
  const client = createServiceClient("solar", global);
765
1467
  const res = await client.get("/solar-kits/batteries", { params: { limit: opts.limit, offset: opts.offset } });
766
1468
  output(res.data, global);
@@ -770,7 +1472,7 @@ function registerKitsCommands(inventory) {
770
1472
  });
771
1473
  kitBatteries.command("create").description("Create a kit battery").requiredOption("--data <json>", "JSON data").action(async (opts) => {
772
1474
  try {
773
- const global = getGlobalOpts3(kits);
1475
+ const global = getGlobalOpts4(kits);
774
1476
  const client = createServiceClient("solar", global);
775
1477
  const res = await client.post("/solar-kits/batteries", parseData2(opts.data));
776
1478
  output(res.data, global);
@@ -780,7 +1482,7 @@ function registerKitsCommands(inventory) {
780
1482
  });
781
1483
  kitBatteries.command("delete <id>").description("Delete a kit battery").action(async (id) => {
782
1484
  try {
783
- const global = getGlobalOpts3(kits);
1485
+ const global = getGlobalOpts4(kits);
784
1486
  const client = createServiceClient("solar", global);
785
1487
  const res = await client.delete(`/solar-kits/batteries/${id}`);
786
1488
  output(res.data ?? { success: true, deleted: id }, global);
@@ -792,7 +1494,7 @@ function registerKitsCommands(inventory) {
792
1494
  'Assemble a solar kit from existing components by ID.\nReferences kit panels, inverters, batteries, and custom assets by their IDs.\n\nCustom assets format: --custom-asset <assetId>:<units> (repeatable)\n\nExamples:\n suntropy inventory kits assemble --name "Kit 5kW" --panel 123 --inverter 456 --panels-count 12 --price 6500\n suntropy inventory kits assemble --name "Kit Premium" --panel 123 --inverter 456 --battery 789 \\\n --panels-count 12 --inverters-count 1 --batteries-count 1 --peak-power 5.4 --price 8500 \\\n --custom-asset 100:12 --custom-asset 200:1 --phase single_phase'
793
1495
  ).requiredOption("--name <identifier>", "Kit name/identifier").option("--panel <kitPanelId>", "Kit panel ID (idKitSolarPanel)").option("--inverter <kitInverterId>", "Kit inverter ID (idKitInverter)").option("--battery <batteryId>", "Battery ID from inventory (batteryId)").option("--panels-count <n>", "Number of panels", "12").option("--inverters-count <n>", "Number of inverters", "1").option("--batteries-count <n>", "Number of batteries", "0").option("--peak-power <kW>", "Total peak power in kW").requiredOption("--price <eur>", "Kit price in EUR (required)").option("--phase <type>", "Phase: single_phase or three_phase", "single_phase").option("--coplanar", "Coplanar mounting").option("--taxes <pct>", "Default tax percentage", "21").option("--custom-asset <id:units>", "Custom asset as id:units (repeatable)", collectCustomAssets, []).action(async (opts) => {
794
1496
  try {
795
- const global = getGlobalOpts3(kits);
1497
+ const global = getGlobalOpts4(kits);
796
1498
  const client = createServiceClient("solar", global);
797
1499
  const validationErrors = [];
798
1500
  if (opts.panel) {
@@ -893,7 +1595,7 @@ function collectCustomAssets(value, previous) {
893
1595
  }
894
1596
 
895
1597
  // src/commands/inventory/manufacturers.ts
896
- function getGlobalOpts4(cmd) {
1598
+ function getGlobalOpts5(cmd) {
897
1599
  let root = cmd;
898
1600
  while (root.parent) root = root.parent;
899
1601
  return root.opts();
@@ -902,7 +1604,7 @@ function registerManufacturersCommands(inventory) {
902
1604
  const mfr = inventory.command("manufacturers").description("Manage manufacturers (referenced by all inventory devices)");
903
1605
  mfr.command("list").description("List all manufacturers. Fields: idManufacturer, name, imageUrl").action(async () => {
904
1606
  try {
905
- const global = getGlobalOpts4(mfr);
1607
+ const global = getGlobalOpts5(mfr);
906
1608
  const client = createServiceClient("solar", global);
907
1609
  const res = await client.get("/manufacturers");
908
1610
  output(res.data, global);
@@ -912,7 +1614,7 @@ function registerManufacturersCommands(inventory) {
912
1614
  });
913
1615
  mfr.command("create").description("Create a new manufacturer").requiredOption("--data <json>", 'JSON: { "name": "Manufacturer Name", "imageUrl": "..." }').action(async (opts) => {
914
1616
  try {
915
- const global = getGlobalOpts4(mfr);
1617
+ const global = getGlobalOpts5(mfr);
916
1618
  const client = createServiceClient("solar", global);
917
1619
  const body = JSON.parse(opts.data);
918
1620
  const res = await client.post("/manufacturers", body);
@@ -924,7 +1626,7 @@ function registerManufacturersCommands(inventory) {
924
1626
  }
925
1627
 
926
1628
  // src/commands/inventory/custom-fields.ts
927
- function getGlobalOpts5(cmd) {
1629
+ function getGlobalOpts6(cmd) {
928
1630
  let root = cmd;
929
1631
  while (root.parent) root = root.parent;
930
1632
  return root.opts();
@@ -939,7 +1641,7 @@ function registerCustomFieldsCommands(inventory) {
939
1641
  );
940
1642
  fields.command("list").description("List all custom fields").action(async () => {
941
1643
  try {
942
- const global = getGlobalOpts5(fields);
1644
+ const global = getGlobalOpts6(fields);
943
1645
  const client = createServiceClient("solar", global);
944
1646
  const res = await client.get("/custom-asset/custom-field/all");
945
1647
  output(res.data, global);
@@ -949,7 +1651,7 @@ function registerCustomFieldsCommands(inventory) {
949
1651
  });
950
1652
  fields.command("get <id>").description("Get a custom field by ID").action(async (id) => {
951
1653
  try {
952
- const global = getGlobalOpts5(fields);
1654
+ const global = getGlobalOpts6(fields);
953
1655
  const client = createServiceClient("solar", global);
954
1656
  const res = await client.get(`/custom-asset/custom-field/id/${id}`);
955
1657
  output(res.data, global);
@@ -967,7 +1669,7 @@ Example:
967
1669
  suntropy inventory custom-fields create --data '{"label":"Size","type":"options","customAssetTypeId":1,"customFieldOptions":[{"label":"S","value":"s"},{"label":"M","value":"m"},{"label":"L","value":"l"}]}'`
968
1670
  ).requiredOption("--data <json>", "Field definition as JSON").action(async (opts) => {
969
1671
  try {
970
- const global = getGlobalOpts5(fields);
1672
+ const global = getGlobalOpts6(fields);
971
1673
  const client = createServiceClient("solar", global);
972
1674
  const body = parseData3(opts.data);
973
1675
  const res = await client.post("/custom-asset/custom-field", body);
@@ -978,7 +1680,7 @@ Example:
978
1680
  });
979
1681
  fields.command("update <id>").description("Update a custom field").requiredOption("--data <json>", "Updated field data as JSON").action(async (id, opts) => {
980
1682
  try {
981
- const global = getGlobalOpts5(fields);
1683
+ const global = getGlobalOpts6(fields);
982
1684
  const client = createServiceClient("solar", global);
983
1685
  const body = parseData3(opts.data);
984
1686
  const res = await client.put(`/custom-asset/custom-field/${id}`, body);
@@ -989,7 +1691,7 @@ Example:
989
1691
  });
990
1692
  fields.command("delete <id>").description("Delete a custom field").action(async (id) => {
991
1693
  try {
992
- const global = getGlobalOpts5(fields);
1694
+ const global = getGlobalOpts6(fields);
993
1695
  const client = createServiceClient("solar", global);
994
1696
  const res = await client.delete(`/custom-asset/custom-field/${id}`);
995
1697
  output(res.data ?? { success: true, deleted: id }, global);
@@ -1092,7 +1794,7 @@ function registerInventoryCommands(program2) {
1092
1794
  }
1093
1795
 
1094
1796
  // src/commands/studies/builder.ts
1095
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync2 } from "fs";
1797
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync2 } from "fs";
1096
1798
 
1097
1799
  // node_modules/uuid/dist/esm-node/rng.js
1098
1800
  import crypto from "crypto";
@@ -1345,10 +2047,10 @@ function readStudy(filePath) {
1345
2047
  if (!existsSync2(filePath)) {
1346
2048
  throw new Error(`Study file not found: ${filePath}. Use 'studies init --file ${filePath}' to create one.`);
1347
2049
  }
1348
- return JSON.parse(readFileSync2(filePath, "utf-8"));
2050
+ return JSON.parse(readFileSync3(filePath, "utf-8"));
1349
2051
  }
1350
2052
  function writeStudy(filePath, study) {
1351
- writeFileSync3(filePath, JSON.stringify(study, null, 2), "utf-8");
2053
+ writeFileSync4(filePath, JSON.stringify(study, null, 2), "utf-8");
1352
2054
  }
1353
2055
  function updateStudy(filePath, updater) {
1354
2056
  const study = readStudy(filePath);
@@ -1381,7 +2083,7 @@ function deepMerge(target, source) {
1381
2083
  }
1382
2084
  }
1383
2085
  }
1384
- function getGlobalOpts6(cmd) {
2086
+ function getGlobalOpts7(cmd) {
1385
2087
  let root = cmd;
1386
2088
  while (root.parent) root = root.parent;
1387
2089
  return root.opts();
@@ -1406,7 +2108,7 @@ function registerStudyBuilderCommands(studies) {
1406
2108
  stepsProgress,
1407
2109
  completionPercentage: 0,
1408
2110
  missing
1409
- }, getGlobalOpts6(studies));
2111
+ }, getGlobalOpts7(studies));
1410
2112
  } catch (err) {
1411
2113
  outputError(err instanceof Error ? err : new Error(String(err)));
1412
2114
  }
@@ -1422,7 +2124,7 @@ function registerStudyBuilderCommands(studies) {
1422
2124
  stepsProgress,
1423
2125
  completionPercentage: calculateCompletionPercentage(stepsProgress),
1424
2126
  missing
1425
- }, getGlobalOpts6(studies));
2127
+ }, getGlobalOpts7(studies));
1426
2128
  } catch (err) {
1427
2129
  outputError(err instanceof Error ? err : new Error(String(err)));
1428
2130
  }
@@ -1432,7 +2134,7 @@ function registerStudyBuilderCommands(studies) {
1432
2134
  ).option("--file <path>", "Study file path").option("--state-id <n>", "Solar study metadata state ID").option("--credit-amount <n>", "Credit amount to consume").option("--save-as-new", "Force save as new study (even if _id exists)").action(async (opts) => {
1433
2135
  try {
1434
2136
  const filePath = resolveFile(opts);
1435
- const global = getGlobalOpts6(studies);
2137
+ const global = getGlobalOpts7(studies);
1436
2138
  const study = readStudy(filePath);
1437
2139
  const { stepsProgress } = evaluateSteps(study);
1438
2140
  const progress = study.solarStudyProgress;
@@ -1475,7 +2177,7 @@ function registerStudyBuilderCommands(studies) {
1475
2177
  ).option("--file <path>", "Output file path").action(async (studyId, opts) => {
1476
2178
  try {
1477
2179
  const filePath = resolveFile(opts);
1478
- const global = getGlobalOpts6(studies);
2180
+ const global = getGlobalOpts7(studies);
1479
2181
  const client = createServiceClient("solar", global);
1480
2182
  const res = await client.get(`/solar-study/findById/${studyId}`);
1481
2183
  const study = res.data;
@@ -1504,7 +2206,7 @@ function registerStudyBuilderCommands(studies) {
1504
2206
  study.name = opts.name;
1505
2207
  return void 0;
1506
2208
  });
1507
- output(result, getGlobalOpts6(studies));
2209
+ output(result, getGlobalOpts7(studies));
1508
2210
  } catch (err) {
1509
2211
  outputError(err instanceof Error ? err : new Error(String(err)));
1510
2212
  }
@@ -1515,7 +2217,7 @@ function registerStudyBuilderCommands(studies) {
1515
2217
  study.market = opts.market;
1516
2218
  return void 0;
1517
2219
  });
1518
- output(result, getGlobalOpts6(studies));
2220
+ output(result, getGlobalOpts7(studies));
1519
2221
  } catch (err) {
1520
2222
  outputError(err instanceof Error ? err : new Error(String(err)));
1521
2223
  }
@@ -1540,7 +2242,7 @@ function registerStudyBuilderCommands(studies) {
1540
2242
  study.clientDetails = cd;
1541
2243
  return void 0;
1542
2244
  });
1543
- output(result, getGlobalOpts6(studies));
2245
+ output(result, getGlobalOpts7(studies));
1544
2246
  } catch (err) {
1545
2247
  outputError(err instanceof Error ? err : new Error(String(err)));
1546
2248
  }
@@ -1549,7 +2251,7 @@ function registerStudyBuilderCommands(studies) {
1549
2251
  "Set ATR tariff and geographical zone. Auto-sets phase (>3 periods \u2192 three_phase).\nCommon tariff IDs (Spain): 13=2.0TD (3 periods), 14=3.0TD (6 periods)\nZone IDs (Spain): 1=Peninsula, 2=Canarias, 3=Baleares\nExample: suntropy studies set tariff --file study.json --tariff-id 13 --zone-id 1"
1550
2252
  ).option("--file <path>", "Study file path").requiredOption("--tariff-id <n>", "ATR tariff ID").option("--zone-id <n>", "Geographical zone ID", "1").option("--market <code>", "Market code for tariff lookup", "es").action(async (opts) => {
1551
2253
  try {
1552
- const global = getGlobalOpts6(studies);
2254
+ const global = getGlobalOpts7(studies);
1553
2255
  const client = createServiceClient("periods", global);
1554
2256
  const tariffRes = await client.get("/tarifas-atr", {
1555
2257
  params: { market: opts.market }
@@ -1613,7 +2315,7 @@ Examples:
1613
2315
  }
1614
2316
  return void 0;
1615
2317
  });
1616
- output(result, getGlobalOpts6(studies));
2318
+ output(result, getGlobalOpts7(studies));
1617
2319
  } catch (err) {
1618
2320
  outputError(err instanceof Error ? err : new Error(String(err)));
1619
2321
  }
@@ -1634,7 +2336,7 @@ Examples:
1634
2336
  suntropy studies set consumption --file study.json --monthly '{"1":350,"2":320,...}'`
1635
2337
  ).option("--file <path>", "Study file path").option("--from-file <curvePath>", "Load PowerCurve from JSON file").option("--annual <kWh>", "Annual consumption in kWh").option("--pattern <name>", "Pattern: Balance, Nightly, Morning, Afternoon, Domestic, Commercial").option("--by-period <json>", 'Period consumption JSON: {"p1":N,"p2":N,...}').option("--monthly <json>", 'Monthly consumption JSON: {"1":N,...,"12":N}').option("--monthly-by-period <json>", 'Monthly by period: {"1":{"p1":N,...},...}').option("--tariff <code>", "Tariff code for profile lookup (default: from study)").option("--market <code>", "Market code (default: from study)").action(async (opts) => {
1636
2338
  try {
1637
- const global = getGlobalOpts6(studies);
2339
+ const global = getGlobalOpts7(studies);
1638
2340
  const filePath = resolveFile(opts);
1639
2341
  const study = readStudy(filePath);
1640
2342
  const studyTariff = study.atrTariff?.nombre || "3.0TD";
@@ -1645,7 +2347,7 @@ Examples:
1645
2347
  let curveData;
1646
2348
  let introductionMode;
1647
2349
  if (opts.fromFile) {
1648
- curveData = JSON.parse(readFileSync2(opts.fromFile, "utf-8"));
2350
+ curveData = JSON.parse(readFileSync3(opts.fromFile, "utf-8"));
1649
2351
  introductionMode = "upload";
1650
2352
  } else if (opts.annual && opts.pattern) {
1651
2353
  const profilesClient = createServiceClient("profiles", global);
@@ -1714,7 +2416,7 @@ Examples:
1714
2416
  if (opts.power) surface.installedPower = parseFloat(opts.power);
1715
2417
  if (opts.panelsCount) surface.panelNumber = parseInt(opts.panelsCount);
1716
2418
  if (opts.production) {
1717
- surface.production = JSON.parse(readFileSync2(opts.production, "utf-8"));
2419
+ surface.production = JSON.parse(readFileSync3(opts.production, "utf-8"));
1718
2420
  }
1719
2421
  study.location = { lat: parseFloat(opts.lat), lng: parseFloat(opts.lon) };
1720
2422
  study.mapCenter = { lat: parseFloat(opts.lat), lng: parseFloat(opts.lon) };
@@ -1723,7 +2425,7 @@ Examples:
1723
2425
  study.surfaces = surfaces;
1724
2426
  return "surfaces";
1725
2427
  });
1726
- output(result, getGlobalOpts6(studies));
2428
+ output(result, getGlobalOpts7(studies));
1727
2429
  } catch (err) {
1728
2430
  outputError(err instanceof Error ? err : new Error(String(err)));
1729
2431
  }
@@ -1740,7 +2442,7 @@ Examples:
1740
2442
  study.surfaces = surfaces.length > 0 ? surfaces : void 0;
1741
2443
  return "surfaces";
1742
2444
  });
1743
- output(result, getGlobalOpts6(studies));
2445
+ output(result, getGlobalOpts7(studies));
1744
2446
  } catch (err) {
1745
2447
  outputError(err instanceof Error ? err : new Error(String(err)));
1746
2448
  }
@@ -1749,7 +2451,7 @@ Examples:
1749
2451
  'Set solar panel for the study. Auto-sets peakPowerIntroductionMode to "solarPanel".\nFetches full panel data from inventory.\nExample: suntropy studies set panel --file study.json --panel-id 456 --panels-count 12'
1750
2452
  ).option("--file <path>", "Study file path").requiredOption("--panel-id <n>", "Solar panel ID from inventory").option("--panels-count <n>", "Number of panels").action(async (opts) => {
1751
2453
  try {
1752
- const global = getGlobalOpts6(studies);
2454
+ const global = getGlobalOpts7(studies);
1753
2455
  const solarClient = createServiceClient("solar", global);
1754
2456
  const panelRes = await solarClient.get(`/solar-panels/${opts.panelId}`);
1755
2457
  const panel = panelRes.data;
@@ -1774,7 +2476,7 @@ Examples:
1774
2476
  'Set solar kit for the study. Auto-sets peakPowerIntroductionMode to "solarKit".\nFetches full kit data from inventory.\nExample: suntropy studies set kit --file study.json --kit-id 123'
1775
2477
  ).option("--file <path>", "Study file path").requiredOption("--kit-id <n>", "Solar kit ID from inventory").action(async (opts) => {
1776
2478
  try {
1777
- const global = getGlobalOpts6(studies);
2479
+ const global = getGlobalOpts7(studies);
1778
2480
  const solarClient = createServiceClient("solar", global);
1779
2481
  const kitsRes = await solarClient.get("/solar-kits", { params: { unactive: true } });
1780
2482
  const allKits = Array.isArray(kitsRes.data) && Array.isArray(kitsRes.data[0]) ? kitsRes.data[0] : Array.isArray(kitsRes.data) ? kitsRes.data : kitsRes.data?.data || [];
@@ -1800,7 +2502,7 @@ Examples:
1800
2502
  "Set inverter(s) for the study (when using solarPanel mode).\nExample: suntropy studies set inverter --file study.json --inverter-id 789"
1801
2503
  ).option("--file <path>", "Study file path").requiredOption("--inverter-id <ids>", "Inverter ID(s), comma-separated for multiple").action(async (opts) => {
1802
2504
  try {
1803
- const global = getGlobalOpts6(studies);
2505
+ const global = getGlobalOpts7(studies);
1804
2506
  const solarClient = createServiceClient("solar", global);
1805
2507
  const ids = opts.inverterId.split(",").map((s) => parseInt(s.trim()));
1806
2508
  const inverters = [];
@@ -1827,7 +2529,7 @@ Examples:
1827
2529
  study.instalationPhaseNumber = opts.phase;
1828
2530
  return void 0;
1829
2531
  });
1830
- output(result, getGlobalOpts6(studies));
2532
+ output(result, getGlobalOpts7(studies));
1831
2533
  } catch (err) {
1832
2534
  outputError(err instanceof Error ? err : new Error(String(err)));
1833
2535
  }
@@ -1853,7 +2555,7 @@ Examples:
1853
2555
  study.economicResults = er;
1854
2556
  return void 0;
1855
2557
  });
1856
- output(result, getGlobalOpts6(studies));
2558
+ output(result, getGlobalOpts7(studies));
1857
2559
  } catch (err) {
1858
2560
  outputError(err instanceof Error ? err : new Error(String(err)));
1859
2561
  }
@@ -1862,7 +2564,7 @@ Examples:
1862
2564
  "Set selected custom assets for the study.\nExample: suntropy studies set custom-assets --file study.json --asset 100:12 --asset 200:1"
1863
2565
  ).option("--file <path>", "Study file path").requiredOption("--asset <id:qty>", "Custom asset as id:quantity (repeatable)", collectAssets, []).action(async (opts) => {
1864
2566
  try {
1865
- const global = getGlobalOpts6(studies);
2567
+ const global = getGlobalOpts7(studies);
1866
2568
  const solarClient = createServiceClient("solar", global);
1867
2569
  const assets = [];
1868
2570
  for (const { id, quantity } of opts.asset) {
@@ -1893,7 +2595,7 @@ Examples:
1893
2595
  }
1894
2596
  return void 0;
1895
2597
  });
1896
- output(result, getGlobalOpts6(studies));
2598
+ output(result, getGlobalOpts7(studies));
1897
2599
  } catch (err) {
1898
2600
  outputError(err instanceof Error ? err : new Error(String(err)));
1899
2601
  }
@@ -1912,7 +2614,7 @@ Example: suntropy studies set data --file study.json --data '{"referenceId":"REF
1912
2614
  deepMerge(study, parsed);
1913
2615
  return cascadeField;
1914
2616
  });
1915
- output(result, getGlobalOpts6(studies));
2617
+ output(result, getGlobalOpts7(studies));
1916
2618
  } catch (err) {
1917
2619
  outputError(err instanceof Error ? err : new Error(String(err)));
1918
2620
  }
@@ -1921,7 +2623,7 @@ Example: suntropy studies set data --file study.json --data '{"referenceId":"REF
1921
2623
  "Calculate production for study surfaces using backend API.\nExample: suntropy studies calculate production --file study.json [--surface-index 0 | --all-surfaces]"
1922
2624
  ).option("--file <path>", "Study file path").option("--surface-index <n>", "Calculate for specific surface index").option("--all-surfaces", "Calculate for all surfaces").option("--losses <n>", "Losses percentage", "14").action(async (opts) => {
1923
2625
  try {
1924
- const global = getGlobalOpts6(studies);
2626
+ const global = getGlobalOpts7(studies);
1925
2627
  const filePath = resolveFile(opts);
1926
2628
  const study = readStudy(filePath);
1927
2629
  const surfaces = study.surfaces;
@@ -1968,7 +2670,7 @@ Example: suntropy studies set data --file study.json --data '{"referenceId":"REF
1968
2670
  "Calculate energy results replicating frontend SolarResultCalculator.\nComputes: net consumption, excesses, spending/savings by period, coverage.\nRequires: consumption, production, energyPrices, atrTariff.\nExample: suntropy studies calculate-results --file study.json"
1969
2671
  ).option("--file <path>", "Study file path").action(async (opts) => {
1970
2672
  try {
1971
- const global = getGlobalOpts6(studies);
2673
+ const global = getGlobalOpts7(studies);
1972
2674
  const filePath = resolveFile(opts);
1973
2675
  const study = readStudy(filePath);
1974
2676
  const consumption = study.consumption;
@@ -2120,7 +2822,7 @@ Example: suntropy studies set data --file study.json --data '{"referenceId":"REF
2120
2822
  "Optimize peak power based on consumption.\n\nSupports two modes:\n - Panel mode (default or when solarPanel is set): iterates peak power\n - Kit mode (when solarKit is set or --use-kits): evaluates available kits\n\nOptimization criteria (pick one):\n --energy-savings <pct> Target energy savings percentage\n --raw-consumption <pct> Production as percentage of consumption\n --max-excesses <pct> Max excesses as percentage of production\n --max-overproduction-months <n> Max months with overproduction\n\nSurface constraints:\n If surfaces have area + panel dimensions, max panels per surface is calculated.\n Without area, no surface constraint is applied (unlimited space).\n\nExamples:\n suntropy studies optimize-peakpower --file study.json --energy-savings 70\n suntropy studies optimize-peakpower --file study.json --raw-consumption 100 --use-kits\n suntropy studies optimize-peakpower --file study.json --max-excesses 15"
2121
2823
  ).option("--file <path>", "Study file path").option("--energy-savings <pct>", "Target energy savings %").option("--raw-consumption <pct>", "Target production as % of consumption").option("--max-excesses <pct>", "Max excesses as % of production").option("--max-overproduction-months <n>", "Max months with overproduction").option("--use-kits", "Force kit mode (fetch all active kits and select optimal)").option("--apply", "Apply the result to the study file (set peakpower/kit, recalculate production)").action(async (opts) => {
2122
2824
  try {
2123
- const global = getGlobalOpts6(studies);
2825
+ const global = getGlobalOpts7(studies);
2124
2826
  const filePath = resolveFile(opts);
2125
2827
  const study = readStudy(filePath);
2126
2828
  const evaluationMode = {};
@@ -2432,7 +3134,7 @@ Example: suntropy studies set data --file study.json --data '{"referenceId":"REF
2432
3134
  study.comments = comments;
2433
3135
  return void 0;
2434
3136
  });
2435
- output(result, getGlobalOpts6(studies));
3137
+ output(result, getGlobalOpts7(studies));
2436
3138
  } catch (err) {
2437
3139
  outputError(err instanceof Error ? err : new Error(String(err)));
2438
3140
  }
@@ -2441,7 +3143,7 @@ Example: suntropy studies set data --file study.json --data '{"referenceId":"REF
2441
3143
  'Add a comment to an existing study via API.\nExample: suntropy studies comment abc123 --content "Revisado por agente"'
2442
3144
  ).requiredOption("--content <text>", "Comment text").action(async (studyId, opts) => {
2443
3145
  try {
2444
- const global = getGlobalOpts6(studies);
3146
+ const global = getGlobalOpts7(studies);
2445
3147
  const client = createServiceClient("solar", global);
2446
3148
  const comment = createComment("commented", opts.content);
2447
3149
  const res = await client.post(`/solar-study/addSolarStudyComment/${studyId}`, comment);
@@ -2582,7 +3284,7 @@ function createComment(type, content) {
2582
3284
  }
2583
3285
 
2584
3286
  // src/commands/studies/index.ts
2585
- function getGlobalOpts7(cmd) {
3287
+ function getGlobalOpts8(cmd) {
2586
3288
  let root = cmd;
2587
3289
  while (root.parent) root = root.parent;
2588
3290
  return root.opts();
@@ -2701,7 +3403,7 @@ function registerStudiesCommands(program2) {
2701
3403
  );
2702
3404
  studies.command("list").description("List solar studies metadata. Fields: idSolarStudyMetadata, solarStudyId, clientName, peakPower, currentState, creationTimestamp").option("--limit <n>", "Max results", "20").option("--offset <n>", "Skip results", "0").option("--state <state>", "Filter by state name").option("--client-name <name>", "Filter by client name").option("--from <date>", "Filter from date (YYYY-MM-DD)").option("--to <date>", "Filter to date (YYYY-MM-DD)").action(async (opts) => {
2703
3405
  try {
2704
- const global = getGlobalOpts7(studies);
3406
+ const global = getGlobalOpts8(studies);
2705
3407
  const client = createServiceClient("solar", global);
2706
3408
  const body = {
2707
3409
  limit: parseInt(opts.limit),
@@ -2735,7 +3437,7 @@ function registerStudiesCommands(program2) {
2735
3437
  });
2736
3438
  studies.command("metadata <id>").description("Get solar study metadata by metadata ID (relational). Full MySQL record with state, costs, versions.").option("--by-study-id", "Interpret <id> as MongoDB solarStudyId instead of metadata ID").action(async (id, opts) => {
2737
3439
  try {
2738
- const global = getGlobalOpts7(studies);
3440
+ const global = getGlobalOpts8(studies);
2739
3441
  const client = createServiceClient("solar", global);
2740
3442
  const path = opts.byStudyId ? `/solar-study/metadata/solar-study-id/${id}` : `/solar-study/findSolarStudyMetadataById/${id}`;
2741
3443
  const res = await client.get(path);
@@ -2748,7 +3450,7 @@ function registerStudiesCommands(program2) {
2748
3450
  "Get solar study by MongoDB ID. By default returns summary (no heavy curves).\nExpand sections: surfaces, results, economics, batteries, consumption, equipment, client, location\nExamples:\n suntropy studies get abc123\n suntropy studies get abc123 --expand surfaces,results\n suntropy studies get abc123 --expand all\n suntropy studies get abc123 --fields name,market (bypasses expand filter)"
2749
3451
  ).option("--expand <sections>", 'Comma-separated sections to expand (or "all")').action(async (studyId, opts) => {
2750
3452
  try {
2751
- const global = getGlobalOpts7(studies);
3453
+ const global = getGlobalOpts8(studies);
2752
3454
  const client = createServiceClient("solar", global);
2753
3455
  const res = await client.get(`/solar-study/findById/${studyId}`);
2754
3456
  const study = res.data;
@@ -2770,7 +3472,7 @@ function registerStudiesCommands(program2) {
2770
3472
  "Extract and analyze a PowerCurve from a study.\nCurve names: consumption, production, net-consumption, excesses\nDefault: --stats. Use --raw for full hourly data (8760 values).\nUse --monthly for monthly aggregates, --daily for daily totals."
2771
3473
  ).option("--stats", "Show statistics (default if no other flag)").option("--monthly", "Monthly accumulated values").option("--daily", "Daily accumulated values").option("--raw", "Full hourly DayCurve[] data").option("--total", "Just the total accumulated value").option("--surface-index <n>", "Surface index for production curve", "0").option("--save <file>", "Save curve data to file").action(async (studyId, curveName, opts) => {
2772
3474
  try {
2773
- const global = getGlobalOpts7(studies);
3475
+ const global = getGlobalOpts8(studies);
2774
3476
  const client = createServiceClient("solar", global);
2775
3477
  const res = await client.get(`/solar-study/findById/${studyId}`);
2776
3478
  const study = res.data;
@@ -2837,7 +3539,7 @@ function registerStudiesCommands(program2) {
2837
3539
  });
2838
3540
  studies.command("calculate-production").description("Calculate solar production curve for given coordinates and configuration").requiredOption("--lat <n>", "Latitude").requiredOption("--lon <n>", "Longitude").requiredOption("--power <w>", "Installed power in Watts").option("--angle <n>", "Panel inclination degrees", "30").option("--azimuth <n>", "Panel orientation degrees (0=north, 180=south)", "180").option("--losses <n>", "Losses percentage", "14").option("--year <n>", "Year for calculation", String((/* @__PURE__ */ new Date()).getFullYear())).option("--save <file>", "Save result to file").action(async (opts) => {
2839
3541
  try {
2840
- const global = getGlobalOpts7(studies);
3542
+ const global = getGlobalOpts8(studies);
2841
3543
  const client = createServiceClient("solar", global);
2842
3544
  const body = {
2843
3545
  lat: parseFloat(opts.lat),
@@ -2857,7 +3559,7 @@ function registerStudiesCommands(program2) {
2857
3559
  });
2858
3560
  studies.command("optimize-surfaces").description("Calculate optimal panel angle and azimuth for coordinates").requiredOption("--lat <n>", "Latitude").requiredOption("--lon <n>", "Longitude").action(async (opts) => {
2859
3561
  try {
2860
- const global = getGlobalOpts7(studies);
3562
+ const global = getGlobalOpts8(studies);
2861
3563
  const client = createServiceClient("solar", global);
2862
3564
  const res = await client.get("/solar-study/optimizeSurfaces", {
2863
3565
  params: { lat: parseFloat(opts.lat), lon: parseFloat(opts.lon) }
@@ -2871,8 +3573,8 @@ function registerStudiesCommands(program2) {
2871
3573
  }
2872
3574
 
2873
3575
  // src/commands/curves/index.ts
2874
- import { readFileSync as readFileSync3 } from "fs";
2875
- function getGlobalOpts8(cmd) {
3576
+ import { readFileSync as readFileSync4 } from "fs";
3577
+ function getGlobalOpts9(cmd) {
2876
3578
  let root = cmd;
2877
3579
  while (root.parent) root = root.parent;
2878
3580
  return root.opts();
@@ -2891,7 +3593,7 @@ function readStdin() {
2891
3593
  async function readCurveInput(inputPath) {
2892
3594
  let raw;
2893
3595
  if (inputPath && inputPath !== "-") {
2894
- raw = readFileSync3(inputPath, "utf-8");
3596
+ raw = readFileSync4(inputPath, "utf-8");
2895
3597
  } else {
2896
3598
  raw = await readStdin();
2897
3599
  }
@@ -2916,7 +3618,7 @@ function registerCurvesCommands(program2) {
2916
3618
  const data = await readCurveInput(opts.input);
2917
3619
  const pc = await buildCurve(data, "stats");
2918
3620
  const raw = pc.calculateStatistics();
2919
- output(raw.statistics || raw, getGlobalOpts8(curves));
3621
+ output(raw.statistics || raw, getGlobalOpts9(curves));
2920
3622
  } catch (err) {
2921
3623
  outputError(err);
2922
3624
  }
@@ -2925,7 +3627,7 @@ function registerCurvesCommands(program2) {
2925
3627
  try {
2926
3628
  const data = await readCurveInput(opts.input);
2927
3629
  const pc = await buildCurve(data, "total");
2928
- output({ total: pc.getTotalAcumulate(), days: pc.days.length }, getGlobalOpts8(curves));
3630
+ output({ total: pc.getTotalAcumulate(), days: pc.days.length }, getGlobalOpts9(curves));
2929
3631
  } catch (err) {
2930
3632
  outputError(err);
2931
3633
  }
@@ -2935,32 +3637,32 @@ function registerCurvesCommands(program2) {
2935
3637
  const data = await readCurveInput(opts.input);
2936
3638
  const pc = await buildCurve(data, "multiplied");
2937
3639
  const result = pc.applyMultiplier(parseFloat(factor));
2938
- output(serializeCurve(result), getGlobalOpts8(curves));
3640
+ output(serializeCurve(result), getGlobalOpts9(curves));
2939
3641
  } catch (err) {
2940
3642
  outputError(err);
2941
3643
  }
2942
3644
  });
2943
3645
  curves.command("aggregate").description("Sum two PowerCurves (A + B). Returns a new PowerCurve.").requiredOption("--a <file>", "First curve file").requiredOption("--b <file>", "Second curve file").action(async (opts) => {
2944
3646
  try {
2945
- const dataA = JSON.parse(readFileSync3(opts.a, "utf-8"));
2946
- const dataB = JSON.parse(readFileSync3(opts.b, "utf-8"));
3647
+ const dataA = JSON.parse(readFileSync4(opts.a, "utf-8"));
3648
+ const dataB = JSON.parse(readFileSync4(opts.b, "utf-8"));
2947
3649
  const pcA = await buildCurve(dataA, "a");
2948
3650
  const pcB = await buildCurve(dataB, "b");
2949
3651
  const result = pcA.aggregatePowerCurve(pcB);
2950
- output(serializeCurve(result), getGlobalOpts8(curves));
3652
+ output(serializeCurve(result), getGlobalOpts9(curves));
2951
3653
  } catch (err) {
2952
3654
  outputError(err);
2953
3655
  }
2954
3656
  });
2955
3657
  curves.command("subtract").description("Subtract two PowerCurves (A - B). Returns a new PowerCurve.").requiredOption("--a <file>", "First curve file (minuend)").requiredOption("--b <file>", "Second curve file (subtrahend)").action(async (opts) => {
2956
3658
  try {
2957
- const dataA = JSON.parse(readFileSync3(opts.a, "utf-8"));
2958
- const dataB = JSON.parse(readFileSync3(opts.b, "utf-8"));
3659
+ const dataA = JSON.parse(readFileSync4(opts.a, "utf-8"));
3660
+ const dataB = JSON.parse(readFileSync4(opts.b, "utf-8"));
2959
3661
  const pcA = await buildCurve(dataA, "a");
2960
3662
  const pcB = await buildCurve(dataB, "b");
2961
3663
  const negB = pcB.applyMultiplier(-1);
2962
3664
  const result = pcA.aggregatePowerCurve(negB);
2963
- output(serializeCurve(result), getGlobalOpts8(curves));
3665
+ output(serializeCurve(result), getGlobalOpts9(curves));
2964
3666
  } catch (err) {
2965
3667
  outputError(err);
2966
3668
  }
@@ -2970,7 +3672,7 @@ function registerCurvesCommands(program2) {
2970
3672
  const data = await readCurveInput(opts.input);
2971
3673
  const pc = await buildCurve(data, "positive");
2972
3674
  const result = pc.filterNegativeValues();
2973
- output(serializeCurve(result), getGlobalOpts8(curves));
3675
+ output(serializeCurve(result), getGlobalOpts9(curves));
2974
3676
  } catch (err) {
2975
3677
  outputError(err);
2976
3678
  }
@@ -2980,7 +3682,7 @@ function registerCurvesCommands(program2) {
2980
3682
  const data = await readCurveInput(opts.input);
2981
3683
  const pc = await buildCurve(data, "negative");
2982
3684
  const result = pc.filterPositiveValues();
2983
- output(serializeCurve(result), getGlobalOpts8(curves));
3685
+ output(serializeCurve(result), getGlobalOpts9(curves));
2984
3686
  } catch (err) {
2985
3687
  outputError(err);
2986
3688
  }
@@ -2990,7 +3692,7 @@ function registerCurvesCommands(program2) {
2990
3692
  const data = await readCurveInput(opts.input);
2991
3693
  const pc = await buildCurve(data, "sorted");
2992
3694
  const sortedDays = pc.sortByDate();
2993
- output({ days: sortedDays, identifier: pc.identifier, ignore0: pc.ignore0 }, getGlobalOpts8(curves));
3695
+ output({ days: sortedDays, identifier: pc.identifier, ignore0: pc.ignore0 }, getGlobalOpts9(curves));
2994
3696
  } catch (err) {
2995
3697
  outputError(err);
2996
3698
  }
@@ -3002,7 +3704,7 @@ function registerCurvesCommands(program2) {
3002
3704
  const start = opts.start ? new Date(opts.start) : void 0;
3003
3705
  const end = opts.end ? new Date(opts.end) : void 0;
3004
3706
  const filteredDays = pc.filterByDates(start, end);
3005
- output({ days: filteredDays, identifier: pc.identifier, ignore0: pc.ignore0 }, getGlobalOpts8(curves));
3707
+ output({ days: filteredDays, identifier: pc.identifier, ignore0: pc.ignore0 }, getGlobalOpts9(curves));
3006
3708
  } catch (err) {
3007
3709
  outputError(err);
3008
3710
  }
@@ -3012,7 +3714,7 @@ function registerCurvesCommands(program2) {
3012
3714
  const data = await readCurveInput(opts.input);
3013
3715
  const pc = await buildCurve(data, "serie");
3014
3716
  const serie = pc.convertoToSerie();
3015
- output(serie, getGlobalOpts8(curves));
3717
+ output(serie, getGlobalOpts9(curves));
3016
3718
  } catch (err) {
3017
3719
  outputError(err);
3018
3720
  }
@@ -3023,9 +3725,9 @@ function registerCurvesCommands(program2) {
3023
3725
  try {
3024
3726
  const data = await readCurveInput(opts.input);
3025
3727
  const pc = await buildCurve(data, "by-period");
3026
- const periodsData = JSON.parse(readFileSync3(opts.periods, "utf-8"));
3728
+ const periodsData = JSON.parse(readFileSync4(opts.periods, "utf-8"));
3027
3729
  const result = pc.aggregateByPeriod(periodsData);
3028
- output(result, getGlobalOpts8(curves));
3730
+ output(result, getGlobalOpts9(curves));
3029
3731
  } catch (err) {
3030
3732
  outputError(err);
3031
3733
  }
@@ -3033,8 +3735,8 @@ function registerCurvesCommands(program2) {
3033
3735
  }
3034
3736
 
3035
3737
  // src/commands/consumption/index.ts
3036
- import { readFileSync as readFileSync4 } from "fs";
3037
- function getGlobalOpts9(cmd) {
3738
+ import { readFileSync as readFileSync5 } from "fs";
3739
+ function getGlobalOpts10(cmd) {
3038
3740
  let root = cmd;
3039
3741
  while (root.parent) root = root.parent;
3040
3742
  return root.opts();
@@ -3053,7 +3755,7 @@ Examples:
3053
3755
  suntropy consumption estimate --annual 5000 --custom-profile-id abc123`
3054
3756
  ).requiredOption("--annual <kWh>", "Annual consumption in kWh").option("--pattern <name>", "Consumption pattern: Balance, Nightly, Morning, Afternoon, Domestic, Commercial").option("--start-date <YYYY-MM-DD>", "Start date (default: Jan 1 current year)").option("--end-date <YYYY-MM-DD>", "End date (default: Dec 31 current year)").option("--tariff <code>", "Electricity tariff code (e.g. 3.0TD)", "3.0TD").option("--type <type>", "Profile type: Initial or Final", "Final").option("--market <code>", "Market: es, pt, it", "es").option("--custom-profile-id <id>", "Use a custom consumption profile by ID").option("--monthly-data <json>", 'Monthly consumption JSON: {"1":val,"2":val,...,"12":val}').option("--daily-curve <json>", "Custom daily curve JSON (DayCurve format)").option("--save <file>", "Save result to file").action(async (opts) => {
3055
3757
  try {
3056
- const global = getGlobalOpts9(consumption);
3758
+ const global = getGlobalOpts10(consumption);
3057
3759
  const client = createServiceClient("profiles", global);
3058
3760
  const year = (/* @__PURE__ */ new Date()).getFullYear();
3059
3761
  const startDate = opts.startDate || `${year}-01-01`;
@@ -3085,7 +3787,7 @@ Examples:
3085
3787
  "Fetch REE (Red El\xE9ctrica) hourly profiles for a date range and tariff.\nReturns the raw profile data used as basis for consumption estimation.\nExample: suntropy consumption ree-profiles --start 2024-01-01 --end 2024-12-31 --tariff 3.0TD"
3086
3788
  ).requiredOption("--start <YYYY-MM-DD>", "Start date").requiredOption("--end <YYYY-MM-DD>", "End date").option("--tariff <code>", "Tariff code", "3.0TD").option("--type <type>", "Profile type: Initial or Final", "Final").option("--market <code>", "Market code: es, pt, it", "es").option("--save <file>", "Save result to file").action(async (opts) => {
3087
3789
  try {
3088
- const global = getGlobalOpts9(consumption);
3790
+ const global = getGlobalOpts10(consumption);
3089
3791
  const client = createServiceClient("profiles", global);
3090
3792
  const res = await client.get("/ree-profiles", {
3091
3793
  params: {
@@ -3103,7 +3805,7 @@ Examples:
3103
3805
  });
3104
3806
  consumption.command("custom-tags").description("List available custom consumption profile tags for the authenticated client").action(async () => {
3105
3807
  try {
3106
- const global = getGlobalOpts9(consumption);
3808
+ const global = getGlobalOpts10(consumption);
3107
3809
  const client = createServiceClient("profiles", global);
3108
3810
  const res = await client.get("/custom-profiles/getTags");
3109
3811
  output(res.data, global);
@@ -3113,7 +3815,7 @@ Examples:
3113
3815
  });
3114
3816
  consumption.command("custom-profile-info").description("Get details of a custom consumption profile by ID").requiredOption("--id <profileId>", "Custom profile ID").action(async (opts) => {
3115
3817
  try {
3116
- const global = getGlobalOpts9(consumption);
3818
+ const global = getGlobalOpts10(consumption);
3117
3819
  const client = createServiceClient("profiles", global);
3118
3820
  const res = await client.get("/custom-profiles/getInfo", {
3119
3821
  params: { id: opts.id }
@@ -3127,7 +3829,7 @@ Examples:
3127
3829
  "Fetch period distribution (hour\u2192P1-P6 mapping) from the periods service.\nReturns DayCurve[] where each hour's value is the period number (1-6).\nUse with `suntropy curves by-period` to aggregate any curve by tariff period.\n\nCommon tariff IDs (Spain): 13=2.0TD, 14=3.0TD\nZone IDs (Spain): 1=Peninsula, 2=Canarias, 3=Baleares\n\nExamples:\n suntropy consumption periods --tariff-id 14 --zone-id 1 --save /tmp/periods.json\n suntropy consumption periods --tariff-id 13 --start 2025-01-01 --end 2025-12-31"
3128
3830
  ).option("--tariff-id <n>", "ATR tariff ID (13=2.0TD, 14=3.0TD)", "14").option("--zone-id <n>", "Geographical zone ID (1=Peninsula)", "1").option("--start <YYYY-MM-DD>", "Start date (default: Jan 1 current year)").option("--end <YYYY-MM-DD>", "End date (default: Dec 31 current year)").option("--market <code>", "Market: es, pt, it, fr", "es").option("--save <file>", "Save result to file").action(async (opts) => {
3129
3831
  try {
3130
- const global = getGlobalOpts9(consumption);
3832
+ const global = getGlobalOpts10(consumption);
3131
3833
  const client = createServiceClient("periods", global);
3132
3834
  const year = (/* @__PURE__ */ new Date()).getFullYear();
3133
3835
  const res = await client.get("/periodos", {
@@ -3148,12 +3850,12 @@ Examples:
3148
3850
  "Generate consumption curve from an uploaded file.\nSupported formats: Portuguese EREDES ZIP files.\nExample: suntropy consumption from-file --eredes-zip /path/to/file.zip"
3149
3851
  ).option("--eredes-zip <path>", "Path to Portuguese EREDES ZIP file").option("--save <file>", "Save result to file").action(async (opts) => {
3150
3852
  try {
3151
- const global = getGlobalOpts9(consumption);
3853
+ const global = getGlobalOpts10(consumption);
3152
3854
  const client = createServiceClient("profiles", global);
3153
3855
  if (opts.eredesZip) {
3154
3856
  const FormData = (await import("form-data")).default;
3155
3857
  const form = new FormData();
3156
- form.append("file", readFileSync4(opts.eredesZip), {
3858
+ form.append("file", readFileSync5(opts.eredesZip), {
3157
3859
  filename: opts.eredesZip.split("/").pop(),
3158
3860
  contentType: "application/zip"
3159
3861
  });
@@ -3171,7 +3873,7 @@ Examples:
3171
3873
  }
3172
3874
 
3173
3875
  // src/commands/solarform/index.ts
3174
- function getGlobalOpts10(cmd) {
3876
+ function getGlobalOpts11(cmd) {
3175
3877
  let root = cmd;
3176
3878
  while (root.parent) root = root.parent;
3177
3879
  return root.opts();
@@ -3237,7 +3939,7 @@ function registerSolarformCommands(program2) {
3237
3939
  'Create a solar study with minimal parameters (simplified mode).\nAutomatically resolves location from region/subregion, applies consumption patterns,\nand optimizes kit selection.\n\nConsumption patterns: Balance, Nightly, Morning, Afternoon, Domestic, Commercial\nConsumption mode: monthlyConsumption (kWh) or monthlySpending (EUR)\nExcesses modes: PPA, gridSelling, noInjection, virtualBattery\n\nExamples:\n suntropy solarform simple --region "Andaluc\xEDa" --sub-region "Sevilla" --consumption 5000\n suntropy solarform simple --region "Catalu\xF1a" --sub-region "Barcelona" --consumption 300 --consumption-mode monthlySpending --save\n suntropy solarform simple --region "Madrid" --sub-region "Madrid" --consumption 8000 --pattern Domestic --kit-id abc123 --save'
3238
3940
  ).requiredOption("--region <name>", "Region name (must exist in database)").requiredOption("--sub-region <name>", "Sub-region name").requiredOption("--consumption <value>", "Consumption value (kWh or EUR depending on --consumption-mode)").option("--pattern <name>", "Consumption pattern: Balance, Nightly, Morning, Afternoon, Domestic, Commercial", "Balance").option("--consumption-mode <mode>", "monthlyConsumption (kWh) or monthlySpending (EUR)", "monthlyConsumption").option("--kit-id <id>", "Use a specific solar kit instead of auto-optimization").option("--excesses-mode <mode>", "Excesses compensation: PPA, gridSelling, noInjection, virtualBattery").option("--assigned-user <uid>", "Assign study to a user UID").option("--email <email>", "Send results to this email").option("--save", "Save study to database").option("--raw", "Return full study with PowerCurve data (no compaction)").option("--save-file <file>", "Save result to local file").action(async (opts) => {
3239
3941
  try {
3240
- const global = getGlobalOpts10(solarform);
3942
+ const global = getGlobalOpts11(solarform);
3241
3943
  const client = createServiceClient("solar", global);
3242
3944
  const body = {
3243
3945
  region: opts.region,
@@ -3281,7 +3983,7 @@ Examples:
3281
3983
  cat study-input.json | suntropy solarform calculate --data - --save --email client@co.com`
3282
3984
  ).requiredOption("--data <json>", "Full SimplifiedSolarStudy JSON body (or - for stdin)").option("--save", "Save study to database").option("--email <email>", "Send results to this email").option("--raw", "Return full study with PowerCurve data (no compaction)").option("--save-file <file>", "Save result to local file").action(async (opts) => {
3283
3985
  try {
3284
- const global = getGlobalOpts10(solarform);
3986
+ const global = getGlobalOpts11(solarform);
3285
3987
  const client = createServiceClient("solar", global);
3286
3988
  const body = await parseData4(opts.data);
3287
3989
  if (!body) {
@@ -3309,7 +4011,7 @@ Examples:
3309
4011
  "Get the solar form configuration for the authenticated client.\nReturns form settings, appearance, enabled steps, custom fields, etc."
3310
4012
  ).action(async () => {
3311
4013
  try {
3312
- const global = getGlobalOpts10(solarform);
4014
+ const global = getGlobalOpts11(solarform);
3313
4015
  const client = createServiceClient("solar", global);
3314
4016
  const res = await client.get("/solar-form/solar-form-config");
3315
4017
  output(res.data, global);
@@ -3321,7 +4023,7 @@ Examples:
3321
4023
  "List or create solar form submission statistics.\nUsed for tracking form analytics and conversion data."
3322
4024
  ).option("--create", "Create a new statistics entry").option("--update", "Update an existing statistics entry").option("--data <json>", "Statistics data as JSON").option("--stats-id <id>", "Statistics ID (for update or linking)").action(async (opts) => {
3323
4025
  try {
3324
- const global = getGlobalOpts10(solarform);
4026
+ const global = getGlobalOpts11(solarform);
3325
4027
  const client = createServiceClient("solar", global);
3326
4028
  if (opts.update) {
3327
4029
  const body = await parseData4(opts.data);