@skilly-hand/skilly-hand 0.1.1 → 0.3.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
@@ -16,6 +16,22 @@ All notable changes to this project are documented in this file.
16
16
  ### Removed
17
17
  - _None._
18
18
 
19
+ ## [0.3.0] - 2026-04-03
20
+ [View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.3.0)
21
+
22
+ ### Added
23
+ - Added portable skill `figma-mcp-0to1` with setup, tool-selection, first-canvas, and troubleshooting documentation.
24
+
25
+ ### Changed
26
+ - Synced generated documentation outputs in `AGENTS.md` and `catalog/README.md` to include the current 5-skill registry.
27
+ - Refreshed root documentation to reflect current catalog composition and release/doc sync workflow.
28
+
29
+ ### Fixed
30
+ - Updated catalog manifest test expectations for the 5-skill portable catalog.
31
+
32
+ ### Removed
33
+ - _None._
34
+
19
35
  ## [0.1.1] - 2026-04-03
20
36
  [View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.1.1)
21
37
 
package/README.md CHANGED
@@ -25,6 +25,7 @@
25
25
  - **Installs portable AI agent skills** into your project from a curated catalog
26
26
  - **Auto-detects your stack** and recommends relevant skills automatically
27
27
  - **Supports every major coding assistant** — Claude Code, OpenCode, Cursor, Copilot, Gemini, and Codex — from a single command
28
+ - **Ships a curated core skill set** including orchestration, SDD workflow, and Figma MCP onboarding
28
29
  - **Preserves original agentic structures** in `source/legacy/` as a migration reference
29
30
 
30
31
  ---
@@ -50,15 +51,30 @@ npx skilly-hand
50
51
 
51
52
  ---
52
53
 
54
+ ## Current Portable Catalog
55
+
56
+ The catalog currently includes:
57
+
58
+ - `agents-root-orchestrator`
59
+ - `figma-mcp-0to1`
60
+ - `skill-creator`
61
+ - `spec-driven-development`
62
+ - `token-optimizer`
63
+
64
+ See [catalog/README.md](./catalog/README.md) for generated skill metadata.
65
+
66
+ ---
67
+
53
68
  ## Release Workflow (npm)
54
69
 
55
70
  1. Confirm session: `npm whoami` (or `npm login`).
56
71
  2. Keep `CHANGELOG.md` up to date under `## [Unreleased]` as work lands.
57
- 3. Regenerate derived catalog files when needed: `npm run build && npm run catalog:sync`.
72
+ 3. Regenerate derived files when needed: `npm run build && npm run catalog:sync && npm run agentic:self:sync`.
58
73
  4. Run publish gate: `npm run verify:publish`.
59
74
  5. Inspect package payload: `npm pack --dry-run --json`.
60
75
  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).
76
+ 7. Publish with assisted 2FA flow: `npm run publish:otp` (or `npm run publish:next` for prereleases).
77
+ - The script runs the publish gate, asks for OTP with hidden input, and if left blank lets npm trigger your default security method.
62
78
  8. Smoke test after publish: `npx @skilly-hand/skilly-hand@<version> --help`.
63
79
  9. Verify npm metadata (README render, changelog, license, executable bin).
64
80
 
package/catalog/README.md CHANGED
@@ -1,30 +1,13 @@
1
- <div align="center">
1
+ # Portable Catalog
2
2
 
3
- # Portable Skill Catalog
3
+ Published portable skills consumed by the `skilly-hand` CLI.
4
4
 
5
- *Published portable skills consumed by the `skilly-hand` CLI.*
5
+ | Skill | Description | Tags | Installs For |
6
+ | ----- | ----------- | ---- | ------------ |
7
+ | `agents-root-orchestrator` | Author root AGENTS.md as a Where/What/When orchestrator that routes tasks and skill invocation clearly. | core, workflow, orchestration | all |
8
+ | `figma-mcp-0to1` | Guide users from Figma MCP installation and authentication through first canvas creation, with function-level tool coverage and operational recovery patterns. | figma, mcp, workflow, design | all |
9
+ | `skill-creator` | Create and standardize AI skills with reusable structure, metadata rules, and templates. | core, workflow, authoring | all |
10
+ | `spec-driven-development` | Plan, execute, and verify multi-step work through versioned specs with small, testable tasks. | core, workflow, planning | all |
11
+ | `token-optimizer` | Classify task complexity and right-size reasoning depth, context gathering, and response detail to reduce wasted tokens. | core, workflow, efficiency | all |
6
12
 
