systemlink-cli 1.6.2__tar.gz → 1.6.4__tar.gz

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.
Files changed (76) hide show
  1. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/PKG-INFO +1 -1
  2. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/pyproject.toml +1 -1
  3. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/_version.py +1 -1
  4. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/skills/systemlink-webapp/SKILL.md +22 -0
  5. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/skills/systemlink-webapp/references/nimble-angular.md +14 -0
  6. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/user_click.py +99 -0
  7. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/utils.py +1 -1
  8. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/LICENSE +0 -0
  9. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/dff-editor/editor.js +0 -0
  10. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/dff-editor/index.html +0 -0
  11. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/__init__.py +0 -0
  12. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/__main__.py +0 -0
  13. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/asset_click.py +0 -0
  14. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/cli_formatters.py +0 -0
  15. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/cli_utils.py +0 -0
  16. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/comment_click.py +0 -0
  17. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/completion_click.py +0 -0
  18. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/config.py +0 -0
  19. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/config_click.py +0 -0
  20. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/dff_click.py +0 -0
  21. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/dff_decorators.py +0 -0
  22. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/example_click.py +0 -0
  23. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/example_loader.py +0 -0
  24. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/example_provisioner.py +0 -0
  25. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/README.md +0 -0
  26. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/_schema/schema-v1.0.json +0 -0
  27. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/demo-complete-workflow/README.md +0 -0
  28. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
  29. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/demo-test-plans/README.md +0 -0
  30. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/demo-test-plans/config.yaml +0 -0
  31. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
  32. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
  33. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
  34. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
  35. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
  36. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
  37. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
  38. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
  39. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
  40. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  41. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/feed_click.py +0 -0
  42. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/file_click.py +0 -0
  43. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/function_click.py +0 -0
  44. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/function_templates.py +0 -0
  45. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/main.py +0 -0
  46. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/mcp_click.py +0 -0
  47. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/mcp_server.py +0 -0
  48. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/notebook_click.py +0 -0
  49. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/platform.py +0 -0
  50. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/policy_click.py +0 -0
  51. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/policy_utils.py +0 -0
  52. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/profiles.py +0 -0
  53. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/response_handlers.py +0 -0
  54. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/rich_output.py +0 -0
  55. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/routine_click.py +0 -0
  56. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/skill_click.py +0 -0
  57. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/skills/slcli/SKILL.md +0 -0
  58. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
  59. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/skills/slcli/references/filtering.md +0 -0
  60. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
  61. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/skills/systemlink-webapp/references/layout-patterns.md +0 -0
  62. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
  63. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/ssl_trust.py +0 -0
  64. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/system_click.py +0 -0
  65. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/table_utils.py +0 -0
  66. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/tag_click.py +0 -0
  67. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/templates_click.py +0 -0
  68. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/testmonitor_click.py +0 -0
  69. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/universal_handlers.py +0 -0
  70. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/web_editor.py +0 -0
  71. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/webapp_click.py +0 -0
  72. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/workflow_preview.py +0 -0
  73. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/workflows_click.py +0 -0
  74. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/workitem_click.py +0 -0
  75. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/workspace_click.py +0 -0
  76. {systemlink_cli-1.6.2 → systemlink_cli-1.6.4}/slcli/workspace_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: systemlink-cli
3
- Version: 1.6.2
3
+ Version: 1.6.4
4
4
  Summary: SystemLink Integrator CLI - cross-platform CLI for SystemLink workflows and templates.
5
5
  License-File: LICENSE
6
6
  Author: Fred Visser
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "systemlink-cli"
3
- version = "1.6.2"
3
+ version = "1.6.4"
4
4
  description = "SystemLink Integrator CLI - cross-platform CLI for SystemLink workflows and templates."
5
5
  authors = ["Fred Visser <fred.visser@emerson.com>"]
6
6
  packages = [{ include = "slcli" }]
@@ -1,4 +1,4 @@
1
1
  """Version information for slcli."""
2
2
 
3
3
  # This file is auto-generated. Do not edit manually.
4
- __version__ = "1.6.2"
4
+ __version__ = "1.6.4"
@@ -161,6 +161,9 @@ import { APP_BASE_HREF } from '@angular/common';
161
161
  // Most Nimble component modules are exported from the main `@ni/nimble-angular` barrel.
