@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.
@@ -0,0 +1,241 @@
1
+ import { printText } from "./result-files.mjs";
2
+ import { SmallPenError } from "@smallpen/core";
3
+ import { commandContract } from "./command-contract.mjs";
4
+ import { CLI_RULES } from "./workflow.mjs";
5
+ import {
6
+ COMMAND_GROUPS,
7
+ PUBLIC_COMMANDS,
8
+ actionContract,
9
+ inputQueries,
10
+ groupRoute,
11
+ groupDefinition,
12
+ groupActions,
13
+ publicGuidance,
14
+ } from "./command-tree.mjs";
15
+
16
+ export const nextQuery = (argv) => ({ operation: `smallpen.${argv[0]}`, argv });
17
+ const query = (...argv) => nextQuery([...argv, "--json"]);
18
+ const rules = {
19
+ project: [
20
+ "Blank packages and workspace ownership",
21
+ () => [CLI_RULES.modelling.packages],
22
+ ],
23
+ flows: [
24
+ "Prototype triggers and destinations",
25
+ () => [CLI_RULES.actions.flows],
26
+ ],
27
+ validate: [
28
+ "Check scope, issues and coverage",
29
+ () => [CLI_RULES.actions.validate],
30
+ ],
31
+ themes: [
32
+ "Temporary theme combinations and defaults",
33
+ () =>
34
+ Object.values(CLI_RULES.selection).filter(
35
+ (value) => typeof value === "string",
36
+ ),
37
+ ],
38
+ tokens: [
39
+ "Token layers, rows and option values",
40
+ () => [CLI_RULES.modelling.tokenLayers, CLI_RULES.modelling.tokenWrites],
41
+ ],
42
+ components: [
43
+ "Component variants and generated design system",
44
+ () => [
45
+ CLI_RULES.modelling.componentVariants,
46
+ CLI_RULES.modelling.designSystem,
47
+ ],
48
+ ],
49
+ platforms: [
50
+ "Desktop, mobile and other platforms: themes, variants or Presentations",
51
+ () => [CLI_RULES.modelling.platforms],
52
+ ],
53
+ targets: [
54
+ "Packages, Screens and Presentations",
55
+ () => [CLI_RULES.modelling.packages, CLI_RULES.modelling.presentations],
56
+ ],
57
+ output: [
58
+ "Specifications for implementation, wireframes for structure, PNG for appearance",
59
+ () =>
60
+ Object.values(CLI_RULES.output)
61
+ .filter((value) => typeof value === "string")
62
+ .slice(0, 3),
63
+ ],
64
+ writes: [
65
+ "Atomic edits, retries and explicit reverse edits",
66
+ () => [CLI_RULES.actions.settings, CLI_RULES.output.writes],
67
+ ],
68
+ };
69
+
70
+ const inputs = {
71
+ apply: "batch",
72
+ init: "init",
73
+ "import-tokens": "token",
74
+ };
75
+ const notes = {
76
+ themes: [
77
+ "Read available groups/options/defaults. This call resolves only its arguments; no selection is remembered.",
78
+ ],
79
+ view: [
80
+ "Names the target: --page, --component, --token, --canvas or nothing for the whole package; --as text first.",
81
+ ],
82
+ export: [
83
+ "Three outputs: --format spec for implementation, wireframe for structure, png for appearance. --format png --evidence writes all three from one native layout. Compare layout.hash; image inspection requires an image-capable caller.",
84
+ ],
85
+ validate: [
86
+ "A page checks every version and a component every variant unless --platform or --variant names one; read coverage and skipped checks.",
87
+ ],
88
+ "import-tokens": [
89
+ "Review without --apply first. --dry-run takes precedence over --apply and never commits.",
90
+ ],
91
+ };
92
+
93
+ export function commandOverview(name) {
94
+ const contract = commandContract(name);
95
+ const positional = contract.arguments.map(({ name, required }) =>
96
+ required ? `<${name}>` : `[${name}]`,
97
+ );
98
+ const required = Object.values(contract.parameters)
99
+ .filter((parameter) => parameter.required)
100
+ .map(
101
+ ({ option, type }) => `${option}${type === "boolean" ? "" : " <value>"}`,
102
+ );
103
+ return {
104
+ command: name,
105
+ purpose: contract.purpose,
106
+ usage: ["smallpen", name, ...positional, ...required, "[options]"].join(
107
+ " ",
108
+ ),
109
+ options: Object.values(contract.parameters).map(({ option }) => option),
110
+ ...(notes[name] ? { notes: notes[name] } : {}),
111
+ nextOperations: [
112
+ query("schema", "command", name),
113
+ ...(inputs[name] ? [query("schema", inputs[name])] : []),
114
+ ],
115
+ };
116
+ }
117
+
118
+ function unknown(topic, section) {
119
+ throw new SmallPenError(
120
+ "unknown_help_topic",
121
+ `Unknown help topic: ${[topic, section].filter(Boolean).join(" ")}`,
122
+ {
123
+ nextOperations: [
124
+ query(
125
+ "help",
126
+ ...(["workflow", "rules"].includes(topic) ? [topic] : []),
127
+ ),
128
+ ],
129
+ },
130
+ );
131
+ }
132
+
133
+ export function helpTopic(topic, section, { full = false } = {}) {
134
+ if (!topic) {
135
+ if (section) return unknown(topic, section);
136
+ return {
137
+ purpose: "Manage local SmallPen design packages",
138
+ usage: "smallpen COMMAND [arguments] [options]",
139
+ notes: [
140
+ "Use help OBJECT to list actions, help OBJECT ACTION for usage, and schema command OBJECT ACTION for parameters and input queries. Common rules: help rules.",
141
+ "One .smallpen directory is one Canonical Package. Each call is independent; there is no current selection, current page, or cross-call session state.",
142
+ "Reads start small. --full expands data; large results use a temporary resultFile with SHA-256. --stdout explicitly allows large stdout. Random names share one temporary folder; files older than 30 minutes are cleaned each invocation. --output retains exports.",
143
+ ],
144
+ commands: PUBLIC_COMMANDS,
145
+ };
146
+ }
147
+ if (Object.hasOwn(COMMAND_GROUPS, topic)) {
148
+ const definition = groupDefinition(topic, section);
149
+ if (definition)
150
+ return {
151
+ command: [topic, section].filter(Boolean).join(" "),
152
+ purpose: definition.purpose,
153
+ usage: `smallpen ${[topic, section].filter(Boolean).join(" ")} ACTION [arguments] [options]`,
154
+ options: ["--help", "--json"],
155
+ actions: groupActions(topic, section),
156
+ nextOperations: [
157
+ query("schema", "command", topic, ...(section?.split(" ") ?? [])),
158
+ ],
159
+ };
160
+ const route = groupRoute(topic, section);
161
+ const contract = actionContract(topic, section);
162
+ const engineNotes = notes[route.engine];
163
+ const required = Object.values(contract.parameters)
164
+ .filter((p) => p.required)
165
+ .map((p) => `${p.option} <value>`);
166
+ return {
167
+ command: route.name,
168
+ purpose: route.purpose,
169
+ usage: [
170
+ "smallpen",
171
+ route.name,
172
+ ...contract.arguments.map((a) =>
173
+ a.required ? `<${a.name}>` : `[${a.name}]`,
174
+ ),
175
+ ...required,
176
+ "[options]",
177
+ ].join(" "),
178
+ options: Object.values(contract.parameters).map((p) => p.option),
179
+ ...(engineNotes ? { notes: engineNotes } : {}),
180
+ nextOperations: [
181
+ query("schema", "command", ...route.name.split(" ")),
182
+ ...inputQueries(route),
183
+ ],
184
+ ...(full
185
+ ? {
186
+ parameters: contract.parameters,
187
+ intentActions: contract.intentActions,
188
+ }
189
+ : {}),
190
+ };
191
+ }
192
+ if (topic === "workflow") return helpTopic("rules", section, { full });
193
+ if (topic === "rules") {
194
+ if (full && !section) return CLI_RULES;
195
+ if (section) {
196
+ if (!Object.hasOwn(rules, section)) return unknown(topic, section);
197
+ const [title, read] = rules[section];
198
+ return { topic, id: section, title, notes: read() };
199
+ }
200
+ return {
201
+ topic,
202
+ rules: Object.entries(rules).map(([id, [title]]) => ({
203
+ id,
204
+ title,
205
+ nextOperations: [query("help", "rules", id)],
206
+ })),
207
+ };
208
+ }
209
+ if (section) return unknown(topic, section);
210
+ return commandOverview(topic);
211
+ }
212
+
213
+ export function printBriefHelp(topic, section, options) {
214
+ const value = helpTopic(topic, section, options);
215
+ if (["rules", "workflow"].includes(topic) && options?.full && !section) {
216
+ printText(JSON.stringify(value, null, 2) + "\n");
217
+ return;
218
+ }
219
+ const lines = [
220
+ topic && topic !== "help"
221
+ ? `SmallPen ${topic}${section ? ` ${section}` : ""}`
222
+ : "SmallPen Canonical Package CLI",
223
+ value.purpose ?? value.title ?? "",
224
+ value.usage ? `Usage: ${value.usage}` : "",
225
+ ];
226
+ if (value.options) lines.push(`Options: ${value.options.join(" ")}`);
227
+ lines.push(...(value.notes ?? []));
228
+ if (value.guidance) lines.push(value.guidance);
229
+ for (const entry of value.stages ?? value.rules ?? [])
230
+ lines.push(
231
+ `${entry.id}: ${entry.title}`,
232
+ ` smallpen ${entry.nextOperations[0].argv.join(" ")}`,
233
+ );
234
+ for (const { command, purpose } of value.commands ?? [])
235
+ lines.push(` ${command.padEnd(20)} ${purpose}`);
236
+ for (const { action, purpose } of value.actions ?? [])
237
+ lines.push(` ${action.padEnd(20)} ${purpose}`);
238
+ for (const { argv } of value.nextOperations ?? [])
239
+ lines.push(`Next: smallpen ${argv.join(" ")}`);
240
+ printText(lines.filter(Boolean).join("\n") + "\n");
241
+ }