@skilly-hand/skilly-hand 0.1.0 → 0.2.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### Added
8
+ - _None._
9
+
10
+ ### Changed
11
+ - _None._
12
+
13
+ ### Fixed
14
+ - _None._
15
+
16
+ ### Removed
17
+ - _None._
18
+
19
+ ## [0.1.1] - 2026-04-03
20
+ [View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.1.1)
21
+
7
22
  ### Added
8
23
  - Added automated changelog rotation via `scripts/release-changelog.mjs` to create dated release sections with npm version links.
9
24
 
@@ -14,4 +29,4 @@ All notable changes to this project are documented in this file.
14
29
  - _None._
15
30
 
16
31
  ### Removed
17
- - _None._
32
+ - Removed `source/legacy/` directory containing the old skills and agentic structure superseded by the catalog.
package/README.md CHANGED
@@ -58,7 +58,8 @@ npx skilly-hand
58
58
  4. Run publish gate: `npm run verify:publish`.
59
59
  5. Inspect package payload: `npm pack --dry-run --json`.
60
60
  6. Bump version intentionally: `npm version patch|minor|major` (this auto-rotates `CHANGELOG.md`, creates a dated release section, and inserts a version-specific npm link).
61
- 7. Publish the root package: `npm publish` (or `npm publish --tag next` for prereleases).
61
+ 7. Publish with assisted 2FA flow: `npm run publish:otp` (or `npm run publish:next` for prereleases).
62
+ - The script runs the publish gate, asks for OTP with hidden input, and if left blank lets npm trigger your default security method.
62
63
  8. Smoke test after publish: `npx @skilly-hand/skilly-hand@<version> --help`.
63
64
  9. Verify npm metadata (README render, changelog, license, executable bin).
64
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilly-hand/skilly-hand",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "CC-BY-NC-4.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -30,6 +30,9 @@
30
30
  "test": "node --test tests/*.test.js",
31
31
  "verify:packlist": "node ./scripts/verify-packlist.mjs",
32
32
  "verify:publish": "npm run catalog:check && npm test && npm run verify:packlist",
33
+ "publish:prepare": "npm run verify:publish && npm pack --dry-run --json",
34
+ "publish:otp": "node ./scripts/publish-with-otp.mjs",
35
+ "publish:next": "node ./scripts/publish-with-otp.mjs --tag next",
33
36
  "prepublishOnly": "npm run verify:publish",
34
37
  "version": "node ./scripts/release-changelog.mjs",
35
38
  "changelog:release": "node ./scripts/release-changelog.mjs",
@@ -2,8 +2,11 @@
2
2
  import path from "node:path";
3
3
  import { loadAllSkills } from "../../catalog/src/index.js";
4
4
  import { installProject, runDoctor, uninstallProject } from "../../core/src/index.js";
5
+ import { createTerminalRenderer } from "../../core/src/terminal.js";
5
6
  import { detectProject } from "../../detectors/src/index.js";
6
7
 