7
- </div>
8
-
9
- ---
10
-
11
- ## Skills
12
-
13
- | Skill | Description | Tags |
14
- | ----- | ----------- | ---- |
15
- | `agents-root-orchestrator` | Author root `AGENTS.md` as a Where/What/When orchestrator that routes tasks and skill invocation clearly. | `core` `workflow` `orchestration` |
16
- | `skill-creator` | Create and standardize AI skills with reusable structure, metadata rules, and templates. | `core` `workflow` `authoring` |
17
- | `spec-driven-development` | Plan, execute, and verify multi-step work through versioned specs with small, testable tasks. | `core` `workflow` `planning` |
18
- | `token-optimizer` | Classify task complexity and right-size reasoning depth, context gathering, and response detail to reduce wasted tokens. | `core` `workflow` `efficiency` |
19
-
20
- ---
21
-
22
- ## Install a skill
23
-
24
- ```bash
25
- npx skilly-hand install <skill-name>
26
- npx skilly-hand install agents-root-orchestrator
27
- ```
28
-
29
- > [!NOTE]
30
- > Legacy source under `source/legacy/agentic-structure` is preserved as reference material and is **excluded from installation**.
13
+ Legacy source remains under `source/legacy/agentic-structure` and is excluded from installation.
@@ -1,5 +1,6 @@
1
1
  [
2
2
  "agents-root-orchestrator",
3
+ "figma-mcp-0to1",
3
4
  "skill-creator",
4
5
  "spec-driven-development",
5
6
  "token-optimizer"
@@ -0,0 +1,69 @@
1
+ # Figma MCP 0-to-1 Guide
2
+
3
+ ## When to Use
4
+
5
+ Use this skill when:
6
+
7
+ - You need to set up Figma MCP from scratch.
8
+ - You need a reliable path from connection to first successful canvas output.
9
+ - You need to choose the right Figma MCP function for a task.
10
+ - You need operational recovery for permission, auth, tool-loading, or rate-limit failures.
11
+
12
+ Do not use this skill for:
13
+
14
+ - Generic frontend implementation that does not require Figma MCP.
15
+ - One-off code-only tasks with no design context.
16
+ - Legacy repository-specific Figma pipelines that already define their own strict workflow.
17
+
18
+ ---
19
+
20
+ ## Routing Map
21
+
22
+ Choose subskills by intent:
23
+
24
+ | Intent | Subskill |
25
+ | --- | --- |
26
+ | Install and authenticate MCP connection | [agents/install-auth.md](agents/install-auth.md) |
27
+ | Select exact function/tool and expected inputs | [agents/tool-function-catalog.md](agents/tool-function-catalog.md) |
28
+ | Create first canvas output safely | [agents/canvas-creation-playbook.md](agents/canvas-creation-playbook.md) |
29
+ | Recover from errors, limits, or drift | [agents/troubleshooting-ops.md](agents/troubleshooting-ops.md) |
30
+
31
+ ---
32
+
33
+ ## Standard Execution Sequence
34
+
35
+ 1. Set up server transport and authentication.
36
+ 2. Verify connectivity with a low-risk call (`whoami` on remote, or a read tool).
37
+ 3. Select the smallest tool that solves the immediate task.
38
+ 4. Run creation in short, validated steps (avoid large one-shot requests).
39
+ 5. If anything fails, use troubleshooting flow before retrying.
40
+
41
+ ---
42
+
43
+ ## Core Rules
44
+
45
+ - Prefer remote server for broadest feature coverage and write workflows.
46
+ - Treat write actions as staged operations, not a single large operation.
47
+ - Use link-based node targeting for reliable design-context extraction.
48
+ - Keep a clear distinction between read context tools and write/canvas tools.
49
+ - For repeated team workflows, reuse prompts and config snippets from `assets/`.
50
+
51
+ ---
52
+
53
+ ## Key References
54
+
55
+ - Full function matrix: [references/official-tools-matrix.md](references/official-tools-matrix.md)
56
+ - Client setup snippets: [assets/client-config-snippets.md](assets/client-config-snippets.md)
57
+ - Prompt starters: [assets/prompt-recipes.md](assets/prompt-recipes.md)
58
+
59
+ ---
60
+
61
+ ## Commands
62
+
63
+ ```bash
64
+ # Codex CLI (manual remote setup)
65
+ codex mcp add figma --url https://mcp.figma.com/mcp
66
+
67
+ # Verify catalog integrity in this repository
68
+ npm run catalog:check
69
+ ```
@@ -0,0 +1,54 @@
1
+ # Canvas Creation Playbook
2
+
3
+ ## Goal
4
+
5
+ Deliver a first successful Figma canvas result with low risk and clear verification.
6
+
7
+ ## Safe 0-to-1 Workflow
8
+
9
+ 1. **Create or choose file context**
10
+ - New file path: call `create_new_file`.
11
+ - Existing file path: confirm URL/node target and permissions.
12
+
13
+ 2. **Verify identity and access**
14
+ - Run `whoami` (remote) to confirm account/plan.
15
+ - If access mismatch appears, stop and fix permissions first.
16
+
17
+ 3. **Read before write**
18
+ - Call `search_design_system` to reuse components/variables.
19
+ - Optionally call `get_metadata` for structure baseline.
20
+
21
+ 4. **Perform first write step**
22
+ - Use `use_figma` for a small, atomic action:
23
+ - Create one frame.
24
+ - Add one text node.
25
+ - Apply one style/token decision.
26
+
27
+ 5. **Validate state**
28
+ - Inspect with `get_metadata` or `get_screenshot`.
29
+ - Confirm node IDs and visual result.
30
+
31
+ 6. **Iterate in small increments**
32
+ - Add layout, spacing, variants, or additional sections in separate write steps.
33
+ - Re-validate after each step.
34
+
35
+ ## Alternative First-Creation Flows
36
+
37
+ ### Diagram-first flow (FigJam)
38
+
39
+ 1. `create_new_file` (FigJam)
40
+ 2. `generate_diagram` from plain-language workflow description
41
+ 3. Validate diagram structure and labels
42
+
43
+ ### Code-to-canvas flow
44
+
45
+ 1. Ask client to start local app capture workflow
46
+ 2. Capture screen/element states to Figma
47
+ 3. Open generated file and validate generated layers
48
+ 4. Follow up with `use_figma` for cleanup/polish
49
+
50
+ ## Guardrails
51
+
52
+ - Do not run large multi-section writes as the first operation.
53
+ - Always return to read/verify tools after each write step.
54
+ - If a write fails, use troubleshooting flow before retrying.
@@ -0,0 +1,54 @@
1
+ # Install and Auth
2
+
3
+ ## Goal
4
+
5
+ Get Figma MCP connected and authenticated, with a verified first tool call.
6
+
7
+ ## Server Modes
8
+
9
+ | Mode | Endpoint | Recommended | Notes |
10
+ | --- | --- | --- | --- |
11
+ | Remote | `https://mcp.figma.com/mcp` | Yes | Broadest features, including write-to-canvas workflows. |
12
+ | Desktop | `http://127.0.0.1:3845/mcp` | Only for special org/enterprise needs | Requires Figma desktop app + Dev Mode desktop MCP toggle. |
13
+
14
+ ## Codex Setup
15
+
16
+ ### Preferred path (Figma app/plugin workflow)
17
+
18
+ - Use the Figma app path if available in your Codex client.
19
+ - Complete auth prompt flow and grant access.
20
+
21
+ ### Manual Codex CLI path
22
+
23
+ ```bash
24
+ codex mcp add figma --url https://mcp.figma.com/mcp
25
+ ```
26
+
27
+ When prompted, authenticate the server.
28
+
29
+ ## Other Common Clients (manual patterns)
30
+
31
+ - VS Code: add HTTP MCP server in `mcp.json` using the remote endpoint.
32
+ - Cursor: add MCP server in MCP settings or use its Figma plugin flow.
33
+ - Claude Code: `claude mcp add --transport http figma https://mcp.figma.com/mcp`.
34
+ - Gemini CLI: install Figma extension from `figma/mcp-server-guide`, then run auth command in CLI.
35
+
36
+ See exact snippets in [../assets/client-config-snippets.md](../assets/client-config-snippets.md).
37
+
38
+ ## Verification Checklist
39
+
40
+ 1. Confirm server appears in client MCP list.
41
+ 2. Run a low-risk probe:
42
+ - Remote: `whoami` (recommended)
43
+ - Desktop-only contexts: `get_metadata` or `get_design_context` on a known node
44
+ 3. Confirm the authenticated user has access to the target Figma file.
45
+
46
+ ## First Prompt After Setup
47
+
48
+ - "Use Figma MCP `whoami` and show which account is authenticated."
49
+ - "Use Figma MCP `get_design_context` for this node URL and summarize dimensions, layout, and variables."
50
+
51
+ ## Notes
52
+
53
+ - If tools do not appear, restart MCP client session and retry authentication.
54
+ - If permissions fail, verify file sharing and plan seat alignment before retry.
@@ -0,0 +1,38 @@
1
+ # Tool and Function Catalog
2
+
3
+ ## Goal
4
+
5
+ Pick the smallest correct Figma MCP function for the task, with predictable inputs and output shape.
6
+
7
+ ## Start Here
8
+
9
+ 1. Read [../references/official-tools-matrix.md](../references/official-tools-matrix.md).
10
+ 2. Identify whether task is read, write, design-system search, code-connect, or diagram.
11
+ 3. Prefer read-first calls before write calls on unknown files.
12
+
13
+ ## Selection Rules
14
+
15
+ - Need full style/layout context for implementation: `get_design_context`.
16
+ - Output too large or structure-first pass: `get_metadata` first, then targeted `get_design_context`.
17
+ - Need variables/tokens only: `get_variable_defs`.
18
+ - Need visual reference: `get_screenshot`.
19
+ - Need FigJam extraction: `get_figjam`.
20
+ - Need design-system discovery before creating: `search_design_system`.
21
+ - Need to write/create/update content: `use_figma` (remote write workflows).
22
+ - Need new blank file: `create_new_file`.
23
+ - Need Mermaid-to-FigJam: `generate_diagram`.
24
+ - Need code-connect lookup/update: `get_code_connect_map`, `add_code_connect_map`, suggestion/confirm functions.
25
+
26
+ ## Prompting Pattern
27
+
28
+ Use direct tool-trigger language when selection is ambiguous:
29
+
30
+ - "Use Figma MCP `get_metadata` first, then `get_design_context` only for the selected child nodes."
31
+ - "Use Figma MCP `search_design_system` before creating any new component."
32
+ - "Use Figma MCP `create_new_file`, then `use_figma` to add a first frame and typography style."
33
+
34
+ ## Remote/Desktop Caveats
35
+
36
+ - Remote-only flags are tracked in the matrix; confirm availability before relying on those functions.
37
+ - Write-to-canvas flows are remote-first in current official guidance.
38
+ - Desktop mode is valid for local selection-based extraction, but with narrower workflow coverage.
@@ -0,0 +1,54 @@
1
+ # Troubleshooting and Recovery
2
+
3
+ ## Goal
4
+
5
+ Recover quickly from Figma MCP setup and execution failures without compounding errors.
6
+
7
+ ## Decision Flow
8
+
9
+ ```text
10
+ Failure observed?
11
+ -> Tools missing/not loading
12
+ -> Recheck MCP server config, restart client session, re-auth
13
+ -> Permission/resource error
14
+ -> Verify link format, run whoami, confirm seat/plan and file access
15
+ -> Rate-limit behavior
16
+ -> Reduce read call volume, stage calls, wait/backoff, upgrade seat/plan if needed
17
+ -> Write failure
18
+ -> Stop retries, inspect state with read tools, fix cause, retry scoped step
19
+ ```
20
+
21
+ ## Common Issues
22
+
23
+ ### Tools are not visible
24
+
25
+ - Confirm server endpoint is correct.
26
+ - Confirm MCP server is started in client.
27
+ - Re-authenticate and restart client session.
28
+
29
+ ### Permission denied or inaccessible resources
30
+
31
+ 1. Validate Figma URL format and node id.
32
+ 2. Run `whoami` to verify authenticated account.
33
+ 3. Confirm authenticated user belongs to correct plan and has file access.
34
+
35
+ ### Rate limiting
36
+
37
+ - Read-heavy calls are rate-limited by plan/seat.
38
+ - Use smaller selections and fewer repeated reads.
39
+ - Prefer `get_metadata` preflight before broad `get_design_context`.
40
+ - Batch intent into fewer, targeted calls.
41
+
42
+ ### Write step errors
43
+
44
+ - Do not immediately retry the exact same large request.
45
+ - Split into smaller write actions.
46
+ - Verify partial outcomes using read tools before next step.
47
+
48
+ ## Scoped Retry Pattern
49
+
50
+ 1. Isolate the failed step.
51
+ 2. Run one diagnostic read call (`get_metadata` or `whoami`).
52
+ 3. Correct only the failing input.
53
+ 4. Retry that one step.
54
+ 5. Re-validate and continue.
@@ -0,0 +1,76 @@
1
+ # Client Config Snippets
2
+
3
+ ## Remote Endpoint
4
+
5
+ `https://mcp.figma.com/mcp`
6
+
7
+ ## Desktop Endpoint
8
+
9
+ `http://127.0.0.1:3845/mcp`
10
+
11
+ ## Codex CLI (manual)
12
+
13
+ ```bash
14
+ codex mcp add figma --url https://mcp.figma.com/mcp
15
+ ```
16
+
17
+ ## VS Code `mcp.json` (remote)
18
+
19
+ ```json
20
+ {
21
+ "inputs": [],
22
+ "servers": {
23
+ "figma": {
24
+ "url": "https://mcp.figma.com/mcp",
25
+ "type": "http"
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ ## VS Code `mcp.json` (desktop)
32
+
33
+ ```json
34
+ {
35
+ "servers": {
36
+ "figma-desktop": {
37
+ "type": "http",
38
+ "url": "http://127.0.0.1:3845/mcp"
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## Cursor MCP config (remote)
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "figma": {
50
+ "url": "https://mcp.figma.com/mcp"
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Claude Code (manual remote)
57
+
58
+ ```bash
59
+ claude mcp add --transport http figma https://mcp.figma.com/mcp
60
+ claude mcp list
61
+ ```
62
+
63
+ ## Claude Code (manual desktop)
64
+
65
+ ```bash
66
+ claude mcp add --transport http figma-desktop http://127.0.0.1:3845/mcp
67
+ claude mcp list
68
+ ```
69
+
70
+ ## Gemini CLI
71
+
72
+ ```bash
73
+ gemini extensions install https://github.com/figma/mcp-server-guide
74
+ # then inside gemini CLI:
75
+ # /mcp auth figma
76
+ ```
@@ -0,0 +1,34 @@
1
+ # Prompt Recipes
2
+
3
+ ## Setup and Verification
4
+
5
+ - "Set up Figma MCP remote server and verify with `whoami`."
6
+ - "Use Figma MCP `whoami` and tell me which account and plans are active."
7
+
8
+ ## Read Context
9
+
10
+ - "Use `get_design_context` for this node URL and return layout, spacing, typography, and variables."
11
+ - "Use `get_metadata` first, then call `get_design_context` only for the button and header nodes."
12
+ - "Use `get_variable_defs` for this node and list token names with resolved values."
13
+
14
+ ## Design-System and Code Connect
15
+
16
+ - "Use `search_design_system` to find an existing card and button component before creating anything new."
17
+ - "Use `get_code_connect_map` for this node and show mapped source locations."
18
+ - "Use `add_code_connect_map` to map this Figma node to my component path."
19
+
20
+ ## First Canvas Creation
21
+
22
+ - "Use `create_new_file` to create a new design file named 'MCP First Canvas'."
23
+ - "Use `use_figma` to create one frame, add one title text layer, and apply auto layout spacing."
24
+ - "After the write, use `get_screenshot` to verify the result."
25
+
26
+ ## FigJam Flows
27
+
28
+ - "Use `create_new_file` to create a new FigJam board for architecture planning."
29
+ - "Use `generate_diagram` to create a sequence diagram for login and checkout flow."
30
+
31
+ ## Troubleshooting
32
+
33
+ - "I got a permission error. Use `whoami` and tell me what to check next."
34
+ - "I am rate-limited. Switch to a staged workflow with fewer `get_design_context` calls."
@@ -0,0 +1,42 @@
1
+ {
2
+ "id": "figma-mcp-0to1",
3
+ "title": "Figma MCP 0-to-1 Guide",
4
+ "description": "Guide users from Figma MCP installation and authentication through first canvas creation, with function-level tool coverage and operational recovery patterns.",
5
+ "portable": true,
6
+ "tags": ["figma", "mcp", "workflow", "design"],
7
+ "detectors": ["always"],
8
+ "detectionTriggers": ["manual"],
9
+ "installsFor": ["all"],
10
+ "agentSupport": ["codex", "claude", "cursor", "gemini", "copilot"],
11
+ "skillMetadata": {
12
+ "author": "skilly-hand",
13
+ "last-edit": "2026-04-03",
14
+ "license": "Apache-2.0",
15
+ "version": "1.0.0",
16
+ "changelog": "Added a complete Figma MCP setup-to-canvas guide with orchestrator subskills and full official tool matrix coverage; reduces onboarding ambiguity and improves first-run success rates; affects Figma MCP setup, tool selection, and troubleshooting workflows",
17
+ "auto-invoke": "Installing, configuring, or using Figma MCP from setup through first canvas creation",
18
+ "allowed-tools": [
19
+ "Read",
20
+ "Edit",
21
+ "Write",
22
+ "Glob",
23
+ "Grep",
24
+ "Bash",
25
+ "WebFetch",
26
+ "WebSearch",
27
+ "Task",
28
+ "SubAgent"
29
+ ]
30
+ },
31
+ "files": [
32
+ { "path": "SKILL.md", "kind": "instruction" },
33
+ { "path": "agents/install-auth.md", "kind": "instruction" },
34
+ { "path": "agents/tool-function-catalog.md", "kind": "instruction" },
35
+ { "path": "agents/canvas-creation-playbook.md", "kind": "instruction" },
36
+ { "path": "agents/troubleshooting-ops.md", "kind": "instruction" },
37
+ { "path": "assets/client-config-snippets.md", "kind": "asset" },
38
+ { "path": "assets/prompt-recipes.md", "kind": "asset" },
39
+ { "path": "references/official-tools-matrix.md", "kind": "reference" }
40
+ ],
41
+ "dependencies": []
42
+ }
@@ -0,0 +1,49 @@
1
+ # Official Figma MCP Tools Matrix (As of 2026-04-03)
2
+
3
+ This matrix tracks the official Figma MCP tool/function set in current Figma documentation.
4
+
5
+ ## Coverage Matrix
6
+
7
+ | Tool | Primary Purpose | Typical Input | Supported File Types | Availability Notes |
8
+ | --- | --- | --- | --- | --- |
9
+ | `generate_figma_design` | Generate design layers from live web UI (code to canvas path) | URL/local app capture context | No file context required | Remote-only in official docs. |
10
+ | `get_design_context` | Fetch rich design context for implementation | Figma URL or `fileKey` + `nodeId` | Figma Design | Read tool; use link-based node targeting. |
11
+ | `get_variable_defs` | Retrieve variable/token definitions | `fileKey` + `nodeId` | Figma Design | Best for token-only extraction. |
12
+ | `get_code_connect_map` | Read existing Code Connect mappings | `fileKey` + `nodeId` | Figma Design | Use to inspect mapping before updates. |
13
+ | `add_code_connect_map` | Add mapping from node to code component | `fileKey`, `nodeId`, source details | Figma Design | Write-style utility; exempt from read rate limits in docs. |
14
+ | `get_screenshot` | Render node screenshot for visual verification | `fileKey` + `nodeId` | Figma Design | Useful verification after write steps. |
15
+ | `create_design_system_rules` | Generate design-system rules prompt/scaffold | Framework/language context | No file context required | Use to enforce repeatable design-system workflows. |
16
+ | `get_metadata` | Sparse XML structure (ids, names, hierarchy, bounds) | Selection or `fileKey` + `nodeId` | Figma Design | Preferred preflight for large contexts. |
17
+ | `get_figjam` | Fetch FigJam metadata (and node visuals) | `fileKey` + `nodeId` | FigJam | FigJam-specific extraction. |
18
+ | `generate_diagram` | Create FigJam diagram from Mermaid or natural language intent | Diagram description or Mermaid syntax | No file context required | Supports flowchart, gantt, state, sequence. |
19
+ | `whoami` | Show authenticated Figma identity, plans, and seat types | None | No file context required | Remote-only in official docs; exempt from read rate limits. |
20
+ | `get_code_connect_suggestions` | Suggest candidate node-to-code mappings | File context + repo context | Figma Design | Usually part of Code Connect workflows. |
21
+ | `send_code_connect_mappings` | Confirm/persist mapping suggestions | Suggestions payload | Figma Design | Follow-up action after suggestion generation. |
22
+ | `use_figma` | General-purpose write/edit/delete/inspect in Figma via Plugin API workflow | Task instructions and file context | Figma Design, FigJam | Write-to-canvas workflow path; remote-first guidance. |
23
+ | `search_design_system` | Search libraries for components, variables, styles | Query text and optional type narrowing | Figma Design | Use before creating new artifacts. |
24
+ | `create_new_file` | Create new blank Design or FigJam file | Name/type intent | No file context required | Good first step for 0-to-1 creation workflows. |
25
+
26
+ ## Rate-Limit Notes (Official)
27
+
28
+ - Read tools are subject to seat/plan limits.
29
+ - Officially documented exempt tools include:
30
+ - `add_code_connect_map`
31
+ - `generate_figma_design`
32
+ - `whoami`
33
+
34
+ ## Practical Selection Heuristics
35
+
36
+ 1. Start with `whoami` when authentication or permissions are uncertain.
37
+ 2. Use `get_metadata` before `get_design_context` for large files.
38
+ 3. Use `search_design_system` before creating new components.
39
+ 4. Keep `use_figma` calls small and validate after each step.
40
+ 5. Use `get_screenshot` as final visual verification after edits.
41
+
42
+ ## Sources
43
+
44
+ - https://developers.figma.com/docs/figma-mcp-server/
45
+ - https://developers.figma.com/docs/figma-mcp-server/tools-and-prompts/
46
+ - https://developers.figma.com/docs/figma-mcp-server/remote-server-installation/
47
+ - https://developers.figma.com/docs/figma-mcp-server/local-server-installation/
48
+ - https://developers.figma.com/docs/figma-mcp-server/plans-access-and-permissions/
49
+ - https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Figma-MCP-server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilly-hand/skilly-hand",
3
- "version": "0.1.1",
3
+ "version": "0.3.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
+ }