@smallpen/cli 0.1.0-alpha.1 → 0.1.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/command-contract.mjs +1415 -0
- package/bin/command-tree.mjs +512 -0
- package/bin/design-output.mjs +939 -0
- package/bin/help-topics.mjs +241 -0
- package/bin/help.mjs +477 -363
- package/bin/inspection-output.mjs +825 -0
- package/bin/read-output.mjs +219 -0
- package/bin/result-files.mjs +232 -0
- package/bin/reuse-advice.mjs +73 -0
- package/bin/schema.mjs +1070 -0
- package/bin/smallpen-check.cjs +0 -0
- package/bin/smallpen.mjs +4334 -1208
- package/bin/warning-output.mjs +10 -3
- package/bin/windows-artifacts.mjs +72 -0
- package/bin/workflow.mjs +55 -0
- package/package.json +3 -3
package/bin/warning-output.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// Presentation only: the core warning list and mutation semantics stay unchanged.
|
|
2
|
-
|
|
2
|
+
// validate checks Package validity only; say so, so advice on a valid write
|
|
3
|
+
// does not read as a defect that validate failed to report.
|
|
4
|
+
const ADVISORY_NOTE =
|
|
5
|
+
"Design advice only: the write is valid and was not gated. validate does not repeat this advice.";
|
|
6
|
+
|
|
7
|
+
export function warningOutput(warnings, detail, { confirmUnmatched = false, suppressed } = {}) {
|
|
3
8
|
const rank = (warning) => warning.match === "exact"
|
|
4
9
|
? 0
|
|
5
10
|
: warning.suggestions.length > 0 ? 1 : 2;
|
|
@@ -16,6 +21,8 @@ export function warningOutput(warnings, detail, { confirmUnmatched = false } = {
|
|
|
16
21
|
total: warnings.length,
|
|
17
22
|
counts,
|
|
18
23
|
contextScope: warnings[0]?.contextScope,
|
|
24
|
+
...(suppressed ? { suppressed } : {}),
|
|
25
|
+
...(warnings.length > 0 ? { note: ADVISORY_NOTE } : {}),
|
|
19
26
|
};
|
|
20
27
|
if (detail === "full") {
|
|
21
28
|
return {
|
|
@@ -32,7 +39,7 @@ export function warningOutput(warnings, detail, { confirmUnmatched = false } = {
|
|
|
32
39
|
|
|
33
40
|
const groups = new Map();
|
|
34
41
|
for (const [warningIndex, warning] of warnings.entries()) {
|
|
35
|
-
const { nodeId, operationIndex, value, contextScope, ...shared } = warning;
|
|
42
|
+
const { nodeId, element, operationIndex, value, contextScope, ...shared } = warning;
|
|
36
43
|
const key = JSON.stringify(shared);
|
|
37
44
|
let group = groups.get(key);
|
|
38
45
|
if (!group) {
|
|
@@ -40,7 +47,7 @@ export function warningOutput(warnings, detail, { confirmUnmatched = false } = {
|
|
|
40
47
|
groups.set(key, group);
|
|
41
48
|
}
|
|
42
49
|
group.count += 1;
|
|
43
|
-
group.locations.push({ warningIndex, operationIndex, nodeId, value });
|
|
50
|
+
group.locations.push({ warningIndex, operationIndex, nodeId, ...(element !== undefined ? { element } : {}), value });
|
|
44
51
|
}
|
|
45
52
|
const grouped = [...groups.values()].sort(
|
|
46
53
|
(left, right) => rank(left) - rank(right),
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { SmallPenError } from "@smallpen/core";
|
|
4
|
+
|
|
5
|
+
// Use the Windows ACL API: POSIX mkdir modes do not protect Windows files.
|
|
6
|
+
// Paths are JSON on stdin, never interpolated into executable PowerShell code.
|
|
7
|
+
const script = String.raw`
|
|
8
|
+
$ErrorActionPreference = 'Stop'
|
|
9
|
+
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
10
|
+
$items = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
11
|
+
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
12
|
+
foreach ($item in $items) {
|
|
13
|
+
$path = $item.path
|
|
14
|
+
$attributes = [System.IO.File]::GetAttributes($path)
|
|
15
|
+
if (($attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Reparse point is not a private CLI directory' }
|
|
16
|
+
$existing = [System.IO.Directory]::GetAccessControl($path)
|
|
17
|
+
$owner = $existing.GetOwner([System.Security.Principal.SecurityIdentifier])
|
|
18
|
+
if (-not $item.created -and $owner.Value -ne $sid.Value) { throw 'CLI directory belongs to another Windows user' }
|
|
19
|
+
$rules = $existing.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])
|
|
20
|
+
if ($existing.AreAccessRulesProtected -and $owner.Value -eq $sid.Value -and $rules.Count -eq 1 -and $rules[0].IdentityReference.Value -eq $sid.Value -and $rules[0].AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Allow -and $rules[0].FileSystemRights -eq [System.Security.AccessControl.FileSystemRights]::FullControl -and $rules[0].InheritanceFlags -eq [System.Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit') { continue }
|
|
21
|
+
$security = [System.Security.AccessControl.DirectorySecurity]::new()
|
|
22
|
+
$security.SetOwner($sid)
|
|
23
|
+
$security.SetAccessRuleProtection($true, $false)
|
|
24
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new($sid, [System.Security.AccessControl.FileSystemRights]::FullControl, [System.Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit', [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow)
|
|
25
|
+
$security.AddAccessRule($rule)
|
|
26
|
+
[System.IO.Directory]::SetAccessControl($path, $security)
|
|
27
|
+
$actual = [System.IO.Directory]::GetAccessControl($path)
|
|
28
|
+
if ($actual.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'CLI ACL owner was not set' }
|
|
29
|
+
if (-not $actual.AreAccessRulesProtected) { throw 'CLI ACL still inherits access rules' }
|
|
30
|
+
foreach ($access in $actual.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
|
|
31
|
+
if ($access.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Allow -and $access.IdentityReference.Value -ne $sid.Value) { throw 'CLI directory grants another user access' }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
export function secureWindowsDirectories(items) {
|
|
37
|
+
try {
|
|
38
|
+
execFileSync(
|
|
39
|
+
join(
|
|
40
|
+
process.env.SystemRoot ?? "C:\\Windows",
|
|
41
|
+
"System32",
|
|
42
|
+
"WindowsPowerShell",
|
|
43
|
+
"v1.0",
|
|
44
|
+
"powershell.exe",
|
|
45
|
+
),
|
|
46
|
+
[
|
|
47
|
+
"-NoLogo",
|
|
48
|
+
"-NoProfile",
|
|
49
|
+
"-NonInteractive",
|
|
50
|
+
"-EncodedCommand",
|
|
51
|
+
Buffer.from(script, "utf16le").toString("base64"),
|
|
52
|
+
],
|
|
53
|
+
{
|
|
54
|
+
input: JSON.stringify(items),
|
|
55
|
+
timeout: 10000,
|
|
56
|
+
maxBuffer: 65536,
|
|
57
|
+
windowsHide: true,
|
|
58
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
} catch {
|
|
62
|
+
throw new SmallPenError(
|
|
63
|
+
"artifact_directory_unavailable",
|
|
64
|
+
"Windows could not grant and verify private writable access to the CLI temporary directory",
|
|
65
|
+
{
|
|
66
|
+
path: items.at(-1).path,
|
|
67
|
+
recovery:
|
|
68
|
+
"Use a writable NTFS temporary directory and Windows PowerShell, or choose a retained --output file outside the CLI temporary tree.",
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
package/bin/workflow.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const CLI_RULES = Object.freeze({
|
|
2
|
+
purpose:
|
|
3
|
+
"CLI operation rules. Read the selected command and input schema; pass explicit locators and returned IDs.",
|
|
4
|
+
selection: {
|
|
5
|
+
appStateIndependent: true,
|
|
6
|
+
defaults:
|
|
7
|
+
"No --theme: use each project's group defaults, never the App's active selection. Libraries without stored defaults use the real Default option, else the first option in library order.",
|
|
8
|
+
precedence:
|
|
9
|
+
"Project defaults < explicitly requested Scenario themes < explicit --theme. Override only named groups; unspecified groups keep defaults. Presentation chooses structure and does not implicitly choose a theme.",
|
|
10
|
+
dimensions:
|
|
11
|
+
"Groups and options are project-defined. Platform, language, light/dark, brand and density are examples, never required dimensions or fixed flags. --locale changes CLI labels only.",
|
|
12
|
+
writes:
|
|
13
|
+
"token theme default --theme Group/Option explicitly saves a project default; the App's own active selection changes only in the App (or with a set-active-token-themes operation through advanced apply). View, validate and export never save a selection.",
|
|
14
|
+
},
|
|
15
|
+
actions: {
|
|
16
|
+
settings:
|
|
17
|
+
"Work by name: every listed write takes Token paths ({path} binds a Token), theme options as Group/Option and component, page, element, canvas, color and font names; the CLI makes IDs, node tables and coordinates. Names never repeat: siblings, pages, components, canvases, theme options and assets each have their own; \"Name [2]\" picks one of a name stored twice so it can be renamed. Only advanced apply takes IDs. All writes are atomic; --dry-run/--explain/--diff preview without writing.",
|
|
18
|
+
definitions:
|
|
19
|
+
"token theme list returns groups/options/defaults; token list --definitions [--group GROUP] reads stored option values; token list/show/explain resolve values and sources.",
|
|
20
|
+
flows:
|
|
21
|
+
"flow list shows starts and links by page and element name; flow link adds a link, flow unlink removes an element's links, flow start --page makes a start and --remove undoes it.",
|
|
22
|
+
view: "view looks at anything by name: nothing (whole package), --tokens/--token, --components/--component [--variant --element], --pages/--page [--platform --element]; --as text|spec|wireframe|png|issues. Text navigates; spec implements and traces sources; wireframe shows structure; PNG reviews appearance. Outlines collapse component internals and group repeated instances. changes --since REVISION lists, by name, what changed after a revision, including edits made in the App.",
|
|
23
|
+
validate:
|
|
24
|
+
"validate checks canonical validity and reports scoped text/layout/contrast issues separately. status valid means valid data, not complete visual approval. Always read issueCount, visualStatus and coverage.skipped. Pagination does not reduce the checked scope.",
|
|
25
|
+
export:
|
|
26
|
+
"export saves a page/component/region specification, wireframe or PNG. --format png --evidence writes all three from one native solve. Compare layout.hash, target, themes and revision; read coverage and unresolved fields.",
|
|
27
|
+
},
|
|
28
|
+
modelling: {
|
|
29
|
+
tokenLayers:
|
|
30
|
+
"Token names may have project-defined primitive/semantic/component layers; theme groups are independent dimensions, not name layers. Same-name tokens override in library set order (last active set wins). Token bindings reference stable token IDs; literals are also valid node values.",
|
|
31
|
+
tokenWrites:
|
|
32
|
+
"Write Tokens by name with token set: value applies wherever no option has its own value, values set named Group/Option options. Add and change theme options with token theme add/rename/default/delete --theme Group/Option. Resolve freely supplied combinations with --theme; do not create a separate saved-combination object.",
|
|
33
|
+
componentVariants:
|
|
34
|
+
"Component variants describe properties/states. Instance text overrides change content for one placement. They are distinct from token theme groups. Component edits affect its shared definition; instance overrides affect one placement.",
|
|
35
|
+
presentations:
|
|
36
|
+
"A Screen has a Base Presentation and optional different structures. Presentations store structure; token options resolve bound values and do not switch node trees or component variants. Context Axes describe finite non-token choices. Existing Scenarios describe page targets/actions; they are optional and are not required to select Token combinations.",
|
|
37
|
+
platforms:
|
|
38
|
+
"Split platform differences by kind. Values (font size, spacing) go to a Token theme group such as Viewport: Desktop / Mobile; without a saved default (token theme default) or an option named Default, its first option is the default, so list the platform you design first first. Structure or visibility inside a component (sidebar vs bottom tabs, a hidden action) goes to a Platform variant property on that component; shared page shells make good components with a Platform property. A whole page laid out differently is another Presentation. Add Platform only to components whose structure differs: on every component it multiplies variants. Checking a platform's Presentation, pass its theme option with --theme; view and validate hint when one exists but is not selected, or when an instance uses another platform's variant.",
|
|
39
|
+
packages:
|
|
40
|
+
"project init creates a blank self-contained package with one canvas and no pages, Tokens or components (page draw makes the first page); --name names the package and its file. In foundation-product layout write shared Tokens/themes/components to Foundation, pages/instances/Scenarios to Product. Read Product to resolve both; Product cannot edit Foundation components.",
|
|
41
|
+
designSystem:
|
|
42
|
+
"Generated from canonical token and component data; do not draw a duplicate design-system Screen. view/export --design-system inspects it. Representative samples do not prove every variant/combination was checked.",
|
|
43
|
+
},
|
|
44
|
+
output: {
|
|
45
|
+
budgetBytes: 8192,
|
|
46
|
+
strategy:
|
|
47
|
+
"Choose one page/component/variant/element. Read spec for implementation: authored fixed/fill/hug constraints, native bounds, field-level Token and component sources, overrides and node diagnostics. Read wireframe for spatial orientation; its character grid is approximate. Read PNG for high-fidelity appearance when the caller supports images. Text is a compact navigation outline, not an implementation specification. Specification, wireframe and PNG use native App layout; compare layout.hash, revision and selection, not revision alone. export --format png --evidence writes all three and JSON evidence from one solve. Unknown geometry stays ?/partial with a reason; seed rectangles are never actual coordinates. Coverage applies only to the selected size/themes/state, not untested combinations. --element expands details; --full expands the selected tree. Read resultFile.text.path for exact LF/space text, resultFile.path for complete JSON. Temporary paths are unique, expire after 30 minutes and must be consumed promptly; --output retains deliverables. Omitted stdout detail never means a check passed.",
|
|
48
|
+
imagePolicy:
|
|
49
|
+
"view defaults to a text outline for navigation; implementation starts with view --as spec. export defaults to a wireframe; --format spec writes a specification and --format png writes a high-fidelity image. A PNG cannot prove Token bindings, auto-sizing intent or interaction rules. A specification cannot prove visual quality. Models without image input must use spec/issues for structural checks; they may export PNG but must not claim visual inspection. Raster errors fail explicitly; no substitute painter fills in missing geometry.",
|
|
50
|
+
protocol:
|
|
51
|
+
"All stdout JSON is compact, including --full. Default directories expose only public identity, ownership, selection and count fields; read one target for its description or definition. Omit normal public visibility and false deprecation; actual private restrictions, deprecation and replacements remain actionable. Internal object fields never become public directory fields automatically. schema command project list or component list/search documents the output contract. token theme list omits unrelated stored App selection unless --full is explicitly requested. This omission policy must not discard meaningful design values such as 0, false, empty text or a node's hidden state.",
|
|
52
|
+
writes:
|
|
53
|
+
"Choose a unique --batch-id before retryable writes; reuse it only with identical input. Read changed fields and changeReportPath for exact before/after values. reverseEdit describes a new revision-guarded write, not history rollback. After later changes restore only the intended current fields with a new edit; never force stale snapshots. Inspect changed/noChange, revisions and scoped reuseReminders. Existing IDs on put operations usually replace the whole object; use update operations for partial edits.",
|
|
54
|
+
},
|
|
55
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smallpen/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.12",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"bin"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@smallpen/core": "0.1.0-alpha.
|
|
19
|
-
"@smallpen/local-package": "0.1.0-alpha.
|
|
18
|
+
"@smallpen/core": "0.1.0-alpha.12",
|
|
19
|
+
"@smallpen/local-package": "0.1.0-alpha.12"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|