systemlink-cli 1.6.1__tar.gz → 1.6.3__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.
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/PKG-INFO +1 -1
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/pyproject.toml +1 -1
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/_version.py +1 -1
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/skills/slcli/SKILL.md +6 -5
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/skills/systemlink-webapp/SKILL.md +3 -2
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/user_click.py +99 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/utils.py +1 -1
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/webapp_click.py +122 -25
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/LICENSE +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/dff-editor/editor.js +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/dff-editor/index.html +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/__init__.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/__main__.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/asset_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/cli_formatters.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/cli_utils.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/comment_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/completion_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/config.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/config_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/dff_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/dff_decorators.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/example_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/example_loader.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/example_provisioner.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/README.md +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/_schema/schema-v1.0.json +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/demo-complete-workflow/README.md +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/demo-test-plans/README.md +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/demo-test-plans/config.yaml +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/feed_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/file_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/function_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/function_templates.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/main.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/mcp_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/mcp_server.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/notebook_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/platform.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/policy_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/policy_utils.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/profiles.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/response_handlers.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/rich_output.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/routine_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/skill_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/skills/slcli/references/filtering.md +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/skills/systemlink-webapp/references/layout-patterns.md +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/skills/systemlink-webapp/references/nimble-angular.md +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/ssl_trust.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/system_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/table_utils.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/tag_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/templates_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/testmonitor_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/universal_handlers.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/web_editor.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/workflow_preview.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/workflows_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/workitem_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/workspace_click.py +0 -0
- {systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/workspace_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "systemlink-cli"
|
|
3
|
-
version = "1.6.
|
|
3
|
+
version = "1.6.3"
|
|
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" }]
|
|
@@ -666,7 +666,7 @@ Scaffold, package, and publish custom web applications to SystemLink.
|
|
|
666
666
|
|
|
667
667
|
```bash
|
|
668
668
|
slcli webapp init <DIRECTORY> # Scaffold the Angular starter
|
|
669
|
-
slcli webapp manifest init <DIRECTORY> [OPTIONS] # Create
|
|
669
|
+
slcli webapp manifest init <DIRECTORY> [OPTIONS] # Create nipkg.config.json for packaging
|
|
670
670
|
slcli webapp pack [FOLDER] [--config FILE] [-o OUTPUT_FILE] # Package a webapp into a .nipkg
|
|
671
671
|
slcli webapp list [-w WORKSPACE] [-t INT] [-f json]
|
|
672
672
|
slcli webapp get <WEBAPP_ID> [-f json]
|
|
@@ -680,11 +680,12 @@ project-scoped skills into `.agents/skills/` and creates `PROMPTS.md` plus `STAR
|
|
|
680
680
|
AI assistant can bootstrap the Angular workspace in place with the same Nimble/SystemLink
|
|
681
681
|
conventions described by the `systemlink-webapp` skill.
|
|
682
682
|
|
|
683
|
-
`webapp manifest init` writes `
|
|
684
|
-
|
|
683
|
+
`webapp manifest init` writes `nipkg.config.json` using the Plugin Manager field names
|
|
684
|
+
(`section`, `maintainer`, `homepage`, `xbPlugin`, `slPluginManagerTags`,
|
|
685
685
|
`slPluginManagerMinServerVersion`, `iconFile`). `webapp pack --config ...` consumes that
|
|
686
|
-
metadata, carries the icon into the package,
|
|
687
|
-
generated `.nipkg
|
|
686
|
+
metadata, carries the icon into the package, writes the matching control-file fields into the
|
|
687
|
+
generated `.nipkg`, and emits a thin `manifest.json` with `schemaVersion`, `nipkgFile`,
|
|
688
|
+
`sha256`, and any configured provenance fields.
|
|
688
689
|
|
|
689
690
|
### skill — AI skill installation
|
|
690
691
|
|
|
@@ -24,8 +24,9 @@ That command lays down the SystemLink starter layer (`.agents/skills/`, `PROMPTS
|
|
|
24
24
|
`START_HERE.md`) while Angular CLI remains responsible for generating the Angular workspace.
|
|
25
25
|
|
|
26
26
|
When the user wants to package the app for Plugin Manager submission, prefer
|
|
27
|
-
`slcli webapp manifest init <app-dir> ...` to generate `
|
|
28
|
-
with the current Plugin Manager field names, then use `slcli webapp pack --config
|
|
27
|
+
`slcli webapp manifest init <app-dir> ...` to generate `nipkg.config.json`
|
|
28
|
+
with the current Plugin Manager field names, then use `slcli webapp pack --config ...`
|
|
29
|
+
to build the `.nipkg` and generate the thin `manifest.json` with the artifact SHA-256.
|
|
29
30
|
|
|
30
31
|
---
|
|
31
32
|
|
|
@@ -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:
|
|
@@ -4,6 +4,7 @@ Provides local scaffolding (init), packing helpers (pack), and remote
|
|
|
4
4
|
management (list, get, delete, publish, open).
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import hashlib
|
|
7
8
|
import io
|
|
8
9
|
import re
|
|
9
10
|
import shutil
|
|
@@ -45,6 +46,10 @@ _ALLOWED_ICON_EXTENSIONS = frozenset({".svg", ".png", ".jpg", ".jpeg", ".webp",
|
|
|
45
46
|
_MAX_PACKAGE_LENGTH = 100
|
|
46
47
|
_MAX_DISPLAY_NAME_LENGTH = 200
|
|
47
48
|
_MAX_DESCRIPTION_LENGTH = 5000
|
|
49
|
+
_MAX_RELEASE_TAG_LENGTH = 200
|
|
50
|
+
_MAX_SCREENSHOT_COUNT = 3
|
|
51
|
+
_SOURCE_REPO_PATTERN = re.compile(r"^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$")
|
|
52
|
+
_SOURCE_COMMIT_PATTERN = re.compile(r"^[0-9a-f]{40}$")
|
|
48
53
|
_ALLOWED_PLUGIN_MANAGER_KEYS = {
|
|
49
54
|
"buildCommand",
|
|
50
55
|
"buildDir",
|
|
@@ -56,7 +61,11 @@ _ALLOWED_PLUGIN_MANAGER_KEYS = {
|
|
|
56
61
|
"maintainer",
|
|
57
62
|
"nipkgFile",
|
|
58
63
|
"package",
|
|
64
|
+
"releaseTag",
|
|
59
65
|
"section",
|
|
66
|
+
"screenshots",
|
|
67
|
+
"sourceCommit",
|
|
68
|
+
"sourceRepo",
|
|
60
69
|
"slPluginManagerMinServerVersion",
|
|
61
70
|
"slPluginManagerTags",
|
|
62
71
|
"version",
|
|
@@ -237,6 +246,15 @@ def _copy_icon_file_to_directory(source_icon: Path, directory: Path) -> bool:
|
|
|
237
246
|
return True
|
|
238
247
|
|
|
239
248
|
|
|
249
|
+
def _compute_sha256(file_path: Path) -> str:
|
|
250
|
+
"""Return the SHA-256 checksum for a file."""
|
|
251
|
+
sha256_hash = hashlib.sha256()
|
|
252
|
+
with open(file_path, "rb") as input_file:
|
|
253
|
+
for chunk in iter(lambda: input_file.read(1024 * 1024), b""):
|
|
254
|
+
sha256_hash.update(chunk)
|
|
255
|
+
return sha256_hash.hexdigest()
|
|
256
|
+
|
|
257
|
+
|
|
240
258
|
def _normalize_plugin_manager_metadata(raw_metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
241
259
|
"""Normalize legacy App Store keys to Plugin Manager keys."""
|
|
242
260
|
metadata = dict(raw_metadata)
|
|
@@ -252,8 +270,8 @@ def _validate_plugin_manager_metadata(
|
|
|
252
270
|
raw_metadata: Dict[str, Any],
|
|
253
271
|
require_build_dir: bool = False,
|
|
254
272
|
base_dir: Optional[Path] = None,
|
|
255
|
-
) -> Dict[str,
|
|
256
|
-
"""Validate and normalize Plugin Manager
|
|
273
|
+
) -> Dict[str, Any]:
|
|
274
|
+
"""Validate and normalize Plugin Manager packaging metadata."""
|
|
257
275
|
from urllib.parse import urlparse
|
|
258
276
|
|
|
259
277
|
metadata = _normalize_plugin_manager_metadata(raw_metadata)
|
|
@@ -274,8 +292,13 @@ def _validate_plugin_manager_metadata(
|
|
|
274
292
|
build_dir = str(metadata.get("buildDir", "")).strip()
|
|
275
293
|
build_command = str(metadata.get("buildCommand", "")).strip()
|
|
276
294
|
icon_file = str(metadata.get("iconFile", "")).strip()
|
|
295
|
+
source_repo = str(metadata.get("sourceRepo", "")).strip()
|
|
296
|
+
release_tag = str(metadata.get("releaseTag", "")).strip()
|
|
297
|
+
source_commit = str(metadata.get("sourceCommit", "")).strip()
|
|
298
|
+
screenshots_raw = metadata.get("screenshots")
|
|
277
299
|
|
|
278
300
|
errors: List[str] = []
|
|
301
|
+
screenshots: List[str] = []
|
|
279
302
|
|
|
280
303
|
if unexpected_keys:
|
|
281
304
|
errors.append("unexpected field(s): " + ", ".join(unexpected_keys))
|
|
@@ -308,6 +331,25 @@ def _validate_plugin_manager_metadata(
|
|
|
308
331
|
errors.append(f"xbPlugin must be one of: {', '.join(_XB_PLUGIN_VALUES)}")
|
|
309
332
|
if nipkg_file and not nipkg_file.endswith(".nipkg"):
|
|
310
333
|
errors.append("nipkgFile must end with .nipkg")
|
|
334
|
+
if source_repo and not _SOURCE_REPO_PATTERN.match(source_repo):
|
|
335
|
+
errors.append("sourceRepo must be in owner/name format")
|
|
336
|
+
if release_tag and len(release_tag) > _MAX_RELEASE_TAG_LENGTH:
|
|
337
|
+
errors.append(f"releaseTag must be at most {_MAX_RELEASE_TAG_LENGTH} characters")
|
|
338
|
+
if source_commit and not _SOURCE_COMMIT_PATTERN.match(source_commit):
|
|
339
|
+
errors.append("sourceCommit must be a 40-character lowercase git SHA")
|
|
340
|
+
if bool(source_repo) != bool(release_tag):
|
|
341
|
+
errors.append("sourceRepo and releaseTag must be provided together")
|
|
342
|
+
if screenshots_raw not in (None, ""):
|
|
343
|
+
if not isinstance(screenshots_raw, list):
|
|
344
|
+
errors.append("screenshots must be an array of filenames")
|
|
345
|
+
else:
|
|
346
|
+
screenshots = [str(item).strip() for item in screenshots_raw]
|
|
347
|
+
if any(not item for item in screenshots):
|
|
348
|
+
errors.append("screenshots entries must be non-empty strings")
|
|
349
|
+
if len(screenshots) > _MAX_SCREENSHOT_COUNT:
|
|
350
|
+
errors.append(f"screenshots must contain at most {_MAX_SCREENSHOT_COUNT} items")
|
|
351
|
+
if len(set(screenshots)) != len(screenshots):
|
|
352
|
+
errors.append("screenshots entries must be unique")
|
|
311
353
|
if require_build_dir and not build_dir:
|
|
312
354
|
errors.append("buildDir is required when packing from config without a folder argument")
|
|
313
355
|
icon_error = _validate_icon_file_value(icon_file, base_dir)
|
|
@@ -320,7 +362,7 @@ def _validate_plugin_manager_metadata(
|
|
|
320
362
|
click.echo(f" - {error}", err=True)
|
|
321
363
|
sys.exit(ExitCodes.INVALID_INPUT)
|
|
322
364
|
|
|
323
|
-
validated: Dict[str,
|
|
365
|
+
validated: Dict[str, Any] = {
|
|
324
366
|
"package": package_name,
|
|
325
367
|
"version": version,
|
|
326
368
|
"displayName": display_name,
|
|
@@ -342,6 +384,14 @@ def _validate_plugin_manager_metadata(
|
|
|
342
384
|
if build_command:
|
|
343
385
|
validated["buildCommand"] = build_command
|
|
344
386
|
validated["iconFile"] = icon_file
|
|
387
|
+
if source_repo:
|
|
388
|
+
validated["sourceRepo"] = source_repo
|
|
389
|
+
if release_tag:
|
|
390
|
+
validated["releaseTag"] = release_tag
|
|
391
|
+
if source_commit:
|
|
392
|
+
validated["sourceCommit"] = source_commit
|
|
393
|
+
if screenshots:
|
|
394
|
+
validated["screenshots"] = screenshots
|
|
345
395
|
|
|
346
396
|
return validated
|
|
347
397
|
|
|
@@ -349,7 +399,7 @@ def _validate_plugin_manager_metadata(
|
|
|
349
399
|
def _pack_folder_to_nipkg(
|
|
350
400
|
folder: Path,
|
|
351
401
|
output: Optional[Path] = None,
|
|
352
|
-
metadata: Optional[Dict[str,
|
|
402
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
353
403
|
icon_source: Optional[Path] = None,
|
|
354
404
|
) -> Path:
|
|
355
405
|
"""Pack a folder into a .nipkg (ar) file and return the output path.
|
|
@@ -530,7 +580,27 @@ def _build_angular_bootstrap_command(directory: Path) -> str:
|
|
|
530
580
|
)
|
|
531
581
|
|
|
532
582
|
|
|
533
|
-
def
|
|
583
|
+
def _build_submission_manifest(
|
|
584
|
+
nipkg_path: Path, metadata: Optional[Dict[str, Any]] = None
|
|
585
|
+
) -> Dict[str, Any]:
|
|
586
|
+
"""Build the thin submission manifest from a packaged artifact."""
|
|
587
|
+
manifest: Dict[str, Any] = {
|
|
588
|
+
"schemaVersion": 2,
|
|
589
|
+
"nipkgFile": nipkg_path.name,
|
|
590
|
+
"sha256": _compute_sha256(nipkg_path),
|
|
591
|
+
}
|
|
592
|
+
if metadata is not None:
|
|
593
|
+
for key in ("sourceRepo", "releaseTag", "sourceCommit"):
|
|
594
|
+
value = metadata.get(key)
|
|
595
|
+
if value:
|
|
596
|
+
manifest[key] = value
|
|
597
|
+
screenshots = metadata.get("screenshots")
|
|
598
|
+
if screenshots:
|
|
599
|
+
manifest["screenshots"] = screenshots
|
|
600
|
+
return manifest
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _build_webapp_pack_config(
|
|
534
604
|
package_name: str,
|
|
535
605
|
version: str,
|
|
536
606
|
display_name: str,
|
|
@@ -546,9 +616,12 @@ def _build_webapp_manifest_and_config(
|
|
|
546
616
|
build_command: str,
|
|
547
617
|
icon_file: str,
|
|
548
618
|
icon_validation_base_dir: Path,
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
619
|
+
source_repo: str,
|
|
620
|
+
release_tag: str,
|
|
621
|
+
source_commit: str,
|
|
622
|
+
) -> Dict[str, Any]:
|
|
623
|
+
"""Build validated nipkg.config.json payloads."""
|
|
624
|
+
pack_config = _validate_plugin_manager_metadata(
|
|
552
625
|
{
|
|
553
626
|
"package": package_name,
|
|
554
627
|
"version": version,
|
|
@@ -561,18 +634,19 @@ def _build_webapp_manifest_and_config(
|
|
|
561
634
|
"xbPlugin": xb_plugin,
|
|
562
635
|
"slPluginManagerTags": tags,
|
|
563
636
|
"slPluginManagerMinServerVersion": min_server_version,
|
|
564
|
-
"nipkgFile": _default_nipkg_filename(package_name, version),
|
|
565
637
|
"iconFile": icon_file,
|
|
638
|
+
"sourceRepo": source_repo,
|
|
639
|
+
"releaseTag": release_tag,
|
|
640
|
+
"sourceCommit": source_commit,
|
|
566
641
|
},
|
|
567
642
|
base_dir=icon_validation_base_dir,
|
|
568
643
|
)
|
|
569
644
|
|
|
570
|
-
pack_config = dict(manifest)
|
|
571
645
|
pack_config.pop("nipkgFile", None)
|
|
572
646
|
pack_config["buildDir"] = build_dir
|
|
573
647
|
pack_config["buildCommand"] = build_command
|
|
574
648
|
|
|
575
|
-
return
|
|
649
|
+
return pack_config
|
|
576
650
|
|
|
577
651
|
|
|
578
652
|
def _render_angular_prompts_md(directory: Path) -> str:
|
|
@@ -658,7 +732,7 @@ This directory was initialized with `slcli webapp init`.
|
|
|
658
732
|
- bundled AI skills in `.agents/skills/`
|
|
659
733
|
- ready-made prompts in [PROMPTS.md](PROMPTS.md)
|
|
660
734
|
- deployment guidance for `slcli webapp publish`
|
|
661
|
-
- Plugin Manager
|
|
735
|
+
- Plugin Manager packaging config scaffolding via `slcli webapp manifest init`
|
|
662
736
|
|
|
663
737
|
Angular CLI remains the source of truth for the Angular workspace itself. That
|
|
664
738
|
keeps the generated project aligned with current Angular defaults while the
|
|
@@ -774,7 +848,7 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
774
848
|
|
|
775
849
|
@webapp.group(name="manifest")
|
|
776
850
|
def webapp_manifest() -> None:
|
|
777
|
-
"""Create Plugin Manager
|
|
851
|
+
"""Create Plugin Manager packaging config and submission manifest inputs."""
|
|
778
852
|
|
|
779
853
|
@webapp_manifest.command(name="init")
|
|
780
854
|
@click.argument(
|
|
@@ -810,12 +884,27 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
810
884
|
show_default=True,
|
|
811
885
|
help="Build command written to nipkg.config.json",
|
|
812
886
|
)
|
|
887
|
+
@click.option(
|
|
888
|
+
"--source-repo",
|
|
889
|
+
default="",
|
|
890
|
+
help="Optional provenance repository in owner/name format for the generated manifest",
|
|
891
|
+
)
|
|
892
|
+
@click.option(
|
|
893
|
+
"--release-tag",
|
|
894
|
+
default="",
|
|
895
|
+
help="Optional provenance release tag for the generated manifest",
|
|
896
|
+
)
|
|
897
|
+
@click.option(
|
|
898
|
+
"--source-commit",
|
|
899
|
+
default="",
|
|
900
|
+
help="Optional source commit SHA for the generated manifest",
|
|
901
|
+
)
|
|
813
902
|
@click.option(
|
|
814
903
|
"--icon-file",
|
|
815
904
|
required=True,
|
|
816
905
|
help="Path to the icon asset; copied into the manifest directory as iconFile",
|
|
817
906
|
)
|
|
818
|
-
@click.option("--force", is_flag=True, help="Overwrite existing
|
|
907
|
+
@click.option("--force", is_flag=True, help="Overwrite existing packaging files")
|
|
819
908
|
def init_manifest(
|
|
820
909
|
directory: Path,
|
|
821
910
|
package_name: str,
|
|
@@ -831,10 +920,13 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
831
920
|
min_server_version: str,
|
|
832
921
|
build_dir: str,
|
|
833
922
|
build_command: str,
|
|
923
|
+
source_repo: str,
|
|
924
|
+
release_tag: str,
|
|
925
|
+
source_commit: str,
|
|
834
926
|
icon_file: str,
|
|
835
927
|
force: bool,
|
|
836
928
|
) -> None:
|
|
837
|
-
"""Write
|
|
929
|
+
"""Write nipkg.config.json for Plugin Manager packaging."""
|
|
838
930
|
try:
|
|
839
931
|
directory.mkdir(parents=True, exist_ok=True)
|
|
840
932
|
|
|
@@ -842,11 +934,8 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
842
934
|
display_name = display_name or _default_display_name(package_name)
|
|
843
935
|
build_dir = build_dir or _default_angular_build_dir(directory)
|
|
844
936
|
|
|
845
|
-
manifest_path = directory / "manifest.json"
|
|
846
937
|
config_path = directory / "nipkg.config.json"
|
|
847
938
|
existing = []
|
|
848
|
-
if manifest_path.exists() and not force:
|
|
849
|
-
existing.append("manifest.json")
|
|
850
939
|
if config_path.exists() and not force:
|
|
851
940
|
existing.append("nipkg.config.json")
|
|
852
941
|
if existing:
|
|
@@ -860,7 +949,7 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
860
949
|
icon_file, directory, force
|
|
861
950
|
)
|
|
862
951
|
|
|
863
|
-
|
|
952
|
+
pack_config = _build_webapp_pack_config(
|
|
864
953
|
package_name=package_name,
|
|
865
954
|
version=version,
|
|
866
955
|
display_name=display_name,
|
|
@@ -876,11 +965,13 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
876
965
|
build_command=build_command,
|
|
877
966
|
icon_file=manifest_icon_file,
|
|
878
967
|
icon_validation_base_dir=source_icon.parent,
|
|
968
|
+
source_repo=source_repo,
|
|
969
|
+
release_tag=release_tag,
|
|
970
|
+
source_commit=source_commit,
|
|
879
971
|
)
|
|
880
972
|
|
|
881
973
|
copied_icon = _copy_icon_file_to_directory(source_icon, directory)
|
|
882
974
|
try:
|
|
883
|
-
save_json_file(manifest, str(manifest_path))
|
|
884
975
|
save_json_file(pack_config, str(config_path))
|
|
885
976
|
except Exception:
|
|
886
977
|
if copied_icon:
|
|
@@ -888,11 +979,10 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
888
979
|
raise
|
|
889
980
|
|
|
890
981
|
format_success(
|
|
891
|
-
"Created Plugin Manager
|
|
982
|
+
"Created Plugin Manager pack config",
|
|
892
983
|
{
|
|
893
|
-
"Manifest": str(manifest_path),
|
|
894
984
|
"Pack config": str(config_path),
|
|
895
|
-
"
|
|
985
|
+
"Next step": "Run slcli webapp pack --config nipkg.config.json to generate the .nipkg and manifest.json",
|
|
896
986
|
},
|
|
897
987
|
)
|
|
898
988
|
except SystemExit:
|
|
@@ -928,7 +1018,7 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
928
1018
|
) -> None:
|
|
929
1019
|
"""Pack a folder into a .nipkg."""
|
|
930
1020
|
try:
|
|
931
|
-
metadata: Optional[Dict[str,
|
|
1021
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
932
1022
|
resolved_folder = folder
|
|
933
1023
|
|
|
934
1024
|
if config_path is not None:
|
|
@@ -969,7 +1059,14 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
969
1059
|
icon_source = _resolve_local_path(metadata["iconFile"], config_path.parent)
|
|
970
1060
|
|
|
971
1061
|
result = _pack_folder_to_nipkg(resolved_folder, out, metadata, icon_source)
|
|
972
|
-
|
|
1062
|
+
success_data: Dict[str, str] = {"Path": str(result)}
|
|
1063
|
+
if metadata is not None and config_path is not None:
|
|
1064
|
+
manifest_path = config_path.parent / "manifest.json"
|
|
1065
|
+
submission_manifest = _build_submission_manifest(result, metadata)
|
|
1066
|
+
save_json_file(submission_manifest, str(manifest_path))
|
|
1067
|
+
success_data["Manifest"] = str(manifest_path)
|
|
1068
|
+
|
|
1069
|
+
format_success("Packed folder", success_data)
|
|
973
1070
|
except SystemExit:
|
|
974
1071
|
raise
|
|
975
1072
|
except Exception as exc:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/demo-complete-workflow/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/demo-complete-workflow/config.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/exercise-7-1-test-plans/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/exercise-7-1-test-plans/config.yaml
RENAMED
|
File without changes
|
{systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/config.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.6.1 → systemlink_cli-1.6.3}/slcli/skills/slcli/references/analysis-recipes.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|