162
162
  // Icon modules (e.g. NimbleIconMagnifyingGlassModule) are ONLY in the main barrel —
163
163
  // sub-paths like `@ni/nimble-angular/icons/magnifying-glass` do NOT exist.
164
+ // Do NOT add `@ni/nimble-components` or register raw custom elements just to make
165
+ // Angular templates compile. If a Nimble element is unknown, import the missing
166
+ // Angular module from `@ni/nimble-angular` instead.
164
167
  import {
165
168
  NimbleThemeProviderModule,
166
169
  NimbleButtonModule,
@@ -214,8 +217,25 @@ import { MyFeatureComponent } from './my-feature/my-feature.component';
214
217
  export class AppModule {}
215
218
  ```
216
219
 
220
+ When using `@ni/nimble-angular`, prefer the Angular wrappers end-to-end:
221
+
222
+ - Do **not** add `@ni/nimble-components` as a direct dependency just to register icons or other raw custom elements.
223
+ - Do **not** use `CUSTOM_ELEMENTS_SCHEMA` as a workaround for unknown Nimble elements. In this codebase that is usually a sign that the corresponding Nimble Angular module import is missing, and `CUSTOM_ELEMENTS_SCHEMA` suppresses valuable template checking.
224
+ - If Angular reports an unknown Nimble tag, fix the module imports first.
225
+
217
226
  For Nimble form controls (`nimble-text-field`, `nimble-select`, etc.), bind with Angular forms APIs (`[(ngModel)]`, `[formControl]`, or `formControlName`) and use `(ngModelChange)` for value-change reactions. Avoid native control bindings like `[value]`, `(input)`, or `(change)` on Nimble elements.
218
227
 
228
+ For control labels, prefer Nimble's built-in label pattern by slotting text content inside the control instead of pairing the control with a separate HTML `<label>` element for the primary label:
229
+
230
+ ```html
231
+ <nimble-select [(ngModel)]="selectedWorkspace">
232
+ Workspace
233
+ <nimble-list-option *ngFor="let ws of workspaces" [value]="ws.id">
234
+ {{ ws.name }}
235
+ </nimble-list-option>
236
+ </nimble-select>
237
+ ```
238
+
219
239
  **Critical:** Provide `APP_BASE_HREF` via DI and **remove the `<base href="/">` tag from `index.html`**. SystemLink enforces a `base-uri 'self'` CSP directive; the `<base>` element violates it.
220
240
 
221
241
  ---
@@ -737,6 +757,8 @@ Save the returned webapp ID — you'll need it for every subsequent redeploy.
737
757
  | `InputFieldValidationError` on API call | SDK-generated request body has wrong shape | Inspect raw API; the generated type may add or omit a `request: {}` wrapper. Use direct `fetch` with manually constructed body |
738
758
  | nimble-dialog does not open | `*ngIf` destroys element before `ViewChild` can resolve | Remove `*ngIf` from the dialog element; use `@ViewChild` + `ElementRef` and call `nativeElement.show()` / `nativeElement.close()` |
739
759
  | Icon module import fails | Icon sub-path `@ni/nimble-angular/icons/...` does not exist | Import icon modules from the main `@ni/nimble-angular` barrel only |
760
+ | Angular says a Nimble tag is unknown | Missing `@ni/nimble-angular` module import | Import the missing wrapper module instead of adding `CUSTOM_ELEMENTS_SCHEMA` or registering raw `@ni/nimble-components` elements |
761
+ | Nimble form control label looks detached or duplicated | Used a separate HTML `<label>` for the primary control label | Slot the label text inside `nimble-text-field`, `nimble-select`, and similar controls |
740
762
  | Table rows empty despite correct response | `projection` flattens nested objects | Remove `projection` from query body |
741
763
  | `TableRecord` type error | Row type missing index signature | Add `[key: string]: FieldValue \| undefined` |
742
764
  | Button appearance invalid | Wrong value for `appearance` attr | Use `appearance="block" appearance-variant="accent"` |
@@ -1,5 +1,13 @@
1
1
  # Nimble Angular — Template & Usage Reference
2
2
 
3
+ ## Wrapper-first rule
4
+
5
+ When building Angular apps with Nimble, use `@ni/nimble-angular` wrapper modules as the default integration path.
6
+
7
+ - Import the needed Angular module for each Nimble control instead of registering raw custom elements from `@ni/nimble-components`.
8
+ - Do not add `CUSTOM_ELEMENTS_SCHEMA` just to silence unknown Nimble elements in templates. That usually hides a missing module import and weakens Angular's template validation.
9
+ - If a Nimble icon or control is unknown, first look for the matching module in `@ni/nimble-angular` and import it.
10
+
3
11
  ## nimble-theme-provider
4
12
 
5
13
  Wrap your entire app. Always place at the root component level.
@@ -176,6 +184,8 @@ import { NimbleTextFieldModule } from "@ni/nimble-angular";
176
184
  </nimble-text-field>
177
185
  ```
178
186
 
187
+ Use the text inside the element as the control's primary label. Do not add a separate HTML `<label>` element for the basic Nimble control label unless you have a specific accessibility or layout need that cannot be expressed with Nimble's built-in labeling pattern.
188
+
179
189
  Import icon modules from the **main `@ni/nimble-angular` barrel** — icon-specific sub-paths do not exist:
180
190
 
181
191
  ```typescript
@@ -195,6 +205,7 @@ import { NimbleSelectModule, NimbleListOptionModule } from "@ni/nimble-angular";
195
205
  ```html
196
206
  <!-- Basic -->
197
207
  <nimble-select [(ngModel)]="selectedType" (ngModelChange)="onTypeChange()">
208
+ Type
198
209
  <nimble-list-option value="">All types</nimble-list-option>
199
210
  <nimble-list-option value="DOUBLE">Double</nimble-list-option>
200
211
  <nimble-list-option value="STRING">String</nimble-list-option>
@@ -203,12 +214,15 @@ import { NimbleSelectModule, NimbleListOptionModule } from "@ni/nimble-angular";
203
214
 
204
215
  <!-- With built-in filter (useful for long lists) -->
205
216
  <nimble-select filter-mode="standard" [(ngModel)]="selectedWorkspace">
217
+ Workspace
206
218
  <nimble-list-option *ngFor="let ws of workspaces" [value]="ws.id"
207
219
  >{{ ws.name }}</nimble-list-option
208
220
  >
209
221
  </nimble-select>
210
222
  ```
211
223
 
224
+ Use the slotted text content as the select label instead of pairing the control with a separate HTML `<label>` for the primary label.
225
+
212
226
  > Use `filter-mode="standard"` to add a built-in text filter to the dropdown — no custom search logic needed for long option lists.
213
227
 
214
228
  ---
@@ -29,6 +29,103 @@ from .workspace_utils import get_workspace_display_name, resolve_workspace_id
29
29
 
30
30
  USER_QUERY_PAGE_SIZE = 100
31
31
  USER_JSON_DEFAULT_TAKE = 1000
32
+ AUTH_WILDCARD_VALUES = {"*", "*/*", "*:*"}
33
+ AUTH_MUTATING_VERBS = {"*", "create", "write", "update", "manage", "admin", "delete"}
34
+ AUTH_ADMIN_RESOURCE_KEYWORDS = {"policy", "policies", "role", "roles"}
35
+
36
+
37
+ def _has_global_auth_scope(value: Any) -> bool:
38
+ """Return whether an auth scope value grants wildcard access."""
39
+ if isinstance(value, str):
40
+ return value.strip().lower() in AUTH_WILDCARD_VALUES
41
+ if isinstance(value, list):
42
+ return any(
43
+ isinstance(item, str) and item.strip().lower() in AUTH_WILDCARD_VALUES for item in value
44
+ )
45
+ return False
46
+
47
+
48
+ def _action_grants_auth_management(action: str) -> bool:
49
+ """Return whether an action implies auth policy or role management access."""
50
+ normalized = action.strip().lower()
51
+ if normalized in AUTH_WILDCARD_VALUES:
52
+ return True
53
+
54
+ action_parts = [part.strip().lower() for part in normalized.split(":") if part.strip()]
55
+ if len(action_parts) < 2 or action_parts[0] not in {"niauth", "auth"}:
56
+ return False
57
+
58
+ remainder = action_parts[1:]
59
+ if remainder == ["*"]:
60
+ return True
61
+
62
+ if remainder[-1] not in AUTH_MUTATING_VERBS and "*" not in remainder:
63
+ return False
64
+
65
+ if len(remainder) == 1:
66
+ return True
67
+
68
+ return any(part in AUTH_ADMIN_RESOURCE_KEYWORDS for part in remainder[:-1])
69
+
70
+
71
+ def _has_user_admin_access(auth_context: Dict[str, Any]) -> bool:
72
+ """Return whether the current caller appears to have server-admin style access."""
73
+ for policy in auth_context.get("policies", []):
74
+ statements = policy.get("statements", [])
75
+ if not isinstance(statements, list):
76
+ continue
77
+
78
+ for statement in statements:
79
+ if not isinstance(statement, dict):
80
+ continue
81
+
82
+ if not _has_global_auth_scope(statement.get("workspace")):
83
+ continue
84
+ if not _has_global_auth_scope(statement.get("resource")):
85
+ continue
86
+
87
+ actions = statement.get("actions", [])
88
+ if not isinstance(actions, list):
89
+ continue
90
+
91
+ for action in actions:
92
+ if isinstance(action, str) and _action_grants_auth_management(action):
93
+ return True
94
+
95
+ return False
96
+
97
+
98
+ def _try_get_current_auth_context() -> Optional[Dict[str, Any]]:
99
+ """Fetch effective auth data for the current caller when available."""
100
+ url = f"{get_base_url()}/niauth/v1/auth"
101
+
102
+ try:
103
+ auth_response = make_api_request("GET", url, payload=None, handle_errors=False)
104
+ auth_context = auth_response.json()
105
+ return auth_context if isinstance(auth_context, dict) else None
106
+ except Exception as exc:
107
+ error_response = getattr(exc, "response", None)
108
+ if error_response is not None and error_response.status_code in {401, 403}:
109
+ handle_api_error(exc)
110
+ return None
111
+
112
+
113
+ def _ensure_user_admin_access(operation: str) -> None:
114
+ """Fail fast when the caller clearly lacks admin access for user mutations."""
115
+ auth_context = _try_get_current_auth_context()
116
+ if auth_context is None:
117
+ return
118
+
119
+ if _has_user_admin_access(auth_context):
120
+ return
121
+
122
+ click.echo(
123
+ f"✗ User and service account {operation} requires server admin permissions. "
124
+ "The active API key does not appear to have wildcard auth role or policy access "
125
+ "across all workspaces.",
126
+ err=True,
127
+ )
128
+ sys.exit(ExitCodes.PERMISSION_DENIED)
32
129
 
33
130
 
34
131
  def _get_policy_details(policy_id: str) -> Optional[dict]:
@@ -851,6 +948,7 @@ def register_user_commands(cli: click.Group) -> None:
851
948
  from .utils import check_readonly_mode
852
949
 
853
950
  check_readonly_mode("create a user")
951
+ _ensure_user_admin_access("creation")
854
952
 
855
953
  # If user_type wasn't specified via CLI, prompt for it first
856
954
  if user_type is None:
@@ -1060,6 +1158,7 @@ def register_user_commands(cli: click.Group) -> None:
1060
1158
  from .utils import check_readonly_mode
1061
1159
 
1062
1160
  check_readonly_mode("update a user")
1161
+ _ensure_user_admin_access("updates")
1063
1162
 
1064
1163
  # First, fetch the user to check if it's a service account
1065
1164
  get_url = f"{get_base_url()}/niuser/v1/users/{user_id}"
@@ -75,7 +75,7 @@ def handle_api_error(exc: Exception) -> None:
75
75
  if "not found" in error_msg:
76
76
  click.echo(f"✗ Resource not found: {exc}", err=True)
77
77
  sys.exit(ExitCodes.NOT_FOUND)
78
- elif "permission" in error_msg or "unauthorized" in error_msg:
78
+ elif "permission" in error_msg or "unauthorized" in error_msg or "forbidden" in error_msg:
79
79
  click.echo(f"✗ Permission denied: {exc}", err=True)
80
80
  sys.exit(ExitCodes.PERMISSION_DENIED)
81
81
  elif "network" in error_msg or "connection" in error_msg:
File without changes