@enerlence/suntropy-cli 0.11.3 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,6 +35,27 @@ suntropy auth status
35
35
  > root command (e.g. `suntropy --profile dev auth status`). They apply to every
36
36
  > subcommand, including `auth`.
37
37
 
38
+ ### Token resolution order
39
+
40
+ The auth token is resolved in this order (first non-empty wins):
41
+
42
+ 1. `--token <jwt>` global flag
43
+ 2. `SUNTROPY_API_KEY` environment variable
44
+ 3. `token` stored in the active profile (`~/.suntropy/config.json`, written by
45
+ `auth set-key` / `auth login`)
46
+
47
+ The `SUNTROPY_API_KEY` fallback (available since `0.11.3`) lets a host runtime
48
+ inject a just-in-time credential without a prior `auth login` / `auth set-key`:
49
+
50
+ ```bash
51
+ SUNTROPY_API_KEY=<jwt> suntropy studies list
52
+ ```
53
+
54
+ > ⚠️ The value must be a **JWT** (the one you'd pass to `auth set-key --key`),
55
+ > since the CLI sends it as `Authorization: Bearer <token>`. Despite the name, it
56
+ > is not a `shp_`/`devic-`-style API key — an invalid format yields a 401 from
57
+ > the backend.
58
+
38
59
  ## Global Options
39
60
 
40
61
  | Option | Default | Description |
@@ -42,7 +63,7 @@ suntropy auth status
42
63
  | `--format json\|human\|csv` | `json` | Output format |
43
64
  | `--fields f1,f2,...` | all | Select specific fields |
44
65
  | `--server <url>` | config | Override API server URL |
45
- | `--token <jwt>` | config | Override auth token |
66
+ | `--token <jwt>` | `SUNTROPY_API_KEY` env / config | Override auth token (see "Token resolution order") |
46
67
  | `--profile <name>` | default | Config profile |
47
68
  | `--verbose` | false | Show HTTP details on stderr |
48
69
  | `--quiet` | false | Suppress non-data output |
@@ -139,6 +139,15 @@ function createUnauthClient(baseURL) {
139
139
  headers: { "Content-Type": "application/json" }
140
140
  });
141
141
  }
