@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 +22 -1
- package/dist/bin/suntropy.js +40 -2
- package/dist/bin/suntropy.js.map +1 -1
- package/package.json +1 -1
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 |
|
package/dist/bin/suntropy.js
CHANGED
|
@@ -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
|
|
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.
|
|
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)");
|