8
+ const renderer = createTerminalRenderer();
9
+
7
10
  function parseArgs(argv) {
8
11
  const args = [...argv];
9
12
  const positional = [];
@@ -11,11 +14,20 @@ function parseArgs(argv) {
11
14
  dryRun: false,
12
15
  yes: false,
13
16
  verbose: false,
17
+ json: false,
14
18
  agents: [],
15
19
  include: [],
16
20
  exclude: []
17
21
  };
18
22
 
23
+ function takeFlagValue(flagName) {
24
+ const value = args.shift();
25
+ if (!value || value.startsWith("-")) {
26
+ throw new Error(`Missing value for ${flagName}`);
27
+ }
28
+ return value;
29
+ }
30
+
19
31
  while (args.length > 0) {
20
32
  const token = args.shift();
21
33
  if (!token.startsWith("-")) {
@@ -26,10 +38,11 @@ function parseArgs(argv) {
26
38
  if (token === "--dry-run") flags.dryRun = true;
27
39
  else if (token === "--yes" || token === "-y") flags.yes = true;
28
40
  else if (token === "--verbose" || token === "-v") flags.verbose = true;
29
- else if (token === "--agent" || token === "-a") flags.agents.push(args.shift());
30
- else if (token === "--cwd") flags.cwd = args.shift();
31
- else if (token === "--include") flags.include.push(args.shift());
32
- else if (token === "--exclude") flags.exclude.push(args.shift());
41
+ else if (token === "--json") flags.json = true;
42
+ else if (token === "--agent" || token === "-a") flags.agents.push(takeFlagValue(token));
43
+ else if (token === "--cwd") flags.cwd = takeFlagValue(token);
44
+ else if (token === "--include") flags.include.push(takeFlagValue(token));
45
+ else if (token === "--exclude") flags.exclude.push(takeFlagValue(token));
33
46
  else if (token === "--help" || token === "-h") flags.help = true;
34
47
  else throw new Error(`Unknown flag: ${token}`);
35
48
  }
@@ -37,57 +50,251 @@ function parseArgs(argv) {
37
50
  return { command: positional[0], flags };
38
51
  }
39
52
 
40
- function printHelp() {
41
- console.log(`skilly-hand
42
-
43
- Usage:
44
- npx skilly-hand [install]
45
- npx skilly-hand detect
46
- npx skilly-hand list
47
- npx skilly-hand doctor
48
- npx skilly-hand uninstall
49
-
50
- Flags:
51
- --dry-run
52
- --yes
53
- --verbose
54
- --agent <codex|claude|cursor|gemini|copilot>
55
- --cwd <path>
56
- --include <tag>
57
- --exclude <tag>
58
- `);
53
+ function buildHelpText() {
54
+ const usage = renderer.section("Usage", renderer.list([
55
+ "npx skilly-hand [install]",
56
+ "npx skilly-hand detect",
57
+ "npx skilly-hand list",
58
+ "npx skilly-hand doctor",
59
+ "npx skilly-hand uninstall"
60
+ ], { bullet: "-" }));
61
+
62
+ const flags = renderer.section("Flags", renderer.list([
63
+ "--dry-run Show install plan without writing files",
64
+ "--json Emit stable JSON output for automation",
65
+ "--yes, -y Reserved for future non-interactive confirmations",
66
+ "--verbose, -v Reserved for future debug detail",
67
+ "--agent, -a <name> codex|claude|cursor|gemini|copilot (repeatable)",
68
+ "--cwd <path> Project root (defaults to current directory)",
69
+ "--include <tag> Include only skills matching all tags",
70
+ "--exclude <tag> Exclude skills matching any tag",
71
+ "--help, -h Show help"
72
+ ], { bullet: "-" }));
73
+
74
+ const examples = renderer.section("Examples", renderer.list([
75
+ "npx skilly-hand install --dry-run",
76
+ "npx skilly-hand detect --json",
77
+ "npx skilly-hand install --agent codex --agent claude",
78
+ "npx skilly-hand list --include workflow"
79
+ ], { bullet: "-" }));
80
+
81
+ return renderer.joinBlocks([
82
+ renderer.status("info", "skilly-hand", "Portable AI skill orchestration for coding assistants."),
83
+ usage,
84
+ flags,
85
+ examples
86
+ ]);
87
+ }
88
+
89
+ function detectionRows(detections) {
90
+ return detections.map((item) => ({
91
+ technology: item.technology,
92
+ confidence: item.confidence.toFixed(2),
93
+ reasons: item.reasons.join("; "),
94
+ recommended: item.recommendedSkillIds.join(", ")
95
+ }));
59
96
  }
60
97
 
61
- function printDetections(detections) {
98
+ function renderDetections(detections) {
62
99
  if (detections.length === 0) {
63
- console.log("No technology signals were detected.");
64
- return;
100
+ return renderer.status("warn", "No technology signals were detected.", "Only core skills will be selected.");
65
101
  }
66
102
 
67
- for (const item of detections) {
68
- console.log(`- ${item.technology} (${item.confidence})`);
69
- console.log(` reasons: ${item.reasons.join("; ")}`);
70
- console.log(` recommended skills: ${item.recommendedSkillIds.join(", ")}`);
103
+ return renderer.table(
104
+ [
105
+ { key: "technology", header: "Technology" },
106
+ { key: "confidence", header: "Confidence" },
107
+ { key: "reasons", header: "Reasons" },
108
+ { key: "recommended", header: "Recommended Skills" }
109
+ ],
110
+ detectionRows(detections)
111
+ );
112
+ }
113
+
114
+ function renderSkillTable(skills) {
115
+ if (skills.length === 0) {
116
+ return renderer.status("warn", "No skills selected.");
71
117
  }
118
+
119
+ return renderer.table(
120
+ [
121
+ { key: "id", header: "Skill ID" },
122
+ { key: "title", header: "Title" },
123
+ { key: "tags", header: "Tags" }
124
+ ],
125
+ skills.map((skill) => ({
126
+ id: skill.id,
127
+ title: skill.title,
128
+ tags: skill.tags.join(", ")
129
+ }))
130
+ );
131
+ }
132
+
133
+ function printInstallResult(result, flags) {
134
+ const mode = flags.dryRun ? "dry-run" : "apply";
135
+ const preflight = renderer.section(
136
+ "Install Preflight",
137
+ renderer.kv([
138
+ ["Project", result.plan.cwd],
139
+ ["Install root", result.plan.installRoot],
140
+ ["Agents", result.plan.agents.join(", ") || "none"],
141
+ ["Include tags", flags.include.join(", ") || "none"],
142
+ ["Exclude tags", flags.exclude.join(", ") || "none"],
143
+ ["Mode", mode]
144
+ ])
145
+ );
146
+
147
+ const detections = renderer.section("Detected Technologies", renderDetections(result.plan.detections));
148
+ const skills = renderer.section("Skill Plan", renderSkillTable(result.plan.skills));
149
+
150
+ const status = result.applied
151
+ ? renderer.status("success", "Installation completed.", "Managed files and symlinks are in place.")
152
+ : renderer.status("info", "Dry run complete.", "No files were written.");
153
+
154
+ const nextSteps = result.applied
155
+ ? renderer.nextSteps([
156
+ "Review generated AGENTS and assistant instruction files.",
157
+ "Run `npx skilly-hand doctor` to validate installation health.",
158
+ "Use `npx skilly-hand uninstall` to restore backed-up files if needed."
159
+ ])
160
+ : renderer.nextSteps([
161
+ "Run `npx skilly-hand install` to apply this plan.",
162
+ "Adjust `--include` and `--exclude` tags to tune skill selection."
163
+ ]);
164
+
165
+ renderer.write(renderer.joinBlocks([preflight, detections, skills, status, nextSteps]));
166
+ }
167
+
168
+ function printDetectResult(cwd, detections) {
169
+ const summary = renderer.section(
170
+ "Detection Summary",
171
+ renderer.kv([
172
+ ["Project", cwd],
173
+ ["Signals found", String(detections.length)]
174
+ ])
175
+ );
176
+
177
+ const details = renderer.section("Findings", renderDetections(detections));
178
+ renderer.write(renderer.joinBlocks([summary, details]));
179
+ }
180
+
181
+ function printListResult(skills) {
182
+ const summary = renderer.section(
183
+ "Catalog Summary",
184
+ renderer.kv([["Skills available", String(skills.length)]])
185
+ );
186
+
187
+ const table = renderer.section(
188
+ "Skills",
189
+ renderer.table(
190
+ [
191
+ { key: "id", header: "Skill ID" },
192
+ { key: "title", header: "Title" },
193
+ { key: "tags", header: "Tags" },
194
+ { key: "agents", header: "Agents" }
195
+ ],
196
+ skills.map((skill) => ({
197
+ id: skill.id,
198
+ title: skill.title,
199
+ tags: skill.tags.join(", "),
200
+ agents: skill.agentSupport.join(", ")
201
+ }))
202
+ )
203
+ );
204
+
205
+ renderer.write(renderer.joinBlocks([summary, table]));
206
+ }
207
+
208
+ function printDoctorResult(result) {
209
+ const health = result.installed
210
+ ? renderer.status("success", "Installation detected.")
211
+ : renderer.status("warn", "No installation detected.");
212
+
213
+ const summary = renderer.section(
214
+ "Doctor Summary",
215
+ renderer.kv([
216
+ ["Project", result.cwd],
217
+ ["Installed", result.installed ? "yes" : "no"],
218
+ ["Catalog issues", String(result.catalogIssues.length)]
219
+ ])
220
+ );
221
+
222
+ const lock = result.lock
223
+ ? renderer.section(
224
+ "Lock Metadata",
225
+ renderer.kv([
226
+ ["Generated at", result.lock.generatedAt],
227
+ ["Agents", result.lock.agents.join(", ")],
228
+ ["Skills", result.lock.skills.join(", ")]
229
+ ])
230
+ )
231
+ : "";
232
+
233
+ const issues = result.catalogIssues.length
234
+ ? renderer.section("Catalog Issues", renderer.list(result.catalogIssues))
235
+ : renderer.section("Catalog Issues", renderer.status("success", "No catalog issues found."));
236
+
237
+ const probes = renderer.section(
238
+ "Project Probes",
239
+ renderer.table(
240
+ [
241
+ { key: "path", header: "Path" },
242
+ { key: "exists", header: "Exists" },
243
+ { key: "type", header: "Type" }
244
+ ],
245
+ result.fileStatus.map((item) => ({
246
+ path: item.path,
247
+ exists: item.exists ? "yes" : "no",
248
+ type: item.type || "-"
249
+ }))
250
+ )
251
+ );
252
+
253
+ renderer.write(renderer.joinBlocks([health, summary, lock, issues, probes]));
72
254
  }
73
255
 
74
- function printPlan(plan) {
75
- console.log(`Project: ${plan.cwd}`);
76
- console.log(`Install root: ${plan.installRoot}`);
77
- console.log(`Agents: ${plan.agents.join(", ")}`);
78
- console.log("Detected technologies:");
79
- printDetections(plan.detections);
80
- console.log("Skills to install:");
81
- for (const skill of plan.skills) {
82
- console.log(`- ${skill.id}: ${skill.title} [${skill.tags.join(", ")}]`);
256
+ function printUninstallResult(result) {
257
+ if (result.removed) {
258
+ renderer.write(
259
+ renderer.joinBlocks([
260
+ renderer.status("success", "skilly-hand installation removed."),
261
+ renderer.nextSteps([
262
+ "Run `npx skilly-hand install` if you want to reinstall managed files.",
263
+ "Run `npx skilly-hand doctor` to confirm the project state."
264
+ ])
265
+ ])
266
+ );
267
+ return;
83
268
  }
269
+
270
+ renderer.write(
271
+ renderer.joinBlocks([
272
+ renderer.status("warn", "Nothing to uninstall.", result.reason),
273
+ renderer.nextSteps(["Run `npx skilly-hand install` to create a managed installation first."])
274
+ ])
275
+ );
84
276
  }
85
277
 
86
278
  async function main() {
87
279
  const { command, flags } = parseArgs(process.argv.slice(2));
88
280
 
89
281
  if (flags.help) {
90
- printHelp();
282
+ if (flags.json) {
283
+ renderer.writeJson({
284
+ command: command || "install",
285
+ help: true,
286
+ usage: [
287
+ "npx skilly-hand [install]",
288
+ "npx skilly-hand detect",
289
+ "npx skilly-hand list",
290
+ "npx skilly-hand doctor",
291
+ "npx skilly-hand uninstall"
292
+ ]
293
+ });
294
+ return;
295
+ }
296
+
297
+ renderer.write(buildHelpText());
91
298
  return;
92
299
  }
93
300
 
@@ -95,29 +302,57 @@ async function main() {
95
302
  const effectiveCommand = command || "install";
96
303
 
97
304
  if (effectiveCommand === "detect") {
98
- printDetections(await detectProject(cwd));
305
+ const detections = await detectProject(cwd);
306
+ if (flags.json) {
307
+ renderer.writeJson({
308
+ command: "detect",
309
+ cwd,
310
+ count: detections.length,
311
+ detections
312
+ });
313
+ return;
314
+ }
315
+ printDetectResult(cwd, detections);
99
316
  return;
100
317
  }
101
318
 
102
319
  if (effectiveCommand === "list") {
103
320
  const skills = await loadAllSkills();
104
- for (const skill of skills) {
105
- console.log(`- ${skill.id}: ${skill.title}`);
106
- console.log(` tags: ${skill.tags.join(", ")}`);
107
- console.log(` agents: ${skill.agentSupport.join(", ")}`);
321
+ if (flags.json) {
322
+ renderer.writeJson({
323
+ command: "list",
324
+ count: skills.length,
325
+ skills
326
+ });
327
+ return;
108
328
  }
329
+ printListResult(skills);
109
330
  return;
110
331
  }
111
332
 
112
333
  if (effectiveCommand === "doctor") {
113
334
  const result = await runDoctor(cwd);
114
- console.log(JSON.stringify(result, null, 2));
335
+ if (flags.json) {
336
+ renderer.writeJson({
337
+ command: "doctor",
338
+ ...result
339
+ });
340
+ return;
341
+ }
342
+ printDoctorResult(result);
115
343
  return;
116
344
  }
117
345
 
118
346
  if (effectiveCommand === "uninstall") {
119
347
  const result = await uninstallProject(cwd);
120
- console.log(result.removed ? "skilly-hand installation removed." : result.reason);
348
+ if (flags.json) {
349
+ renderer.writeJson({
350
+ command: "uninstall",
351
+ ...result
352
+ });
353
+ return;
354
+ }
355
+ printUninstallResult(result);
121
356
  return;
122
357
  }
123
358
 
@@ -130,15 +365,53 @@ async function main() {
130
365
  excludeTags: flags.exclude
131
366
  });
132
367
 
133
- printPlan(result.plan);
134
- console.log(result.applied ? "Installation completed." : "Dry run only; nothing was written.");
368
+ if (flags.json) {
369
+ renderer.writeJson({
370
+ command: "install",
371
+ applied: result.applied,
372
+ plan: result.plan,
373
+ lockPath: result.lockPath || null
374
+ });
375
+ return;
376
+ }
377
+
378
+ printInstallResult(result, flags);
135
379
  return;
136
380
  }
137
381
 
138
382
  throw new Error(`Unknown command: ${effectiveCommand}`);
139
383
  }
140
384
 
385
+ const jsonRequested = process.argv.includes("--json");
386
+
141
387
  main().catch((error) => {
142
- console.error(error.message);
388
+ const hint =
389
+ error.message.startsWith("Unknown command:")
390
+ ? "Run `npx skilly-hand --help` to see available commands."
391
+ : error.message.startsWith("Unknown flag:") || error.message.startsWith("Missing value")
392
+ ? "Check command flags with `npx skilly-hand --help`."
393
+ : "Retry with `--verbose` for expanded context if needed.";
394
+
395
+ if (jsonRequested) {
396
+ renderer.writeErrorJson({
397
+ ok: false,
398
+ error: {
399
+ what: "skilly-hand command failed",
400
+ why: error.message,
401
+ hint
402
+ }
403
+ });
404
+ process.exitCode = 1;
405
+ return;
406
+ }
407
+
408
+ renderer.writeError(
409
+ renderer.error({
410
+ what: "skilly-hand command failed",
411
+ why: error.message,
412
+ hint,
413
+ exitCode: 1
414
+ })
415
+ );
143
416
  process.exitCode = 1;
144
417
  });
@@ -0,0 +1,196 @@
1
+ const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
2
+
3
+ function asString(value) {
4
+ if (value === null || value === undefined) return "";
5
+ return String(value);
6
+ }
7
+
8
+ function stripAnsi(value) {
9
+ return asString(value).replace(ANSI_PATTERN, "");
10
+ }
11
+
12
+ function padEndAnsi(value, width) {
13
+ const clean = stripAnsi(value);
14
+ if (clean.length >= width) return value;
15
+ return value + " ".repeat(width - clean.length);
16
+ }
17
+
18
+ function normalizeBooleanEnv(value) {
19
+ if (value === undefined || value === null) return null;
20
+ const normalized = String(value).trim().toLowerCase();
21
+ if (normalized === "" || normalized === "0" || normalized === "false" || normalized === "no") return false;
22
+ return true;
23
+ }
24
+
25
+ export function detectColorSupport({ env = process.env, stream = process.stdout } = {}) {
26
+ const noColor = normalizeBooleanEnv(env.NO_COLOR);
27
+ if (noColor) return false;
28
+
29
+ const forceColor = normalizeBooleanEnv(env.FORCE_COLOR);
30
+ if (forceColor === true) return true;
31
+ if (forceColor === false) return false;
32
+
33
+ if (!stream?.isTTY) return false;
34
+ if (env.CI) return false;
35
+ return true;
36
+ }
37
+
38
+ export function detectUnicodeSupport({ env = process.env, stream = process.stdout, platform = process.platform } = {}) {
39
+ if (!stream?.isTTY) return false;
40
+ if (env.TERM === "dumb") return false;
41
+ if (env.NO_UNICODE) return false;
42
+ if (platform === "win32") {
43
+ return Boolean(env.WT_SESSION || env.TERM_PROGRAM || env.ConEmuTask || env.ANSICON);
44
+ }
45
+ return true;
46
+ }
47
+
48
+ function createStyler(enabled) {
49
+ if (!enabled) {
50
+ const passthrough = (value) => asString(value);
51
+ return {
52
+ reset: passthrough,
53
+ bold: passthrough,
54
+ dim: passthrough,
55
+ cyan: passthrough,
56
+ green: passthrough,
57
+ yellow: passthrough,
58
+ red: passthrough,
59
+ magenta: passthrough
60
+ };
61
+ }
62
+
63
+ const wrap = (code) => (value) => `\u001b[${code}m${asString(value)}\u001b[0m`;
64
+ return {
65
+ reset: wrap("0"),
66
+ bold: wrap("1"),
67
+ dim: wrap("2"),
68
+ cyan: wrap("36"),
69
+ green: wrap("32"),
70
+ yellow: wrap("33"),
71
+ red: wrap("31"),
72
+ magenta: wrap("35")
73
+ };
74
+ }
75
+
76
+ function renderKeyValue(entries, style) {
77
+ if (!entries || entries.length === 0) return "";
78
+ const normalized = entries.map(([key, value]) => [asString(key), asString(value)]);
79
+ const width = normalized.reduce((max, [key]) => Math.max(max, key.length), 0);
80
+ return normalized
81
+ .map(([key, value]) => `${style.dim(padEndAnsi(key, width))} : ${value}`)
82
+ .join("\n");
83
+ }
84
+
85
+ function renderList(items, { bullet }) {
86
+ if (!items || items.length === 0) return "";
87
+ return items.map((item) => `${bullet} ${asString(item)}`).join("\n");
88
+ }
89
+
90
+ function renderTable(columns, rows) {
91
+ if (!columns || columns.length === 0) return "";
92
+ const header = columns.map((column) => column.header);
93
+ const matrix = [header, ...rows.map((row) => columns.map((column) => asString(row[column.key] ?? "")))];
94
+ const widths = header.map((_, index) =>
95
+ matrix.reduce((max, line) => Math.max(max, stripAnsi(line[index]).length), 0)
96
+ );
97
+
98
+ const headerLine = header.map((value, index) => padEndAnsi(value, widths[index])).join(" ");
99
+ const separatorLine = widths.map((width) => "-".repeat(Math.max(3, width))).join(" ");
100
+ const body = rows.map((row) =>
101
+ columns.map((column, index) => padEndAnsi(asString(row[column.key] ?? ""), widths[index])).join(" ")
102
+ );
103
+
104
+ return [headerLine, separatorLine, ...body].join("\n");
105
+ }
106
+
107
+ function joinBlocks(blocks) {
108
+ const filtered = blocks.map((block) => asString(block).trimEnd()).filter(Boolean);
109
+ if (filtered.length === 0) return "";
110
+ return filtered.join("\n\n");
111
+ }
112
+
113
+ export function createTerminalRenderer({
114
+ stdout = process.stdout,
115
+ stderr = process.stderr,
116
+ env = process.env,
117
+ platform = process.platform
118
+ } = {}) {
119
+ const colorEnabled = detectColorSupport({ env, stream: stdout });
120
+ const unicodeEnabled = detectUnicodeSupport({ env, stream: stdout, platform });
121
+ const style = createStyler(colorEnabled);
122
+ const symbols = unicodeEnabled
123
+ ? { info: "i", success: "✓", warn: "!", error: "x", bullet: "•", section: "■" }
124
+ : { info: "i", success: "+", warn: "!", error: "x", bullet: "-", section: "#" };
125
+
126
+ const statusStyles = {
127
+ info: style.cyan,
128
+ success: style.green,
129
+ warn: style.yellow,
130
+ error: style.red
131
+ };
132
+
133
+ const renderer = {
134
+ colorEnabled,
135
+ unicodeEnabled,
136
+ symbols,
137
+ style,
138
+ json(value) {
139
+ return JSON.stringify(value, null, 2);
140
+ },
141
+ section(title, body = "") {
142
+ const heading = `${style.cyan(symbols.section)} ${style.bold(asString(title))}`;
143
+ const bodyText = asString(body).trimEnd();
144
+ return bodyText ? `${heading}\n${bodyText}` : heading;
145
+ },
146
+ kv(entries) {
147
+ return renderKeyValue(entries, style);
148
+ },
149
+ list(items, options = {}) {
150
+ return renderList(items, { bullet: options.bullet || symbols.bullet });
151
+ },
152
+ table(columns, rows) {
153
+ return renderTable(columns, rows);
154
+ },
155
+ status(level, message, detail = "") {
156
+ const applied = statusStyles[level] || ((value) => value);
157
+ const icon = symbols[level] || symbols.info;
158
+ const main = `${applied(icon)} ${asString(message)}`;
159
+ if (!detail) return main;
160
+ return `${main}\n${style.dim(" " + asString(detail))}`;
161
+ },
162
+ summary(title, items = []) {
163
+ return renderer.section(title, renderer.list(items));
164
+ },
165
+ nextSteps(steps = []) {
166
+ if (!steps.length) return "";
167
+ return renderer.section("Next Steps", renderer.list(steps));
168
+ },
169
+ error({ what = "Command failed", why = "", hint = "", exitCode = 1 } = {}) {
170
+ const blocks = [
171
+ renderer.status("error", what),
172
+ why ? renderer.kv([["Why", why]]) : "",
173
+ hint ? renderer.kv([["How to recover", hint]]) : "",
174
+ renderer.kv([["Exit code", asString(exitCode)]])
175
+ ];
176
+ return joinBlocks(blocks);
177
+ },
178
+ joinBlocks
179
+ };
180
+
181
+ return {
182
+ ...renderer,
183
+ write(block = "") {
184
+ if (block) stdout.write(`${block}\n`);
185
+ },
186
+ writeError(block = "") {
187
+ if (block) stderr.write(`${block}\n`);
188
+ },
189
+ writeJson(value) {
190
+ stdout.write(`${renderer.json(value)}\n`);
191
+ },
192
+ writeErrorJson(value) {
193
+ stderr.write(`${renderer.json(value)}\n`);
194
+ }
195
+ };
196
+ }