142
+ var OBJECT_ID_RE = /^[a-f0-9]{24}$/i;
143
+ function assertStudyObjectId(id) {
144
+ if (OBJECT_ID_RE.test(id)) return id;
145
+ const looksLikeUuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(id);
146
+ const hint = looksLikeUuid ? `It looks like a UUID (e.g. a chat or shareable uid), not a study _id.` : `A study _id is a 24-character hex MongoDB ObjectId.`;
147
+ throw new Error(
148
+ `Invalid study id: "${id}". ${hint} Get the correct id from \`suntropy studies list\` (the \`solarStudyId\` field).`
149
+ );
150
+ }
142
151
  function assertFound(data, entity, id) {
143
152
  const isEmptyObject = data != null && typeof data === "object" && !Array.isArray(data) && Object.keys(data).length === 0;
144
153
  if (data == null || typeof data !== "object" || Array.isArray(data) || isEmptyObject) {
@@ -2275,6 +2284,7 @@ function registerStudyBuilderCommands(studies) {
2275
2284
  "Download an existing study from the backend into a local file.\nExample: suntropy studies pull abc123 --file /tmp/study.json"
2276
2285
  ).option("--file <path>", "Output file path").action(async (studyId, opts) => {
2277
2286
  try {
2287
+ assertStudyObjectId(studyId);
2278
2288
  const filePath = resolveFile(opts);
2279
2289
  const global = getGlobalOpts7(studies);
2280
2290
  const client = createServiceClient("solar", global);
@@ -2635,7 +2645,7 @@ Examples:
2635
2645
  });
2636
2646
  set.command("economics").description(
2637
2647
  "Set economic parameters for the study.\nExample: suntropy studies set economics --file study.json --margin 15 --total-cost 6500 --lifetime 25"
2638
- ).option("--file <path>", "Study file path").option("--margin <n>", "Margin percentage").option("--total-cost <n>", "Total installation cost (\u20AC)").option("--lifetime <n>", "Installation lifetime in years").option("--inflation <n>", "Inflation rate percentage").option("--taxes-pct <n>", "Tax percentage").option("--include-taxes", "Include taxes in pricing").option("--commercial-fee <n>", "Commercial fee percentage").option("--excesses-mode <mode>", "Excesses compensation: gridSelling, PPA, noInjection, virtualBattery").option("--excesses-buy-price <n>", "Excesses buy price (\u20AC/MWh)").option("--excesses-selling-price <n>", "Excesses selling price (\u20AC/MWh)").option("--peak-power-cost <n>", "Cost per kWp (\u20AC/kWp)").option("--guarantee-production <n>", "Guarantee production percentage").action(async (opts) => {
2648
+ ).option("--file <path>", "Study file path").option("--margin <n>", "Margin percentage").option("--total-cost <n>", "Total installation cost (\u20AC)").option("--lifetime <n>", "Installation lifetime in years").option("--inflation <n>", "Inflation rate percentage").option("--taxes-pct <n>", "Tax percentage").option("--include-taxes", "Include taxes in pricing").option("--commercial-fee <n>", "Commercial fee percentage").option("--excesses-mode <mode>", "Excesses compensation: gridSelling, PPA, noInjection, virtualBattery").option("--excesses-buy-price <n>", "Excesses buy price (\u20AC/MWh)").option("--excesses-selling-price <n>", "Excesses selling price (\u20AC/MWh)").option("--peak-power-cost <n>", "Cost per Wp (\u20AC/Wp). If --total-cost is omitted, totalCost is derived as cost \xD7 installed peak power (W)").option("--guarantee-production <n>", "Guarantee production percentage").action(async (opts) => {
2639
2649
  try {
2640
2650
  const result = updateStudy(resolveFile(opts), (study) => {
2641
2651
  const er = study.economicResults || {};
@@ -2651,6 +2661,30 @@ Examples:
2651
2661
  if (opts.excessesSellingPrice !== void 0) er.excessesSellingPrice = parseFloat(opts.excessesSellingPrice);
2652
2662
  if (opts.peakPowerCost !== void 0) er.peakPowerCost = parseFloat(opts.peakPowerCost);
2653
2663
  if (opts.guaranteeProduction !== void 0) er.guaranteeProductionPercentage = parseFloat(opts.guaranteeProduction);
2664
+ if (opts.peakPowerCost !== void 0 && opts.totalCost === void 0) {
2665
+ const surfaces = study.surfaces || [];
2666
+ let totalWatts = 0;
2667
+ let resolvedCount = 0;
2668
+ let anyDefault = false;
2669
+ for (const surface of surfaces) {
2670
+ const { installedPower, isDefault } = resolveSurfaceInstalledPower(surface, study);
2671
+ if (isDefault) {
2672
+ anyDefault = true;
2673
+ continue;
2674
+ }
2675
+ totalWatts += installedPower;
2676
+ resolvedCount += 1;
2677
+ }
2678
+ if (resolvedCount > 0 && !anyDefault) {
2679
+ er.totalCost = Math.round(parseFloat(opts.peakPowerCost) * totalWatts * 100) / 100;
2680
+ } else {
2681
+ process.stderr.write(
2682
+ JSON.stringify({
2683
+ warning: "Set --peak-power-cost but could not derive totalCost: " + (surfaces.length === 0 ? "the study has no surfaces." : "some surfaces have no resolvable installed power (set panels + kit/panel first).") + " Pass --total-cost explicitly to set it."
2684
+ }) + "\n"
2685
+ );
2686
+ }
2687
+ }
2654
2688
  study.economicResults = er;
2655
2689
  return void 0;
2656
2690
  });
@@ -3246,6 +3280,7 @@ Example: suntropy studies set data --file study.json --data '{"referenceId":"REF
3246
3280
  'Add a comment to an existing study via API.\nExample: suntropy studies comment abc123 --content "Revisado por agente"'
3247
3281
  ).requiredOption("--content <text>", "Comment text").addOption(new Option("--as-alexandria").hideHelp()).addOption(new Option("--reply-to-user <userUID>").hideHelp()).addOption(new Option("--reply-to-name <name>").hideHelp()).action(async (studyId, opts) => {
3248
3282
  try {
3283
+ assertStudyObjectId(studyId);
3249
3284
  const global = getGlobalOpts7(studies);
3250
3285
  const client = createServiceClient("solar", global);
3251
3286
  const comment = createComment("commented", opts.content, {
@@ -3569,6 +3604,7 @@ function registerStudiesCommands(program2) {
3569
3604
  });
3570
3605
  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) => {
3571
3606
  try {
3607
+ if (opts.byStudyId) assertStudyObjectId(id);
3572
3608
  const global = getGlobalOpts8(studies);
3573
3609
  const client = createServiceClient("solar", global);
3574
3610
  const path = opts.byStudyId ? `/solar-study/metadata/solar-study-id/${id}` : `/solar-study/findSolarStudyMetadataById/${id}`;
@@ -3582,6 +3618,7 @@ function registerStudiesCommands(program2) {
3582
3618
  "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)"
3583
3619
  ).option("--expand <sections>", 'Comma-separated sections to expand (or "all")').action(async (studyId, opts) => {
3584
3620
  try {
3621
+ assertStudyObjectId(studyId);
3585
3622
  const global = getGlobalOpts8(studies);
3586
3623
  const client = createServiceClient("solar", global);
3587
3624
  const res = await client.get(`/solar-study/findById/${studyId}`);
@@ -3604,6 +3641,7 @@ function registerStudiesCommands(program2) {
3604
3641
  "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."
3605
3642
  ).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) => {
3606
3643
  try {
3644
+ assertStudyObjectId(studyId);
3607
3645
  const global = getGlobalOpts8(studies);
3608
3646
  const client = createServiceClient("solar", global);
3609
3647
  const res = await client.get(`/solar-study/findById/${studyId}`);
@@ -4598,7 +4636,7 @@ function registerCommandProfileCommand(program2) {
4598
4636
  }
4599
4637
 
4600
4638
  // src/index.ts
4601
- var CLI_VERSION = true ? "0.11.3" : "0.0.0-dev";
4639
+ var CLI_VERSION = true ? "0.11.5" : "0.0.0-dev";
4602
4640
  function createProgram() {
4603
4641
  const program2 = new Command3();
4604
4642
  program2.name("suntropy").description("Agent-first CLI for Suntropy solar platform. Optimized for programmatic data manipulation and progressive exploration.").version(CLI_VERSION).option("--format <format>", "Output format: json (default), human, csv", "json").option("--fields <fields>", "Comma-separated fields to include in output").option("--server <url>", "Override API server URL").option("--token <jwt>", "Override authentication token").option("--profile <name>", "Use a specific config profile").option("--verbose", "Show HTTP request/response details on stderr").option("--quiet", "Suppress non-data output").option("--save <file>", "Save output to file (also writes to stdout)");