systemlink-cli 1.4.8__tar.gz → 1.5.0__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.4.8 → systemlink_cli-1.5.0}/PKG-INFO +1 -1
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/pyproject.toml +1 -1
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/_version.py +1 -1
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/skills/slcli/SKILL.md +14 -7
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/skills/systemlink-webapp/SKILL.md +46 -4
- systemlink_cli-1.5.0/slcli/skills/systemlink-webapp/references/layout-patterns.md +71 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/webapp_click.py +528 -120
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/LICENSE +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/dff-editor/editor.js +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/dff-editor/index.html +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/__init__.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/__main__.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/asset_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/cli_formatters.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/cli_utils.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/comment_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/completion_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/config.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/config_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/dff_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/dff_decorators.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/example_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/example_loader.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/example_provisioner.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/README.md +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/_schema/schema-v1.0.json +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/demo-complete-workflow/README.md +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/demo-test-plans/README.md +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/demo-test-plans/config.yaml +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/feed_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/file_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/function_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/function_templates.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/main.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/mcp_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/mcp_server.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/notebook_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/platform.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/policy_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/policy_utils.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/profiles.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/response_handlers.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/routine_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/skill_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/skills/slcli/references/filtering.md +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/skills/systemlink-webapp/references/nimble-angular.md +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/ssl_trust.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/system_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/table_utils.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/tag_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/templates_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/testmonitor_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/universal_handlers.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/user_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/utils.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/web_editor.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/workflow_preview.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/workflows_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/workitem_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/workspace_click.py +0 -0
- {systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/workspace_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "systemlink-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.5.0"
|
|
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" }]
|
|
@@ -665,18 +665,25 @@ slcli workitem create \
|
|
|
665
665
|
Scaffold, package, and publish custom web applications to SystemLink.
|
|
666
666
|
|
|
667
667
|
```bash
|
|
668
|
-
slcli webapp init
|
|
669
|
-
slcli webapp
|
|
668
|
+
slcli webapp init <DIRECTORY> # Scaffold the Angular starter
|
|
669
|
+
slcli webapp manifest init <DIRECTORY> [OPTIONS] # Create manifest.json + nipkg.config.json
|
|
670
|
+
slcli webapp pack [FOLDER] [--config FILE] [-o OUTPUT_FILE] # Package a webapp into a .nipkg
|
|
670
671
|
slcli webapp list [-w WORKSPACE] [-t INT] [-f json]
|
|
671
672
|
slcli webapp get <WEBAPP_ID> [-f json]
|
|
672
|
-
slcli webapp publish
|
|
673
|
+
slcli webapp publish PATH [--workspace NAME] # Upload and publish a webapp
|
|
673
674
|
slcli webapp delete <WEBAPP_ID>
|
|
674
675
|
slcli webapp open <WEBAPP_ID> # Open webapp URL in browser
|
|
675
676
|
```
|
|
676
677
|
|
|
677
|
-
|
|
678
|
-
- `
|
|
679
|
-
|
|
678
|
+
`webapp init` creates the SystemLink Angular starter, not a generic HTML app. The starter installs
|
|
679
|
+
project-scoped skills into `.agents/skills/` and creates `PROMPTS.md` plus `START_HERE.md` so an
|
|
680
|
+
AI assistant can bootstrap the Angular workspace in place with the same Nimble/SystemLink
|
|
681
|
+
conventions described by the `systemlink-webapp` skill.
|
|
682
|
+
|
|
683
|
+
`webapp manifest init` writes `manifest.json` and `nipkg.config.json` using the Plugin Manager
|
|
684
|
+
field names (`section`, `maintainer`, `homepage`, `xbPlugin`, `slPluginManagerTags`,
|
|
685
|
+
`slPluginManagerMinServerVersion`). `webapp pack --config ...` consumes that metadata and writes
|
|
686
|
+
the matching control-file fields into the generated `.nipkg`.
|
|
680
687
|
|
|
681
688
|
### skill — AI skill installation
|
|
682
689
|
|
|
@@ -693,7 +700,7 @@ Client paths:
|
|
|
693
700
|
|
|
694
701
|
Notes:
|
|
695
702
|
- `agents` is the default client in interactive mode.
|
|
696
|
-
- `webapp init
|
|
703
|
+
- `webapp init` installs project-scoped skills into `.agents/skills/` by default.
|
|
697
704
|
|
|
698
705
|
### example — Built-in example resource provisioning
|
|
699
706
|
|
|
@@ -19,6 +19,14 @@ SystemLink webapps are Angular Single-Page Applications built with the Nimble de
|
|
|
19
19
|
connected to SystemLink REST APIs, and deployed via `slcli webapp publish`. This skill captures
|
|
20
20
|
every gotcha learned from building and deploying real apps.
|
|
21
21
|
|
|
22
|
+
If the user is starting from scratch, prefer `slcli webapp init <app-dir>` first.
|
|
23
|
+
That command lays down the SystemLink starter layer (`.agents/skills/`, `PROMPTS.md`, and
|
|
24
|
+
`START_HERE.md`) while Angular CLI remains responsible for generating the Angular workspace.
|
|
25
|
+
|
|
26
|
+
When the user wants to package the app for Plugin Manager submission, prefer
|
|
27
|
+
`slcli webapp manifest init <app-dir> ...` to generate `manifest.json` and `nipkg.config.json`
|
|
28
|
+
with the current Plugin Manager field names, then use `slcli webapp pack --config ...`.
|
|
29
|
+
|
|
22
30
|
---
|
|
23
31
|
|
|
24
32
|
## Step 1: Understand what the user needs
|
|
@@ -34,15 +42,49 @@ You do NOT need to ask about Angular version or Nimble versions — always use A
|
|
|
34
42
|
|
|
35
43
|
---
|
|
36
44
|
|
|
37
|
-
## Step 2:
|
|
45
|
+
## Step 2: Bootstrap the Angular workspace
|
|
46
|
+
|
|
47
|
+
When the project was created with `slcli webapp init`, generate Angular in the existing starter
|
|
48
|
+
directory so the starter files and bundled skills remain at the project root.
|
|
38
49
|
|
|
39
50
|
```bash
|
|
40
|
-
npx -y @angular/cli@20 new <app-name> --routing --style=scss --skip-git --no-standalone
|
|
41
|
-
cd <app-name>
|
|
51
|
+
npx -y @angular/cli@20 new <app-name> --directory . --routing --style=scss --skip-git --no-standalone --defaults --force
|
|
42
52
|
npm install @ni/nimble-angular
|
|
43
53
|
```
|
|
44
54
|
|
|
45
|
-
> Use `--no-standalone` to generate an NgModule-based app. SystemLink webapps work best with
|
|
55
|
+
> Use `--no-standalone` to generate an NgModule-based app. SystemLink webapps work best with
|
|
56
|
+
> NgModule because it makes it easy to register all Nimble modules in one place.
|
|
57
|
+
|
|
58
|
+
If the user has not run `slcli webapp init` yet and they want a new SystemLink webapp, tell them
|
|
59
|
+
to do that first unless they explicitly want a manual setup.
|
|
60
|
+
|
|
61
|
+
### Starter shell expectations
|
|
62
|
+
|
|
63
|
+
Before building feature-specific pages, establish a reusable shell that is aligned with other
|
|
64
|
+
SystemLink apps:
|
|
65
|
+
|
|
66
|
+
- Root `nimble-theme-provider` that mirrors the host shell theme
|
|
67
|
+
- Responsive page header with title, summary text, and an action area
|
|
68
|
+
- Shared loading, error, and empty states instead of one-off page-specific handling
|
|
69
|
+
- Route-backed top-level navigation only when the app truly has multiple views
|
|
70
|
+
- Reusable API helpers and service-layer code rather than fetch logic embedded in templates
|
|
71
|
+
|
|
72
|
+
Use Nimble layout tokens and spacing rules consistently across the shell and feature pages:
|
|
73
|
+
|
|
74
|
+
- Prefer Nimble spacing tokens over ad-hoc pixel values: `smallPadding` for tight inline gaps,
|
|
75
|
+
`mediumPadding` for default control spacing, `standardPadding` for section padding, and
|
|
76
|
+
`largePadding` between major content regions.
|
|
77
|
+
- Stack controls vertically with a column layout, `mediumPadding` gap, and `standardPadding`
|
|
78
|
+
around the control group.
|
|
79
|
+
- Use `mediumPadding` or `standardPadding` gaps for side-by-side controls; prefer CSS grid for
|
|
80
|
+
aligned multi-column layouts.
|
|
81
|
+
- Inside accordion panels, keep a column layout with `mediumPadding` gaps and
|
|
82
|
+
`standardPadding` bottom padding.
|
|
83
|
+
- Treat `controlHeight` (32px), `controlSlimHeight` (24px), and `labelHeight` (16px) as the
|
|
84
|
+
baseline sizing tokens for controls and labels.
|
|
85
|
+
- Separate major sections with `largePadding` and subsections with `standardPadding`.
|
|
86
|
+
|
|
87
|
+
See [references/layout-patterns.md](references/layout-patterns.md) for the detailed layout guide.
|
|
46
88
|
|
|
47
89
|
---
|
|
48
90
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Layout Patterns
|
|
2
|
+
|
|
3
|
+
Guidance for spacing between controls vertically and horizontally within Nimble-based
|
|
4
|
+
SystemLink webapps.
|
|
5
|
+
|
|
6
|
+
## Spacing tokens
|
|
7
|
+
|
|
8
|
+
Use Nimble design tokens for consistent spacing between controls.
|
|
9
|
+
|
|
10
|
+
| Token | Value | Usage |
|
|
11
|
+
| ----------------- | ----- | ------------------------------------------------------ |
|
|
12
|
+
| `smallPadding` | 4px | Tight spacing for icon margins and inline element gaps |
|
|
13
|
+
| `mediumPadding` | 8px | Default spacing between stacked controls |
|
|
14
|
+
| `standardPadding` | 16px | Section padding and content block margin |
|
|
15
|
+
| `largePadding` | 24px | Separation between major layout sections |
|
|
16
|
+
|
|
17
|
+
## Control heights
|
|
18
|
+
|
|
19
|
+
| Token | Value | Usage |
|
|
20
|
+
| ------------------- | ----- | ------------------------------- |
|
|
21
|
+
| `controlHeight` | 32px | Standard height for controls |
|
|
22
|
+
| `controlSlimHeight` | 24px | Compact control variants |
|
|
23
|
+
| `labelHeight` | 16px | Height of labels above controls |
|
|
24
|
+
|
|
25
|
+
## Vertical stacking
|
|
26
|
+
|
|
27
|
+
When stacking controls vertically, such as text fields, number fields, and checkboxes:
|
|
28
|
+
|
|
29
|
+
- Use `mediumPadding` (8px) as the gap between controls in a flex column.
|
|
30
|
+
- Use `standardPadding` (16px) for content padding around the group.
|
|
31
|
+
- Labels above controls add `labelHeight` (16px) to the effective row height.
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<div
|
|
35
|
+
style="display: flex; flex-direction: column; gap: var(--ni-nimble-medium-padding);"
|
|
36
|
+
>
|
|
37
|
+
<nimble-text-field>Label 1</nimble-text-field>
|
|
38
|
+
<nimble-text-field>Label 2</nimble-text-field>
|
|
39
|
+
</div>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Horizontal layout
|
|
43
|
+
|
|
44
|
+
When placing controls side by side:
|
|
45
|
+
|
|
46
|
+
- Use `mediumPadding` (8px) or `standardPadding` (16px) as the gap.
|
|
47
|
+
- Prefer CSS grid with equal columns for aligned layouts.
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<div
|
|
51
|
+
style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--ni-nimble-medium-padding);"
|
|
52
|
+
>
|
|
53
|
+
<nimble-checkbox>Option A</nimble-checkbox>
|
|
54
|
+
<nimble-checkbox>Option B</nimble-checkbox>
|
|
55
|
+
</div>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Accordion content
|
|
59
|
+
|
|
60
|
+
Inside accordion item content panels:
|
|
61
|
+
|
|
62
|
+
- Use `flex-direction: column` with `mediumPadding` (8px) gap between controls.
|
|
63
|
+
- Indent content by the icon width plus padding so it aligns with the header text.
|
|
64
|
+
- Use `standardPadding` (16px) for bottom padding before the next section.
|
|
65
|
+
|
|
66
|
+
## Section spacing
|
|
67
|
+
|
|
68
|
+
Between major sections or groups of controls:
|
|
69
|
+
|
|
70
|
+
- Use `largePadding` (24px) between distinct content areas.
|
|
71
|
+
- Use `standardPadding` (16px) for subsections within a group.
|
|
@@ -5,6 +5,7 @@ management (list, get, delete, publish, open).
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import io
|
|
8
|
+
import re
|
|
8
9
|
import sys
|
|
9
10
|
import tarfile
|
|
10
11
|
import tempfile
|
|
@@ -25,14 +26,49 @@ from .utils import (
|
|
|
25
26
|
get_base_url,
|
|
26
27
|
get_web_url,
|
|
27
28
|
get_headers,
|
|
29
|
+
load_json_file,
|
|
28
30
|
get_ssl_verify,
|
|
29
31
|
get_workspace_id_with_fallback,
|
|
30
32
|
get_workspace_map,
|
|
31
33
|
handle_api_error,
|
|
32
34
|
sanitize_filename,
|
|
35
|
+
save_json_file,
|
|
33
36
|
)
|
|
34
37
|
from .workspace_utils import get_effective_workspace, get_workspace_display_name
|
|
35
38
|
|
|
39
|
+
_PACKAGE_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
|
|
40
|
+
_VERSION_PATTERN = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
|
41
|
+
_MAINTAINER_PATTERN = re.compile(r"^[^<>]+\s<[^<>@\s]+@[^<>@\s]+>$")
|
|
42
|
+
_XB_PLUGIN_VALUES = ("webapp", "notebook", "dashboard", "routine", "bundle")
|
|
43
|
+
_MAX_PACKAGE_LENGTH = 100
|
|
44
|
+
_MAX_DISPLAY_NAME_LENGTH = 200
|
|
45
|
+
_MAX_DESCRIPTION_LENGTH = 5000
|
|
46
|
+
_ALLOWED_PLUGIN_MANAGER_KEYS = {
|
|
47
|
+
"buildCommand",
|
|
48
|
+
"buildDir",
|
|
49
|
+
"description",
|
|
50
|
+
"displayName",
|
|
51
|
+
"homepage",
|
|
52
|
+
"iconFile",
|
|
53
|
+
"license",
|
|
54
|
+
"maintainer",
|
|
55
|
+
"nipkgFile",
|
|
56
|
+
"package",
|
|
57
|
+
"section",
|
|
58
|
+
"slPluginManagerMinServerVersion",
|
|
59
|
+
"slPluginManagerTags",
|
|
60
|
+
"version",
|
|
61
|
+
"xbPlugin",
|
|
62
|
+
}
|
|
63
|
+
_LEGACY_MANIFEST_KEY_MAP = {
|
|
64
|
+
"appStoreCategory": "section",
|
|
65
|
+
"appStoreType": "xbPlugin",
|
|
66
|
+
"appStoreAuthor": "maintainer",
|
|
67
|
+
"appStoreRepo": "homepage",
|
|
68
|
+
"appStoreTags": "slPluginManagerTags",
|
|
69
|
+
"appStoreMinServerVersion": "slPluginManagerMinServerVersion",
|
|
70
|
+
}
|
|
71
|
+
|
|
36
72
|
|
|
37
73
|
def _get_webapp_base_url() -> str:
|
|
38
74
|
return f"{get_base_url()}/niapp/v1"
|
|
@@ -127,7 +163,130 @@ def _fetch_webapps_page(
|
|
|
127
163
|
return items, cont, total
|
|
128
164
|
|
|
129
165
|
|
|
130
|
-
def
|
|
166
|
+
def _default_nipkg_filename(package_name: str, version: str) -> str:
|
|
167
|
+
"""Return the canonical Plugin Manager package filename."""
|
|
168
|
+
return f"{package_name}_{version}_all.nipkg"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _default_display_name(package_name: str) -> str:
|
|
172
|
+
"""Return a display name derived from a package identifier."""
|
|
173
|
+
words = re.split(r"[._-]+", package_name)
|
|
174
|
+
return " ".join(word.capitalize() for word in words if word)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _default_angular_build_dir(directory: Path) -> str:
|
|
178
|
+
"""Return the default Angular production output path for a starter directory."""
|
|
179
|
+
return f"dist/{_default_angular_project_name(directory)}/browser"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _normalize_plugin_manager_metadata(raw_metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
183
|
+
"""Normalize legacy App Store keys to Plugin Manager keys."""
|
|
184
|
+
metadata = dict(raw_metadata)
|
|
185
|
+
for old_key, new_key in _LEGACY_MANIFEST_KEY_MAP.items():
|
|
186
|
+
if old_key in metadata and new_key not in metadata:
|
|
187
|
+
metadata[new_key] = metadata[old_key]
|
|
188
|
+
for old_key in _LEGACY_MANIFEST_KEY_MAP:
|
|
189
|
+
metadata.pop(old_key, None)
|
|
190
|
+
return metadata
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _validate_plugin_manager_metadata(
|
|
194
|
+
raw_metadata: Dict[str, Any], require_build_dir: bool = False
|
|
195
|
+
) -> Dict[str, str]:
|
|
196
|
+
"""Validate and normalize Plugin Manager manifest metadata."""
|
|
197
|
+
from urllib.parse import urlparse
|
|
198
|
+
|
|
199
|
+
metadata = _normalize_plugin_manager_metadata(raw_metadata)
|
|
200
|
+
unexpected_keys = sorted(set(metadata) - _ALLOWED_PLUGIN_MANAGER_KEYS)
|
|
201
|
+
|
|
202
|
+
package_name = str(metadata.get("package", "")).strip()
|
|
203
|
+
version = str(metadata.get("version", "")).strip()
|
|
204
|
+
display_name = str(metadata.get("displayName", "")).strip()
|
|
205
|
+
description = str(metadata.get("description", "")).strip()
|
|
206
|
+
section = str(metadata.get("section", "")).strip()
|
|
207
|
+
maintainer = str(metadata.get("maintainer", "")).strip()
|
|
208
|
+
homepage = str(metadata.get("homepage", "")).strip()
|
|
209
|
+
license_name = str(metadata.get("license", "")).strip()
|
|
210
|
+
xb_plugin = str(metadata.get("xbPlugin", "")).strip()
|
|
211
|
+
tags = str(metadata.get("slPluginManagerTags", "")).strip()
|
|
212
|
+
min_server_version = str(metadata.get("slPluginManagerMinServerVersion", "")).strip()
|
|
213
|
+
nipkg_file = str(metadata.get("nipkgFile", "")).strip()
|
|
214
|
+
build_dir = str(metadata.get("buildDir", "")).strip()
|
|
215
|
+
build_command = str(metadata.get("buildCommand", "")).strip()
|
|
216
|
+
icon_file = str(metadata.get("iconFile", "")).strip()
|
|
217
|
+
|
|
218
|
+
errors: List[str] = []
|
|
219
|
+
|
|
220
|
+
if unexpected_keys:
|
|
221
|
+
errors.append("unexpected field(s): " + ", ".join(unexpected_keys))
|
|
222
|
+
|
|
223
|
+
if not package_name or not _PACKAGE_PATTERN.match(package_name) or len(package_name) < 3:
|
|
224
|
+
errors.append("package must match ^[a-z0-9][a-z0-9._-]*$ and be at least 3 characters")
|
|
225
|
+
elif len(package_name) > _MAX_PACKAGE_LENGTH:
|
|
226
|
+
errors.append(f"package must be at most {_MAX_PACKAGE_LENGTH} characters")
|
|
227
|
+
if not version or not _VERSION_PATTERN.match(version):
|
|
228
|
+
errors.append("version must be strict semver in MAJOR.MINOR.PATCH format")
|
|
229
|
+
if not display_name or len(display_name) < 3:
|
|
230
|
+
errors.append("displayName must be at least 3 characters")
|
|
231
|
+
elif len(display_name) > _MAX_DISPLAY_NAME_LENGTH:
|
|
232
|
+
errors.append(f"displayName must be at most {_MAX_DISPLAY_NAME_LENGTH} characters")
|
|
233
|
+
if not description or len(description) < 20:
|
|
234
|
+
errors.append("description must be at least 20 characters")
|
|
235
|
+
elif len(description) > _MAX_DESCRIPTION_LENGTH:
|
|
236
|
+
errors.append(f"description must be at most {_MAX_DESCRIPTION_LENGTH} characters")
|
|
237
|
+
if not section or len(section) < 2:
|
|
238
|
+
errors.append("section must be at least 2 characters")
|
|
239
|
+
if not maintainer or not _MAINTAINER_PATTERN.match(maintainer):
|
|
240
|
+
errors.append("maintainer must be in the format 'Name <email@example.com>'")
|
|
241
|
+
if homepage:
|
|
242
|
+
parsed_homepage = urlparse(homepage)
|
|
243
|
+
if not parsed_homepage.scheme or not parsed_homepage.netloc:
|
|
244
|
+
errors.append("homepage must be a valid absolute URI")
|
|
245
|
+
if not license_name or len(license_name) < 2:
|
|
246
|
+
errors.append("license must be at least 2 characters")
|
|
247
|
+
if xb_plugin not in _XB_PLUGIN_VALUES:
|
|
248
|
+
errors.append(f"xbPlugin must be one of: {', '.join(_XB_PLUGIN_VALUES)}")
|
|
249
|
+
if nipkg_file and not nipkg_file.endswith(".nipkg"):
|
|
250
|
+
errors.append("nipkgFile must end with .nipkg")
|
|
251
|
+
if require_build_dir and not build_dir:
|
|
252
|
+
errors.append("buildDir is required when packing from config without a folder argument")
|
|
253
|
+
|
|
254
|
+
if errors:
|
|
255
|
+
click.echo("✗ Invalid plugin manager metadata:", err=True)
|
|
256
|
+
for error in errors:
|
|
257
|
+
click.echo(f" - {error}", err=True)
|
|
258
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
259
|
+
|
|
260
|
+
validated: Dict[str, str] = {
|
|
261
|
+
"package": package_name,
|
|
262
|
+
"version": version,
|
|
263
|
+
"displayName": display_name,
|
|
264
|
+
"description": description,
|
|
265
|
+
"section": section,
|
|
266
|
+
"maintainer": maintainer,
|
|
267
|
+
"license": license_name,
|
|
268
|
+
"xbPlugin": xb_plugin,
|
|
269
|
+
"nipkgFile": nipkg_file or _default_nipkg_filename(package_name, version),
|
|
270
|
+
}
|
|
271
|
+
if homepage:
|
|
272
|
+
validated["homepage"] = homepage
|
|
273
|
+
if tags:
|
|
274
|
+
validated["slPluginManagerTags"] = tags
|
|
275
|
+
if min_server_version:
|
|
276
|
+
validated["slPluginManagerMinServerVersion"] = min_server_version
|
|
277
|
+
if build_dir:
|
|
278
|
+
validated["buildDir"] = build_dir
|
|
279
|
+
if build_command:
|
|
280
|
+
validated["buildCommand"] = build_command
|
|
281
|
+
if icon_file:
|
|
282
|
+
validated["iconFile"] = icon_file
|
|
283
|
+
|
|
284
|
+
return validated
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _pack_folder_to_nipkg(
|
|
288
|
+
folder: Path, output: Optional[Path] = None, metadata: Optional[Dict[str, str]] = None
|
|
289
|
+
) -> Path:
|
|
131
290
|
"""Pack a folder into a .nipkg (ar) file and return the output path.
|
|
132
291
|
|
|
133
292
|
The .nipkg produced by this helper uses a Debian-style ar layout with
|
|
@@ -140,8 +299,28 @@ def _pack_folder_to_nipkg(folder: Path, output: Optional[Path] = None) -> Path:
|
|
|
140
299
|
if not folder.exists() or not folder.is_dir():
|
|
141
300
|
raise click.ClickException(f"Folder not found: {folder}")
|
|
142
301
|
|
|
302
|
+
if metadata is not None:
|
|
303
|
+
package_name = metadata["package"]
|
|
304
|
+
version = metadata["version"]
|
|
305
|
+
architecture = "all"
|
|
306
|
+
else:
|
|
307
|
+
package_name = sanitize_filename(folder.name)
|
|
308
|
+
version = "1.0.0"
|
|
309
|
+
architecture = "all"
|
|
310
|
+
if "_" in folder.name:
|
|
311
|
+
first, rest = folder.name.split("_", 1)
|
|
312
|
+
package_name = sanitize_filename(first)
|
|
313
|
+
rest_parts = rest.split("_")
|
|
314
|
+
if rest_parts:
|
|
315
|
+
version = rest_parts[0]
|
|
316
|
+
if len(rest_parts) > 1:
|
|
317
|
+
architecture = "_".join(rest_parts[1:])
|
|
318
|
+
|
|
143
319
|
if output is None:
|
|
144
|
-
|
|
320
|
+
if metadata is not None:
|
|
321
|
+
output = folder.parent / metadata["nipkgFile"]
|
|
322
|
+
else:
|
|
323
|
+
output = folder.with_suffix(".nipkg")
|
|
145
324
|
|
|
146
325
|
# Ensure parent exists
|
|
147
326
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -150,26 +329,36 @@ def _pack_folder_to_nipkg(folder: Path, output: Optional[Path] = None) -> Path:
|
|
|
150
329
|
# - control.tar.gz (contains a control file with package metadata)
|
|
151
330
|
# - data.tar.gz (contains the payload files)
|
|
152
331
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"
|
|
168
|
-
|
|
169
|
-
"
|
|
170
|
-
|
|
171
|
-
"
|
|
172
|
-
|
|
332
|
+
if metadata is not None:
|
|
333
|
+
control_fields = {
|
|
334
|
+
"Package": metadata["package"],
|
|
335
|
+
"Version": metadata["version"],
|
|
336
|
+
"Architecture": architecture,
|
|
337
|
+
"Description": metadata["description"],
|
|
338
|
+
"Section": metadata["section"],
|
|
339
|
+
"Maintainer": metadata["maintainer"],
|
|
340
|
+
"XB-DisplayName": metadata["displayName"],
|
|
341
|
+
"XB-DisplayVersion": metadata["version"],
|
|
342
|
+
"XB-Plugin": metadata["xbPlugin"],
|
|
343
|
+
"XB-UserVisible": "yes",
|
|
344
|
+
"XB-SlPluginManagerLicense": metadata["license"],
|
|
345
|
+
}
|
|
346
|
+
if metadata.get("homepage"):
|
|
347
|
+
control_fields["Homepage"] = metadata["homepage"]
|
|
348
|
+
if metadata.get("slPluginManagerTags"):
|
|
349
|
+
control_fields["XB-SlPluginManagerTags"] = metadata["slPluginManagerTags"]
|
|
350
|
+
if metadata.get("slPluginManagerMinServerVersion"):
|
|
351
|
+
control_fields["XB-SlPluginManagerMinServerVersion"] = metadata[
|
|
352
|
+
"slPluginManagerMinServerVersion"
|
|
353
|
+
]
|
|
354
|
+
else:
|
|
355
|
+
control_fields = {
|
|
356
|
+
"Package": package_name,
|
|
357
|
+
"Version": version,
|
|
358
|
+
"Architecture": architecture,
|
|
359
|
+
"Maintainer": "slcli <no-reply@example.com>",
|
|
360
|
+
"Description": f"Package created by slcli for {package_name}",
|
|
361
|
+
}
|
|
173
362
|
|
|
174
363
|
control_lines = [f"{k}: {v}" for k, v in control_fields.items()]
|
|
175
364
|
control_content = ("\n".join(control_lines) + "\n").encode("utf-8")
|
|
@@ -248,79 +437,123 @@ def _pack_folder_to_nipkg(folder: Path, output: Optional[Path] = None) -> Path:
|
|
|
248
437
|
# ── Template scaffolding helpers ──────────────────────────────────────────
|
|
249
438
|
|
|
250
439
|
|
|
251
|
-
def
|
|
252
|
-
"""
|
|
253
|
-
directory.
|
|
254
|
-
|
|
255
|
-
target_folder.mkdir(parents=True, exist_ok=True)
|
|
256
|
-
index = target_folder / "index.html"
|
|
257
|
-
if index.exists() and not force:
|
|
258
|
-
click.echo("✗ app/index.html already exists. Use --force to overwrite.", err=True)
|
|
259
|
-
sys.exit(ExitCodes.INVALID_INPUT)
|
|
440
|
+
def _default_angular_project_name(directory: Path) -> str:
|
|
441
|
+
"""Return a safe Angular project name derived from the target directory."""
|
|
442
|
+
project_name = sanitize_filename(directory.name)
|
|
443
|
+
return project_name or "systemlink-webapp"
|
|
260
444
|
|
|
261
|
-
content = """<!doctype html>
|
|
262
|
-
<html>
|
|
263
|
-
<head>
|
|
264
|
-
<meta charset="utf-8">
|
|
265
|
-
<title>Example WebApp</title>
|
|
266
|
-
</head>
|
|
267
|
-
<body>
|
|
268
|
-
<h1>Example WebApp</h1>
|
|
269
|
-
<p>Created with slcli webapp init</p>
|
|
270
|
-
</body>
|
|
271
|
-
</html>
|
|
272
|
-
"""
|
|
273
|
-
index.write_text(content, encoding="utf-8")
|
|
274
|
-
format_success("Created example index.html", {"Path": str(index)})
|
|
275
445
|
|
|
446
|
+
def _build_angular_bootstrap_command(directory: Path) -> str:
|
|
447
|
+
"""Build the canonical Angular CLI command for this starter directory."""
|
|
448
|
+
project_name = _default_angular_project_name(directory)
|
|
449
|
+
return (
|
|
450
|
+
f"npx -y @angular/cli@20 new {project_name} --directory . "
|
|
451
|
+
"--routing --style=scss --skip-git --no-standalone --defaults --force"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _build_webapp_manifest_and_config(
|
|
456
|
+
package_name: str,
|
|
457
|
+
version: str,
|
|
458
|
+
display_name: str,
|
|
459
|
+
description: str,
|
|
460
|
+
section: str,
|
|
461
|
+
maintainer: str,
|
|
462
|
+
homepage: str,
|
|
463
|
+
license_name: str,
|
|
464
|
+
xb_plugin: str,
|
|
465
|
+
tags: str,
|
|
466
|
+
min_server_version: str,
|
|
467
|
+
build_dir: str,
|
|
468
|
+
build_command: str,
|
|
469
|
+
icon_file: str,
|
|
470
|
+
) -> tuple[Dict[str, str], Dict[str, str]]:
|
|
471
|
+
"""Build validated submission manifest and nipkg config payloads."""
|
|
472
|
+
manifest = _validate_plugin_manager_metadata(
|
|
473
|
+
{
|
|
474
|
+
"package": package_name,
|
|
475
|
+
"version": version,
|
|
476
|
+
"displayName": display_name,
|
|
477
|
+
"description": description,
|
|
478
|
+
"section": section,
|
|
479
|
+
"maintainer": maintainer,
|
|
480
|
+
"homepage": homepage,
|
|
481
|
+
"license": license_name,
|
|
482
|
+
"xbPlugin": xb_plugin,
|
|
483
|
+
"slPluginManagerTags": tags,
|
|
484
|
+
"slPluginManagerMinServerVersion": min_server_version,
|
|
485
|
+
"nipkgFile": _default_nipkg_filename(package_name, version),
|
|
486
|
+
}
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
pack_config = dict(manifest)
|
|
490
|
+
pack_config.pop("nipkgFile", None)
|
|
491
|
+
pack_config["buildDir"] = build_dir
|
|
492
|
+
pack_config["buildCommand"] = build_command
|
|
493
|
+
if icon_file:
|
|
494
|
+
pack_config["iconFile"] = icon_file
|
|
495
|
+
|
|
496
|
+
return manifest, pack_config
|
|
276
497
|
|
|
277
|
-
_ANGULAR_PROMPTS_MD = """\
|
|
278
|
-
# SystemLink WebApp — AI Prompts
|
|
279
498
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
and deployment. Just describe what you want.
|
|
499
|
+
def _render_angular_prompts_md(directory: Path) -> str:
|
|
500
|
+
"""Render the prompt file for the Angular starter."""
|
|
501
|
+
bootstrap_command = _build_angular_bootstrap_command(directory)
|
|
502
|
+
return f"""# SystemLink WebApp - AI Prompts
|
|
285
503
|
|
|
286
|
-
|
|
504
|
+
This project was initialized with `slcli webapp init`.
|
|
505
|
+
The bundled `systemlink-webapp` and `slcli` skills are already installed into
|
|
506
|
+
this project so your AI assistant can scaffold the Angular workspace and apply
|
|
507
|
+
the SystemLink-specific conventions immediately.
|
|
287
508
|
|
|
288
|
-
|
|
509
|
+
## Starter Prompt
|
|
289
510
|
|
|
290
|
-
|
|
291
|
-
> It should show which systems are online, recent test results, and
|
|
292
|
-
> any assets due for calibration."
|
|
511
|
+
Use this prompt first when the directory still only contains starter files:
|
|
293
512
|
|
|
294
|
-
|
|
295
|
-
|
|
513
|
+
> "Bootstrap this directory into a maintainable Angular 20 SystemLink webapp.
|
|
514
|
+
> Run `{bootstrap_command}` to generate the Angular workspace in place, then
|
|
515
|
+
> install `@ni/nimble-angular` and `@ni/systemlink-clients-ts`. Create a
|
|
516
|
+
> reusable app shell aligned with other SystemLink apps: `nimble-theme-provider`
|
|
517
|
+
> at the root, a responsive page header, content regions for summary cards and
|
|
518
|
+
> tables, and shared loading, error, and empty states. Keep the app NgModule-
|
|
519
|
+
> based, configure `APP_BASE_HREF`, remove the `<base>` tag, use hash routing,
|
|
520
|
+
> disable `inlineCritical` in production, import Nimble fonts, and sync the app
|
|
521
|
+
> theme with the host SystemLink shell."
|
|
296
522
|
|
|
297
|
-
##
|
|
523
|
+
## Manual Bootstrap
|
|
298
524
|
|
|
299
|
-
|
|
525
|
+
If you want to do the initial setup yourself before handing the project to AI:
|
|
526
|
+
|
|
527
|
+
```bash
|
|
528
|
+
{bootstrap_command}
|
|
529
|
+
npm install @ni/nimble-angular @ni/systemlink-clients-ts
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
The Angular workspace should be created in this directory, not inside a nested
|
|
533
|
+
subfolder.
|
|
534
|
+
|
|
535
|
+
## Example Feature Prompts
|
|
300
536
|
|
|
301
537
|
### Fleet monitoring
|
|
302
538
|
|
|
303
|
-
> "Build a dashboard that shows all connected systems with their
|
|
304
|
-
>
|
|
305
|
-
>
|
|
539
|
+
> "Build a dashboard that shows all connected systems with their status,
|
|
540
|
+
> operating system, and last check-in time. Highlight systems that have been
|
|
541
|
+
> offline for more than 24 hours."
|
|
306
542
|
|
|
307
543
|
### Test results review
|
|
308
544
|
|
|
309
|
-
> "Create a page where I can browse recent test results, filter by
|
|
310
|
-
>
|
|
311
|
-
> summary of failure rates."
|
|
545
|
+
> "Create a page where I can browse recent test results, filter by status,
|
|
546
|
+
> program name, and workspace, and see a summary of failure rates."
|
|
312
547
|
|
|
313
|
-
### Asset
|
|
548
|
+
### Asset and calibration tracking
|
|
314
549
|
|
|
315
|
-
> "Show
|
|
316
|
-
>
|
|
317
|
-
> click on an asset to see its full details."
|
|
550
|
+
> "Show tracked assets grouped by calibration status. I want overdue and due-
|
|
551
|
+
> soon sections, plus an asset details page with key metadata and history."
|
|
318
552
|
|
|
319
553
|
### Production KPIs
|
|
320
554
|
|
|
321
|
-
> "Build a dashboard with
|
|
322
|
-
>
|
|
323
|
-
> 30 days."
|
|
555
|
+
> "Build a dashboard with first-pass yield, throughput per hour, and a trend
|
|
556
|
+
> chart of failures over the last 30 days."
|
|
324
557
|
|
|
325
558
|
### Build and deploy
|
|
326
559
|
|
|
@@ -333,24 +566,46 @@ Describe your goals — the skill handles the technical details.
|
|
|
333
566
|
- [slcli webapp commands](https://ni-kismet.github.io/systemlink-cli/commands.html#webapp)
|
|
334
567
|
"""
|
|
335
568
|
|
|
336
|
-
_ANGULAR_README_MD = """\
|
|
337
|
-
# SystemLink WebApp
|
|
338
569
|
|
|
339
|
-
|
|
340
|
-
|
|
570
|
+
def _render_angular_start_here_md(directory: Path) -> str:
|
|
571
|
+
"""Render the starter guide for the Angular workflow."""
|
|
572
|
+
bootstrap_command = _build_angular_bootstrap_command(directory)
|
|
573
|
+
return f"""# SystemLink Angular WebApp Starter
|
|
574
|
+
|
|
575
|
+
This directory was initialized with `slcli webapp init`.
|
|
576
|
+
|
|
577
|
+
`slcli` owns the SystemLink-specific starter layer for this workflow:
|
|
578
|
+
|
|
579
|
+
- bundled AI skills in `.agents/skills/`
|
|
580
|
+
- ready-made prompts in [PROMPTS.md](PROMPTS.md)
|
|
581
|
+
- deployment guidance for `slcli webapp publish`
|
|
582
|
+
- Plugin Manager manifest scaffolding via `slcli webapp manifest init`
|
|
583
|
+
|
|
584
|
+
Angular CLI remains the source of truth for the Angular workspace itself. That
|
|
585
|
+
keeps the generated project aligned with current Angular defaults while the
|
|
586
|
+
skills and starter files enforce the SystemLink-specific best practices.
|
|
341
587
|
|
|
342
|
-
##
|
|
588
|
+
## Bootstrap the Angular workspace
|
|
343
589
|
|
|
344
|
-
|
|
345
|
-
|
|
590
|
+
```bash
|
|
591
|
+
{bootstrap_command}
|
|
592
|
+
npm install @ni/nimble-angular @ni/systemlink-clients-ts
|
|
593
|
+
```
|
|
346
594
|
|
|
347
|
-
|
|
595
|
+
If you use an AI assistant, ask it to follow the starter prompt in
|
|
596
|
+
[PROMPTS.md](PROMPTS.md). The Angular app should be created in this directory,
|
|
597
|
+
not inside a nested subfolder.
|
|
348
598
|
|
|
349
|
-
|
|
350
|
-
the project — see [PROMPTS.md](PROMPTS.md) for ready-made prompts.
|
|
599
|
+
## Baseline conventions
|
|
351
600
|
|
|
352
|
-
|
|
353
|
-
|
|
601
|
+
- Angular 20 with an NgModule-based app (`--no-standalone`)
|
|
602
|
+
- `@ni/nimble-angular` for UI and design tokens
|
|
603
|
+
- `@ni/systemlink-clients-ts` as the default API integration path
|
|
604
|
+
- `APP_BASE_HREF` provided in DI and no `<base>` tag in `index.html`
|
|
605
|
+
- Hash routing for SystemLink sub-path hosting
|
|
606
|
+
- `inlineCritical: false` in the production build configuration
|
|
607
|
+
- A reusable SystemLink-aligned shell with theme sync, page header, content
|
|
608
|
+
regions, and shared loading, error, and empty states
|
|
354
609
|
|
|
355
610
|
## Deploy to SystemLink
|
|
356
611
|
|
|
@@ -359,22 +614,34 @@ ng build --configuration production
|
|
|
359
614
|
slcli webapp publish dist/<project-name>/browser/ \\
|
|
360
615
|
--name "My Dashboard" --workspace Default
|
|
361
616
|
```
|
|
617
|
+
|
|
618
|
+
## Plugin Manager packaging metadata
|
|
619
|
+
|
|
620
|
+
```bash
|
|
621
|
+
slcli webapp manifest init . \\
|
|
622
|
+
--description "A dashboard for monitoring fleet health and calibration status." \\
|
|
623
|
+
--section Dashboard \\
|
|
624
|
+
--maintainer "Your Name <you@example.com>" \\
|
|
625
|
+
--license MIT
|
|
626
|
+
|
|
627
|
+
slcli webapp pack --config nipkg.config.json
|
|
628
|
+
```
|
|
362
629
|
"""
|
|
363
630
|
|
|
364
631
|
|
|
365
632
|
def _init_angular_template(directory: Path, force: bool) -> None:
|
|
366
|
-
"""Scaffold
|
|
633
|
+
"""Scaffold the SystemLink Angular starter for a new webapp."""
|
|
367
634
|
directory.mkdir(parents=True, exist_ok=True)
|
|
368
635
|
|
|
369
636
|
prompts_file = directory / "PROMPTS.md"
|
|
370
|
-
|
|
637
|
+
start_here_file = directory / "START_HERE.md"
|
|
371
638
|
|
|
372
639
|
# Check for existing files
|
|
373
640
|
existing = []
|
|
374
641
|
if prompts_file.exists() and not force:
|
|
375
642
|
existing.append("PROMPTS.md")
|
|
376
|
-
if
|
|
377
|
-
existing.append("
|
|
643
|
+
if start_here_file.exists() and not force:
|
|
644
|
+
existing.append("START_HERE.md")
|
|
378
645
|
if existing:
|
|
379
646
|
click.echo(
|
|
380
647
|
f"✗ {', '.join(existing)} already exist(s). Use --force to overwrite.",
|
|
@@ -382,22 +649,22 @@ def _init_angular_template(directory: Path, force: bool) -> None:
|
|
|
382
649
|
)
|
|
383
650
|
sys.exit(ExitCodes.INVALID_INPUT)
|
|
384
651
|
|
|
385
|
-
prompts_file.write_text(
|
|
386
|
-
|
|
652
|
+
prompts_file.write_text(_render_angular_prompts_md(directory), encoding="utf-8")
|
|
653
|
+
start_here_file.write_text(_render_angular_start_here_md(directory), encoding="utf-8")
|
|
387
654
|
|
|
388
655
|
# Auto-install AI skills into the project directory
|
|
389
656
|
installed = install_skills_to_directory(directory)
|
|
390
657
|
skill_msg = f"{installed} skill(s) installed" if installed else "skills not found"
|
|
391
658
|
|
|
392
659
|
format_success(
|
|
393
|
-
"Scaffolded
|
|
660
|
+
"Scaffolded SystemLink Angular starter",
|
|
394
661
|
{
|
|
395
662
|
"Directory": str(directory),
|
|
396
663
|
"Skills": skill_msg,
|
|
397
664
|
"Next steps": (
|
|
398
665
|
"1. cd " + str(directory) + "\n"
|
|
399
|
-
" 2. Open
|
|
400
|
-
" 3.
|
|
666
|
+
" 2. Open START_HERE.md and PROMPTS.md\n"
|
|
667
|
+
" 3. Ask AI to bootstrap the Angular workspace in this directory"
|
|
401
668
|
),
|
|
402
669
|
},
|
|
403
670
|
)
|
|
@@ -411,42 +678,148 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
411
678
|
"""Manage web applications (init/pack locally, publish/CRUD remotely)."""
|
|
412
679
|
|
|
413
680
|
@webapp.command(name="init")
|
|
414
|
-
@click.
|
|
415
|
-
"--directory",
|
|
681
|
+
@click.argument(
|
|
416
682
|
"directory",
|
|
417
683
|
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
|
|
418
|
-
default=Path.cwd(),
|
|
419
|
-
show_default="CWD",
|
|
420
|
-
help="Target directory to create the project in",
|
|
421
684
|
)
|
|
685
|
+
@click.option("--force", is_flag=True, help="Overwrite existing starter files")
|
|
686
|
+
def init_webapp(directory: Path, force: bool) -> None:
|
|
687
|
+
"""Scaffold the SystemLink Angular starter for a new webapp."""
|
|
688
|
+
try:
|
|
689
|
+
_init_angular_template(directory, force)
|
|
690
|
+
except SystemExit:
|
|
691
|
+
raise
|
|
692
|
+
except Exception as exc:
|
|
693
|
+
handle_api_error(exc)
|
|
694
|
+
|
|
695
|
+
@webapp.group(name="manifest")
|
|
696
|
+
def webapp_manifest() -> None:
|
|
697
|
+
"""Create Plugin Manager submission manifests and packaging config files."""
|
|
698
|
+
|
|
699
|
+
@webapp_manifest.command(name="init")
|
|
700
|
+
@click.argument(
|
|
701
|
+
"directory",
|
|
702
|
+
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
|
|
703
|
+
)
|
|
704
|
+
@click.option("--package", "package_name", default="", help="Package identifier")
|
|
705
|
+
@click.option("--version", default="0.1.0", show_default=True, help="Semantic version")
|
|
706
|
+
@click.option("--display-name", "display_name", default="", help="Human-readable name")
|
|
707
|
+
@click.option("--description", required=True, help="Plugin description")
|
|
708
|
+
@click.option("--section", required=True, help="Plugin Manager section/category")
|
|
709
|
+
@click.option("--maintainer", required=True, help="Maintainer in 'Name <email>' format")
|
|
710
|
+
@click.option("--homepage", default="", help="Project homepage or source repository URL")
|
|
711
|
+
@click.option("--license", "license_name", required=True, help="License identifier")
|
|
422
712
|
@click.option(
|
|
423
|
-
"--
|
|
424
|
-
"
|
|
425
|
-
type=click.Choice(
|
|
426
|
-
default="
|
|
713
|
+
"--plugin-type",
|
|
714
|
+
"xb_plugin",
|
|
715
|
+
type=click.Choice(list(_XB_PLUGIN_VALUES)),
|
|
716
|
+
default="webapp",
|
|
427
717
|
show_default=True,
|
|
428
|
-
help="
|
|
718
|
+
help="Plugin Manager top-level plugin type",
|
|
429
719
|
)
|
|
430
|
-
@click.option("--
|
|
431
|
-
|
|
432
|
-
""
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
720
|
+
@click.option("--tags", default="", help="Comma-separated Plugin Manager search tags")
|
|
721
|
+
@click.option(
|
|
722
|
+
"--min-server-version",
|
|
723
|
+
default="",
|
|
724
|
+
help="Minimum supported SystemLink server version",
|
|
725
|
+
)
|
|
726
|
+
@click.option("--build-dir", default="", help="Build output directory for nipkg.config.json")
|
|
727
|
+
@click.option(
|
|
728
|
+
"--build-command",
|
|
729
|
+
default="npm run build",
|
|
730
|
+
show_default=True,
|
|
731
|
+
help="Build command written to nipkg.config.json",
|
|
732
|
+
)
|
|
733
|
+
@click.option("--icon-file", default="", help="Icon path written to nipkg.config.json")
|
|
734
|
+
@click.option("--force", is_flag=True, help="Overwrite existing manifest files")
|
|
735
|
+
def init_manifest(
|
|
736
|
+
directory: Path,
|
|
737
|
+
package_name: str,
|
|
738
|
+
version: str,
|
|
739
|
+
display_name: str,
|
|
740
|
+
description: str,
|
|
741
|
+
section: str,
|
|
742
|
+
maintainer: str,
|
|
743
|
+
homepage: str,
|
|
744
|
+
license_name: str,
|
|
745
|
+
xb_plugin: str,
|
|
746
|
+
tags: str,
|
|
747
|
+
min_server_version: str,
|
|
748
|
+
build_dir: str,
|
|
749
|
+
build_command: str,
|
|
750
|
+
icon_file: str,
|
|
751
|
+
force: bool,
|
|
752
|
+
) -> None:
|
|
753
|
+
"""Write manifest.json and nipkg.config.json using the Plugin Manager contract."""
|
|
438
754
|
try:
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
755
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
756
|
+
|
|
757
|
+
package_name = package_name or sanitize_filename(directory.name, "webapp")
|
|
758
|
+
display_name = display_name or _default_display_name(package_name)
|
|
759
|
+
build_dir = build_dir or _default_angular_build_dir(directory)
|
|
760
|
+
|
|
761
|
+
manifest_path = directory / "manifest.json"
|
|
762
|
+
config_path = directory / "nipkg.config.json"
|
|
763
|
+
existing = []
|
|
764
|
+
if manifest_path.exists() and not force:
|
|
765
|
+
existing.append("manifest.json")
|
|
766
|
+
if config_path.exists() and not force:
|
|
767
|
+
existing.append("nipkg.config.json")
|
|
768
|
+
if existing:
|
|
769
|
+
click.echo(
|
|
770
|
+
f"✗ {', '.join(existing)} already exist(s). Use --force to overwrite.",
|
|
771
|
+
err=True,
|
|
772
|
+
)
|
|
773
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
774
|
+
|
|
775
|
+
manifest, pack_config = _build_webapp_manifest_and_config(
|
|
776
|
+
package_name=package_name,
|
|
777
|
+
version=version,
|
|
778
|
+
display_name=display_name,
|
|
779
|
+
description=description,
|
|
780
|
+
section=section,
|
|
781
|
+
maintainer=maintainer,
|
|
782
|
+
homepage=homepage,
|
|
783
|
+
license_name=license_name,
|
|
784
|
+
xb_plugin=xb_plugin,
|
|
785
|
+
tags=tags,
|
|
786
|
+
min_server_version=min_server_version,
|
|
787
|
+
build_dir=build_dir,
|
|
788
|
+
build_command=build_command,
|
|
789
|
+
icon_file=icon_file,
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
save_json_file(manifest, str(manifest_path))
|
|
793
|
+
save_json_file(pack_config, str(config_path))
|
|
794
|
+
format_success(
|
|
795
|
+
"Created Plugin Manager manifest files",
|
|
796
|
+
{
|
|
797
|
+
"Manifest": str(manifest_path),
|
|
798
|
+
"Pack config": str(config_path),
|
|
799
|
+
"nipkgFile": manifest["nipkgFile"],
|
|
800
|
+
},
|
|
801
|
+
)
|
|
443
802
|
except SystemExit:
|
|
444
803
|
raise
|
|
445
804
|
except Exception as exc:
|
|
446
805
|
handle_api_error(exc)
|
|
447
806
|
|
|
448
807
|
@webapp.command(name="pack")
|
|
449
|
-
@click.argument(
|
|
808
|
+
@click.argument(
|
|
809
|
+
"folder",
|
|
810
|
+
required=False,
|
|
811
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
812
|
+
)
|
|
813
|
+
@click.option(
|
|
814
|
+
"--config",
|
|
815
|
+
"config_path",
|
|
816
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
|
|
817
|
+
default=None,
|
|
818
|
+
help=(
|
|
819
|
+
"Path to nipkg.config.json or compatible metadata JSON. "
|
|
820
|
+
"If it does not include buildDir, also pass FOLDER."
|
|
821
|
+
),
|
|
822
|
+
)
|
|
450
823
|
@click.option(
|
|
451
824
|
"--output",
|
|
452
825
|
"output",
|
|
@@ -454,11 +827,46 @@ def register_webapp_commands(cli: Any) -> None:
|
|
|
454
827
|
default=None,
|
|
455
828
|
help="Output .nipkg file path",
|
|
456
829
|
)
|
|
457
|
-
def pack_cmd(
|
|
830
|
+
def pack_cmd(
|
|
831
|
+
folder: Optional[Path], config_path: Optional[Path], output: Optional[Path]
|
|
832
|
+
) -> None:
|
|
458
833
|
"""Pack a folder into a .nipkg."""
|
|
459
834
|
try:
|
|
835
|
+
metadata: Optional[Dict[str, str]] = None
|
|
836
|
+
resolved_folder = folder
|
|
837
|
+
|
|
838
|
+
if config_path is not None:
|
|
839
|
+
raw_data = load_json_file(str(config_path))
|
|
840
|
+
if not isinstance(raw_data, dict):
|
|
841
|
+
click.echo("✗ Config file must contain a JSON object.", err=True)
|
|
842
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
843
|
+
metadata = _validate_plugin_manager_metadata(
|
|
844
|
+
raw_data, require_build_dir=resolved_folder is None
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
if resolved_folder is None:
|
|
848
|
+
build_dir = metadata.get("buildDir", "")
|
|
849
|
+
base_dir = config_path.parent
|
|
850
|
+
resolved_folder = Path(build_dir)
|
|
851
|
+
if not resolved_folder.is_absolute():
|
|
852
|
+
resolved_folder = base_dir / resolved_folder
|
|
853
|
+
resolved_folder = resolved_folder.resolve()
|
|
854
|
+
if not resolved_folder.exists() or not resolved_folder.is_dir():
|
|
855
|
+
click.echo(
|
|
856
|
+
f"✗ buildDir does not exist or is not a directory: {resolved_folder}",
|
|
857
|
+
err=True,
|
|
858
|
+
)
|
|
859
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
860
|
+
|
|
861
|
+
if output is None and metadata.get("nipkgFile"):
|
|
862
|
+
output = config_path.parent / metadata["nipkgFile"]
|
|
863
|
+
|
|
864
|
+
if resolved_folder is None:
|
|
865
|
+
click.echo("✗ Must provide a folder or --config with buildDir.", err=True)
|
|
866
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
867
|
+
|
|
460
868
|
out = Path(output) if output else None
|
|
461
|
-
result = _pack_folder_to_nipkg(
|
|
869
|
+
result = _pack_folder_to_nipkg(resolved_folder, out, metadata)
|
|
462
870
|
format_success("Packed folder", {"Path": str(result)})
|
|
463
871
|
except SystemExit:
|
|
464
872
|
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.4.8 → systemlink_cli-1.5.0}/slcli/examples/demo-complete-workflow/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/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.4.8 → systemlink_cli-1.5.0}/slcli/examples/exercise-7-1-test-plans/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/exercise-7-1-test-plans/config.yaml
RENAMED
|
File without changes
|
{systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/slcli/examples/spec-compliance-notebooks/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.4.8 → systemlink_cli-1.5.0}/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.4.8 → systemlink_cli-1.5.0}/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
|