systemlink-cli 1.5.0__tar.gz → 1.5.2__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.5.0 → systemlink_cli-1.5.2}/PKG-INFO +1 -1
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/pyproject.toml +1 -1
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/_version.py +1 -1
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/skills/slcli/SKILL.md +3 -2
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/skills/systemlink-webapp/SKILL.md +33 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/skills/systemlink-webapp/references/layout-patterns.md +28 -1
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/webapp_click.py +120 -18
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/LICENSE +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/dff-editor/editor.js +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/dff-editor/index.html +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/__init__.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/__main__.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/asset_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/cli_formatters.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/cli_utils.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/comment_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/completion_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/config.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/config_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/dff_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/dff_decorators.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/example_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/example_loader.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/example_provisioner.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/README.md +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/_schema/schema-v1.0.json +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/demo-complete-workflow/README.md +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/demo-test-plans/README.md +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/demo-test-plans/config.yaml +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/feed_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/file_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/function_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/function_templates.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/main.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/mcp_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/mcp_server.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/notebook_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/platform.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/policy_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/policy_utils.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/profiles.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/response_handlers.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/routine_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/skill_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/skills/slcli/references/filtering.md +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/skills/systemlink-webapp/references/nimble-angular.md +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/ssl_trust.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/system_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/table_utils.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/tag_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/templates_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/testmonitor_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/universal_handlers.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/user_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/utils.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/web_editor.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/workflow_preview.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/workflows_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/workitem_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/workspace_click.py +0 -0
- {systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/workspace_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "systemlink-cli"
|
|
3
|
-
version = "1.5.
|
|
3
|
+
version = "1.5.2"
|
|
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" }]
|
|
@@ -682,8 +682,9 @@ conventions described by the `systemlink-webapp` skill.
|
|
|
682
682
|
|
|
683
683
|
`webapp manifest init` writes `manifest.json` and `nipkg.config.json` using the Plugin Manager
|
|
684
684
|
field names (`section`, `maintainer`, `homepage`, `xbPlugin`, `slPluginManagerTags`,
|
|
685
|
-
`slPluginManagerMinServerVersion`). `webapp pack --config ...` consumes that
|
|
686
|
-
the matching control-file fields into the
|
|
685
|
+
`slPluginManagerMinServerVersion`, `iconFile`). `webapp pack --config ...` consumes that
|
|
686
|
+
metadata, carries the icon into the package, and writes the matching control-file fields into the
|
|
687
|
+
generated `.nipkg`.
|
|
687
688
|
|
|
688
689
|
### skill — AI skill installation
|
|
689
690
|
|
|
@@ -80,6 +80,9 @@ Use Nimble layout tokens and spacing rules consistently across the shell and fea
|
|
|
80
80
|
aligned multi-column layouts.
|
|
81
81
|
- Inside accordion panels, keep a column layout with `mediumPadding` gaps and
|
|
82
82
|
`standardPadding` bottom padding.
|
|
83
|
+
- In dense side panels with tabs, use `15px 30px 30px 15px` on the tab container,
|
|
84
|
+
`20px 0 0 15px` on the active tab panel, let the panel own scrolling, and avoid forcing nested
|
|
85
|
+
content blocks to `height: 100%`.
|
|
83
86
|
- Treat `controlHeight` (32px), `controlSlimHeight` (24px), and `labelHeight` (16px) as the
|
|
84
87
|
baseline sizing tokens for controls and labels.
|
|
85
88
|
- Separate major sections with `largePadding` and subsections with `standardPadding`.
|
|
@@ -380,6 +383,36 @@ If you want compile-time token values in SCSS, you can also import Nimble's toke
|
|
|
380
383
|
}
|
|
381
384
|
```
|
|
382
385
|
|
|
386
|
+
### Tabs in dense side panels
|
|
387
|
+
|
|
388
|
+
When placing Nimble tabs inside a dense side panel or details pane, use the tab wrapper and panel
|
|
389
|
+
as the layout primitives instead of forcing inner content blocks to fill the available height.
|
|
390
|
+
|
|
391
|
+
```scss
|
|
392
|
+
.details-tabs {
|
|
393
|
+
padding: 15px 30px 30px 15px;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.details-tab-panel {
|
|
397
|
+
padding: 20px 0 0 15px;
|
|
398
|
+
overflow: auto;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.details-tab-content {
|
|
402
|
+
display: flex;
|
|
403
|
+
flex-direction: column;
|
|
404
|
+
gap: var(--ni-nimble-medium-padding, 8px);
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
- Use `padding: 15px 30px 30px 15px` on the tab control container.
|
|
409
|
+
- Use `padding: 20px 0 0 15px` on the active tab panel content region.
|
|
410
|
+
- Let the tab panel container handle scrolling.
|
|
411
|
+
- Do not force nested form blocks or content stacks to `height: 100%`; that tends to stretch the
|
|
412
|
+
layout and creates inconsistent vertical spacing between controls.
|
|
413
|
+
- Inside the active tab panel, keep stacked controls on an `8px` gap via
|
|
414
|
+
`var(--ni-nimble-medium-padding, 8px)` unless a tighter layout is explicitly needed.
|
|
415
|
+
|
|
383
416
|
### Why this pattern?
|
|
384
417
|
|
|
385
418
|
1. **Themability** — All colors flow through Nimble's theme-aware tokens. If Nimble changes color ramps or adds dark mode, your app automatically inherits it.
|
|
@@ -63,9 +63,36 @@ Inside accordion item content panels:
|
|
|
63
63
|
- Indent content by the icon width plus padding so it aligns with the header text.
|
|
64
64
|
- Use `standardPadding` (16px) for bottom padding before the next section.
|
|
65
65
|
|
|
66
|
+
## Tabs in side panels
|
|
67
|
+
|
|
68
|
+
When using tabs inside a dense side panel or details pane:
|
|
69
|
+
|
|
70
|
+
- Use `padding: 15px 30px 30px 15px` on the tab control container.
|
|
71
|
+
- Use `padding: 20px 0 0 15px` on the active tab panel content region.
|
|
72
|
+
- Let the tab panel container own scrolling.
|
|
73
|
+
- Avoid forcing nested form or content blocks to `height: 100%`; it tends to distort vertical
|
|
74
|
+
spacing between controls.
|
|
75
|
+
- Inside the active tab panel, keep stacked controls on a `mediumPadding` (8px) gap unless a
|
|
76
|
+
tighter layout is explicitly needed.
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<div style="padding: 15px 30px 30px 15px;">
|
|
80
|
+
<nimble-tabs>
|
|
81
|
+
<nimble-tab id="details">Details</nimble-tab>
|
|
82
|
+
<nimble-tab id="settings">Settings</nimble-tab>
|
|
83
|
+
</nimble-tabs>
|
|
84
|
+
<div
|
|
85
|
+
style="padding: 20px 0 0 15px; overflow: auto; display: flex; flex-direction: column; gap: var(--ni-nimble-medium-padding);"
|
|
86
|
+
>
|
|
87
|
+
<nimble-text-field>Label 1</nimble-text-field>
|
|
88
|
+
<nimble-text-field>Label 2</nimble-text-field>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
```
|
|
92
|
+
|
|
66
93
|
## Section spacing
|
|
67
94
|
|
|
68
95
|
Between major sections or groups of controls:
|
|
69
96
|
|
|
70
97
|
- Use `largePadding` (24px) between distinct content areas.
|
|
71
|
-
- Use `standardPadding` (16px) for subsections within a group.
|
|
98
|
+
- Use `standardPadding` (16px) for subsections within a group.
|
|
@@ -6,6 +6,7 @@ management (list, get, delete, publish, open).
|
|
|
6
6
|
|
|
7
7
|
import io
|
|
8
8
|
import re
|
|
9
|
+
import shutil
|
|
9
10
|
import sys
|
|
10
11
|
import tarfile
|
|
11
12
|
import tempfile
|
|
@@ -40,6 +41,7 @@ _PACKAGE_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
|
|
|
40
41
|
_VERSION_PATTERN = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
|
41
42
|
_MAINTAINER_PATTERN = re.compile(r"^[^<>]+\s<[^<>@\s]+@[^<>@\s]+>$")
|
|
42
43
|
_XB_PLUGIN_VALUES = ("webapp", "notebook", "dashboard", "routine", "bundle")
|
|
44
|
+
_ALLOWED_ICON_EXTENSIONS = frozenset({".svg", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".ico"})
|
|
43
45
|
_MAX_PACKAGE_LENGTH = 100
|
|
44
46
|
_MAX_DISPLAY_NAME_LENGTH = 200
|
|
45
47
|
_MAX_DESCRIPTION_LENGTH = 5000
|
|
@@ -179,6 +181,62 @@ def _default_angular_build_dir(directory: Path) -> str:
|
|
|
179
181
|
return f"dist/{_default_angular_project_name(directory)}/browser"
|
|
180
182
|
|
|
181
183
|
|
|
184
|
+
def _resolve_local_path(path_value: str, base_dir: Optional[Path] = None) -> Path:
|
|
185
|
+
"""Resolve a local path against a base directory or the current working directory."""
|
|
186
|
+
path = Path(path_value).expanduser()
|
|
187
|
+
if not path.is_absolute():
|
|
188
|
+
anchor = base_dir if base_dir is not None else Path.cwd()
|
|
189
|
+
path = anchor / path
|
|
190
|
+
return path.resolve()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _validate_icon_file_value(icon_file: str, base_dir: Optional[Path] = None) -> Optional[str]:
|
|
194
|
+
"""Validate the icon asset referenced by Plugin Manager metadata."""
|
|
195
|
+
if not icon_file:
|
|
196
|
+
return "iconFile is required"
|
|
197
|
+
|
|
198
|
+
resolved_icon = _resolve_local_path(icon_file, base_dir)
|
|
199
|
+
if not resolved_icon.exists() or not resolved_icon.is_file():
|
|
200
|
+
return f"iconFile does not exist or is not a file: {icon_file}"
|
|
201
|
+
if resolved_icon.suffix.lower() not in _ALLOWED_ICON_EXTENSIONS:
|
|
202
|
+
allowed_extensions = ", ".join(sorted(ext.lstrip(".") for ext in _ALLOWED_ICON_EXTENSIONS))
|
|
203
|
+
return f"iconFile must be one of: {allowed_extensions}"
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _prepare_icon_file_for_directory(
|
|
208
|
+
icon_file: str, directory: Path, force: bool
|
|
209
|
+
) -> tuple[Path, str]:
|
|
210
|
+
"""Validate an icon asset and return its source path plus stored manifest filename."""
|
|
211
|
+
icon_error = _validate_icon_file_value(icon_file)
|
|
212
|
+
if icon_error:
|
|
213
|
+
click.echo(f"✗ {icon_error}", err=True)
|
|
214
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
215
|
+
|
|
216
|
+
source_icon = _resolve_local_path(icon_file)
|
|
217
|
+
target_icon = directory / source_icon.name
|
|
218
|
+
|
|
219
|
+
if target_icon.exists() and target_icon.resolve() != source_icon:
|
|
220
|
+
if not force:
|
|
221
|
+
click.echo(
|
|
222
|
+
f"✗ {target_icon.name} already exists in {directory}. Use --force to overwrite it.",
|
|
223
|
+
err=True,
|
|
224
|
+
)
|
|
225
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
226
|
+
|
|
227
|
+
return source_icon, target_icon.name
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _copy_icon_file_to_directory(source_icon: Path, directory: Path) -> bool:
|
|
231
|
+
"""Copy an icon asset into the manifest directory when it is not already there."""
|
|
232
|
+
target_icon = directory / source_icon.name
|
|
233
|
+
if target_icon.resolve() == source_icon:
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
shutil.copy2(source_icon, target_icon)
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
|
|
182
240
|
def _normalize_plugin_manager_metadata(raw_metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
183
241
|
"""Normalize legacy App Store keys to Plugin Manager keys."""
|
|
184
242
|
metadata = dict(raw_metadata)
|
|
@@ -191,7 +249,9 @@ def _normalize_plugin_manager_metadata(raw_metadata: Dict[str, Any]) -> Dict[str
|
|
|
191
249
|
|
|
192
250
|
|
|
193
251
|
def _validate_plugin_manager_metadata(
|
|
194
|
-
raw_metadata: Dict[str, Any],
|
|
252
|
+
raw_metadata: Dict[str, Any],
|
|
253
|
+
require_build_dir: bool = False,
|
|
254
|
+
base_dir: Optional[Path] = None,
|
|
195
255
|
) -> Dict[str, str]:
|
|
196
256
|
"""Validate and normalize Plugin Manager manifest metadata."""
|
|
197
257
|
from urllib.parse import urlparse
|
|
@@ -250,6 +310,9 @@ def _validate_plugin_manager_metadata(
|
|
|
250
310
|
errors.append("nipkgFile must end with .nipkg")
|
|
251
311
|
if require_build_dir and not build_dir:
|
|
252
312
|
errors.append("buildDir is required when packing from config without a folder argument")
|
|
313
|
+
icon_error = _validate_icon_file_value(icon_file, base_dir)
|
|
314
|
+
if icon_error:
|
|
315
|
+
errors.append(icon_error)
|
|
253
316
|
|
|
254
317
|
if errors:
|
|
255
318
|
click.echo("✗ Invalid plugin manager metadata:", err=True)
|
|
@@ -278,14 +341,16 @@ def _validate_plugin_manager_metadata(
|
|
|
278
341
|
validated["buildDir"] = build_dir
|
|
279
342
|
if build_command:
|
|
280
343
|
validated["buildCommand"] = build_command
|
|
281
|
-
|
|
282
|
-
validated["iconFile"] = icon_file
|
|
344
|
+
validated["iconFile"] = icon_file
|
|
283
345
|
|
|
284
346
|
return validated
|
|
285
347
|
|
|
286
348
|
|
|
287
349
|
def _pack_folder_to_nipkg(
|
|
288
|
-
folder: Path,
|
|
350
|
+
folder: Path,
|
|
351
|
+
output: Optional[Path] = None,
|
|
352
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
353
|
+
icon_source: Optional[Path] = None,
|
|
289
354
|
) -> Path:
|
|
290
355
|
"""Pack a folder into a .nipkg (ar) file and return the output path.
|
|
291
356
|
|
|
@@ -351,6 +416,8 @@ def _pack_folder_to_nipkg(
|
|
|
351
416
|
control_fields["XB-SlPluginManagerMinServerVersion"] = metadata[
|
|
352
417
|
"slPluginManagerMinServerVersion"
|
|
353
418
|
]
|
|
419
|
+
if metadata.get("iconFile"):
|
|
420
|
+
control_fields["XB-SlPluginManagerIcon"] = Path(metadata["iconFile"]).name
|
|
354
421
|
else:
|
|
355
422
|
control_fields = {
|
|
356
423
|
"Package": package_name,
|
|
@@ -374,10 +441,21 @@ def _pack_folder_to_nipkg(
|
|
|
374
441
|
|
|
375
442
|
# Create data.tar.gz in-memory containing the folder contents at the root
|
|
376
443
|
data_buf = io.BytesIO()
|
|
377
|
-
with
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
444
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
445
|
+
payload_folder = folder
|
|
446
|
+
if metadata is not None and icon_source is not None:
|
|
447
|
+
icon_name = Path(metadata["iconFile"]).name
|
|
448
|
+
folder_icon = folder.resolve() / icon_name
|
|
449
|
+
resolved_icon_source = icon_source.resolve()
|
|
450
|
+
if not folder_icon.exists() or folder_icon.resolve() != resolved_icon_source:
|
|
451
|
+
payload_folder = Path(temp_dir) / folder.name
|
|
452
|
+
shutil.copytree(folder, payload_folder)
|
|
453
|
+
shutil.copy2(resolved_icon_source, payload_folder / icon_name)
|
|
454
|
+
|
|
455
|
+
with tarfile.open(fileobj=data_buf, mode="w:gz") as dtf:
|
|
456
|
+
# tarfile.add will handle directories and files; preserve relative paths
|
|
457
|
+
dtf.add(str(payload_folder), arcname=".")
|
|
458
|
+
data_bytes = data_buf.getvalue()
|
|
381
459
|
|
|
382
460
|
# debian-binary content
|
|
383
461
|
debian_bin = b"2.0\n"
|
|
@@ -467,6 +545,7 @@ def _build_webapp_manifest_and_config(
|
|
|
467
545
|
build_dir: str,
|
|
468
546
|
build_command: str,
|
|
469
547
|
icon_file: str,
|
|
548
|
+
icon_validation_base_dir: Path,
|
|
470
549
|
) -> tuple[Dict[str, str], Dict[str, str]]:
|
|
471
550
|
"""Build validated submission manifest and nipkg config payloads."""
|
|
472
551
|
manifest = _validate_plugin_manager_metadata(
|
|
@@ -483,15 +562,15 @@ def _build_webapp_manifest_and_config(
|
|
|
483
562
|
"slPluginManagerTags": tags,
|
|
484
563
|
"slPluginManagerMinServerVersion": min_server_version,
|
|
485
564
|
"nipkgFile": _default_nipkg_filename(package_name, version),
|
|
486
|
-
|
|
565
|
+
"iconFile": icon_file,
|
|
566
|
+
},
|
|
567
|
+
base_dir=icon_validation_base_dir,
|
|
487
568
|
)
|
|
488
569
|
|
|
489
570
|
pack_config = dict(manifest)
|
|
490
571
|
pack_config.pop("nipkgFile", None)
|
|
491
572
|
pack_config["buildDir"] = build_dir
|
|
492
573
|
pack_config["buildCommand"] = build_command
|
|
493
|
-
if icon_file:
|
|
494
|
-
pack_config["iconFile"] = icon_file
|
|
495
574
|
|
|
496
575
|
return manifest, pack_config
|
|
497
576
|
|
|
@@ -622,7 +701,8 @@ slcli webapp manifest init . \\
|
|
|
622
701
|
--description "A dashboard for monitoring fleet health and calibration status." \\
|
|
623
702
|
--section Dashboard \\
|
|
624
703
|
--maintainer "Your Name <you@example.com>" \\
|
|
625
|
-
--license MIT
|
|
704
|
+
--license MIT \\
|
|
705
|
+
--icon-file ./icon.svg
|
|
626
706
|
|
|
627
707
|
slcli webapp pack --config nipkg.config.json
|
|
628
708
|
```
|
|
@@ -730,7 +810,11 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
730
810
|
show_default=True,
|
|
731
811
|
help="Build command written to nipkg.config.json",
|
|
732
812
|
)
|
|
733
|
-
@click.option(
|
|
813
|
+
@click.option(
|
|
814
|
+
"--icon-file",
|
|
815
|
+
required=True,
|
|
816
|
+
help="Path to the icon asset; copied into the manifest directory as iconFile",
|
|
817
|
+
)
|
|
734
818
|
@click.option("--force", is_flag=True, help="Overwrite existing manifest files")
|
|
735
819
|
def init_manifest(
|
|
736
820
|
directory: Path,
|
|
@@ -772,6 +856,10 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
772
856
|
)
|
|
773
857
|
sys.exit(ExitCodes.INVALID_INPUT)
|
|
774
858
|
|
|
859
|
+
source_icon, manifest_icon_file = _prepare_icon_file_for_directory(
|
|
860
|
+
icon_file, directory, force
|
|
861
|
+
)
|
|
862
|
+
|
|
775
863
|
manifest, pack_config = _build_webapp_manifest_and_config(
|
|
776
864
|
package_name=package_name,
|
|
777
865
|
version=version,
|
|
@@ -786,11 +874,19 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
786
874
|
min_server_version=min_server_version,
|
|
787
875
|
build_dir=build_dir,
|
|
788
876
|
build_command=build_command,
|
|
789
|
-
icon_file=
|
|
877
|
+
icon_file=manifest_icon_file,
|
|
878
|
+
icon_validation_base_dir=source_icon.parent,
|
|
790
879
|
)
|
|
791
880
|
|
|
792
|
-
|
|
793
|
-
|
|
881
|
+
copied_icon = _copy_icon_file_to_directory(source_icon, directory)
|
|
882
|
+
try:
|
|
883
|
+
save_json_file(manifest, str(manifest_path))
|
|
884
|
+
save_json_file(pack_config, str(config_path))
|
|
885
|
+
except Exception:
|
|
886
|
+
if copied_icon:
|
|
887
|
+
(directory / manifest_icon_file).unlink(missing_ok=True)
|
|
888
|
+
raise
|
|
889
|
+
|
|
794
890
|
format_success(
|
|
795
891
|
"Created Plugin Manager manifest files",
|
|
796
892
|
{
|
|
@@ -841,7 +937,9 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
841
937
|
click.echo("✗ Config file must contain a JSON object.", err=True)
|
|
842
938
|
sys.exit(ExitCodes.INVALID_INPUT)
|
|
843
939
|
metadata = _validate_plugin_manager_metadata(
|
|
844
|
-
raw_data,
|
|
940
|
+
raw_data,
|
|
941
|
+
require_build_dir=resolved_folder is None,
|
|
942
|
+
base_dir=config_path.parent,
|
|
845
943
|
)
|
|
846
944
|
|
|
847
945
|
if resolved_folder is None:
|
|
@@ -866,7 +964,11 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
866
964
|
sys.exit(ExitCodes.INVALID_INPUT)
|
|
867
965
|
|
|
868
966
|
out = Path(output) if output else None
|
|
869
|
-
|
|
967
|
+
icon_source: Optional[Path] = None
|
|
968
|
+
if metadata is not None and config_path is not None:
|
|
969
|
+
icon_source = _resolve_local_path(metadata["iconFile"], config_path.parent)
|
|
970
|
+
|
|
971
|
+
result = _pack_folder_to_nipkg(resolved_folder, out, metadata, icon_source)
|
|
870
972
|
format_success("Packed folder", {"Path": str(result)})
|
|
871
973
|
except SystemExit:
|
|
872
974
|
raise
|
|
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.5.0 → systemlink_cli-1.5.2}/slcli/examples/demo-complete-workflow/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/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.5.0 → systemlink_cli-1.5.2}/slcli/examples/exercise-7-1-test-plans/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/exercise-7-1-test-plans/config.yaml
RENAMED
|
File without changes
|
{systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/slcli/examples/spec-compliance-notebooks/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/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
|
{systemlink_cli-1.5.0 → systemlink_cli-1.5.2}/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
|
|
File without changes
|