@skilly-hand/skilly-hand 0.4.0 → 0.6.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 +33 -0
- package/README.md +11 -0
- package/catalog/README.md +1 -0
- package/catalog/catalog-index.json +1 -0
- package/catalog/skills/angular-guidelines/SKILL.md +176 -0
- package/catalog/skills/angular-guidelines/agents/angular-tester.md +130 -0
- package/catalog/skills/angular-guidelines/agents/component-creator.md +124 -0
- package/catalog/skills/angular-guidelines/manifest.json +41 -0
- package/catalog/skills/figma-mcp-0to1/manifest.json +8 -2
- package/catalog/skills/spec-driven-development/SKILL.md +25 -0
- package/catalog/skills/spec-driven-development/manifest.json +8 -2
- package/package.json +4 -1
- package/packages/cli/src/bin.js +345 -85
- package/packages/core/src/index.js +42 -8
- package/packages/core/src/ui/brand.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,39 @@ All notable changes to this project are documented in this file.
|
|
|
16
16
|
### Removed
|
|
17
17
|
- _None._
|
|
18
18
|
|
|
19
|
+
## [0.6.0] - 2026-04-03
|
|
20
|
+
[View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.6.0)
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Added portable skill `angular-guidelines` for Angular-only generation/review workflows, including mandatory latest-stable preflight checks and modern Angular best-practice defaults.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- _None._
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- _None._
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
- _None._
|
|
33
|
+
|
|
34
|
+
## [0.5.0] - 2026-04-03
|
|
35
|
+
[View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.5.0)
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- Interactive command launcher when running `npx skilly-hand` in a TTY, including install skill/agent selection flow.
|
|
39
|
+
- New `selectedSkillIds` install path for explicitly choosing portable skills.
|
|
40
|
+
- Comprehensive CLI interaction tests in `tests/interactive-cli.test.js`.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- Help, docs, and install/uninstall confirmation messaging now reflect current behavior and naming (`skilly-hand` branding).
|
|
44
|
+
- CLI bin execution mode and command routing were refactored into testable `runCli`/service helpers.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- Non-interactive invocation without a command now defaults to install output instead of opening prompts.
|
|
48
|
+
|
|
49
|
+
### Removed
|
|
50
|
+
- _None._
|
|
51
|
+
|
|
19
52
|
## [0.4.0] - 2026-04-03
|
|
20
53
|
[View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.4.0)
|
|
21
54
|
|
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ npm install
|
|
|
37
37
|
npx skilly-hand
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
`npx skilly-hand` opens an interactive command launcher when running in a TTY.
|
|
41
|
+
|
|
40
42
|
---
|
|
41
43
|
|
|
42
44
|
## Commands
|
|
@@ -49,6 +51,14 @@ npx skilly-hand
|
|
|
49
51
|
| `npx skilly-hand doctor` | Diagnose installation and configuration issues |
|
|
50
52
|
| `npx skilly-hand uninstall` | Remove installed skills |
|
|
51
53
|
|
|
54
|
+
### Common Flags
|
|
55
|
+
|
|
56
|
+
| Flag | Description |
|
|
57
|
+
| ---- | ----------- |
|
|
58
|
+
| `--json` | Emit machine-readable output and disable interactive prompts |
|
|
59
|
+
| `--yes`, `-y` | Skip confirmation prompts for mutating commands (`install`, `uninstall`) |
|
|
60
|
+
| `--dry-run` | Preview install plan without writing files |
|
|
61
|
+
|
|
52
62
|
---
|
|
53
63
|
|
|
54
64
|
## Current Portable Catalog
|
|
@@ -56,6 +66,7 @@ npx skilly-hand
|
|
|
56
66
|
The catalog currently includes:
|
|
57
67
|
|
|
58
68
|
- `agents-root-orchestrator`
|
|
69
|
+
- `angular-guidelines`
|
|
59
70
|
- `figma-mcp-0to1`
|
|
60
71
|
- `skill-creator`
|
|
61
72
|
- `spec-driven-development`
|
package/catalog/README.md
CHANGED
|
@@ -5,6 +5,7 @@ Published portable skills consumed by the `skilly-hand` CLI.
|
|
|
5
5
|
| Skill | Description | Tags | Installs For |
|
|
6
6
|
| ----- | ----------- | ---- | ------------ |
|
|
7
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
|
+
| `angular-guidelines` | Guide Angular code generation and review using latest stable Angular verification and modern framework best practices. | angular, frontend, workflow, best-practices | all |
|
|
8
9
|
| `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
10
|
| `skill-creator` | Create and standardize AI skills with reusable structure, metadata rules, and templates. | core, workflow, authoring | all |
|
|
10
11
|
| `spec-driven-development` | Plan, execute, and verify multi-step work through versioned specs with small, testable tasks. | core, workflow, planning | all |
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Angular Guidelines
|
|
2
|
+
|
|
3
|
+
## When to Use
|
|
4
|
+
|
|
5
|
+
Use this skill when:
|
|
6
|
+
|
|
7
|
+
- You are generating Angular components, directives, pipes, services, or supporting files.
|
|
8
|
+
- You are refactoring existing Angular code to current framework patterns.
|
|
9
|
+
- You are reviewing Angular code quality and framework-alignment in an Angular workspace.
|
|
10
|
+
|
|
11
|
+
Do not use this skill for:
|
|
12
|
+
|
|
13
|
+
- Non-Angular frontend stacks (React, Vue, Svelte, or framework-agnostic UI tasks).
|
|
14
|
+
- Deep architecture decisions outside code artifact generation/review scope.
|
|
15
|
+
- Pure test-strategy design unrelated to Angular implementation details.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Routing Map
|
|
20
|
+
|
|
21
|
+
Choose sub-agents by intent:
|
|
22
|
+
|
|
23
|
+
| Intent | Sub-agent |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| Create, refactor, or review Angular components | [agents/component-creator.md](agents/component-creator.md) |
|
|
26
|
+
| Write or review Angular tests | [agents/angular-tester.md](agents/angular-tester.md) |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Standard Execution Sequence
|
|
31
|
+
|
|
32
|
+
1. Run latest stable Angular preflight checks.
|
|
33
|
+
2. Route to the smallest matching sub-agent by task intent.
|
|
34
|
+
3. Apply the sub-agent checklist before finalizing generated code or review output.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Critical Patterns
|
|
39
|
+
|
|
40
|
+
### Pattern 1: Latest Stable Angular Preflight (Mandatory)
|
|
41
|
+
|
|
42
|
+
Before generating or changing Angular code:
|
|
43
|
+
|
|
44
|
+
1. Check the latest stable Angular core release:
|
|
45
|
+
`npm view @angular/core version`
|
|
46
|
+
2. Check the project's installed or declared Angular version:
|
|
47
|
+
`npm ls @angular/core` or inspect `package.json`.
|
|
48
|
+
3. If versions diverge, generate content for the latest stable APIs and call out upgrade steps.
|
|
49
|
+
4. If npm metadata is unavailable, verify against official Angular release sources before proceeding.
|
|
50
|
+
|
|
51
|
+
Never hardcode a specific Angular major as the default baseline.
|
|
52
|
+
|
|
53
|
+
### Pattern 2: Modern Angular Defaults for New Code
|
|
54
|
+
|
|
55
|
+
Use these defaults unless project constraints explicitly prevent them:
|
|
56
|
+
|
|
57
|
+
| Area | Default |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| Component model | Standalone-first (`standalone: true`) |
|
|
60
|
+
| State + bindings | Signals (`signal`, `computed`, `input`, `output`) |
|
|
61
|
+
| Template flow | `@if`, `@for`, `@switch` control flow blocks |
|
|
62
|
+
| Dependency injection | `inject()` over constructor injection for new code |
|
|
63
|
+
| Forms | Typed reactive forms |
|
|
64
|
+
| Rendering strategy | OnPush-friendly patterns and deferred/lazy rendering where appropriate |
|
|
65
|
+
|
|
66
|
+
### Pattern 3: Generation and Review Guardrails
|
|
67
|
+
|
|
68
|
+
- Keep generated files focused and minimal for the requested artifact.
|
|
69
|
+
- Prefer framework-native patterns over custom abstractions unless required by repo conventions.
|
|
70
|
+
- Call out deprecated patterns in reviewed code and suggest modern Angular replacements.
|
|
71
|
+
- For component-specific work, apply [agents/component-creator.md](agents/component-creator.md).
|
|
72
|
+
- For testing-specific work, apply [agents/angular-tester.md](agents/angular-tester.md).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Decision Tree
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
Is this an Angular project (angular.json or @angular/core present)?
|
|
80
|
+
NO -> Do not apply this skill
|
|
81
|
+
YES -> Continue
|
|
82
|
+
|
|
83
|
+
Is this a create/generate task?
|
|
84
|
+
YES -> Run latest stable preflight, then generate with modern defaults
|
|
85
|
+
NO -> Continue
|
|
86
|
+
|
|
87
|
+
Is this a refactor task?
|
|
88
|
+
YES -> Preserve behavior, migrate incrementally to modern Angular patterns
|
|
89
|
+
NO -> Continue
|
|
90
|
+
|
|
91
|
+
Is this a review task?
|
|
92
|
+
YES -> Validate latest-stable alignment + best-practice checklist
|
|
93
|
+
NO -> Apply the minimal Angular guidance needed for the request
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Code Examples
|
|
99
|
+
|
|
100
|
+
### Example 1: Standalone + Signals Component
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { ChangeDetectionStrategy, Component, computed, input, output } from "@angular/core";
|
|
104
|
+
|
|
105
|
+
@Component({
|
|
106
|
+
selector: "app-badge",
|
|
107
|
+
standalone: true,
|
|
108
|
+
template: `
|
|
109
|
+
<span [attr.data-tone]="tone()">{{ label() }}</span>
|
|
110
|
+
@if (showCount()) {
|
|
111
|
+
<small>{{ count() }}</small>
|
|
112
|
+
}
|
|
113
|
+
`,
|
|
114
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
115
|
+
})
|
|
116
|
+
export class BadgeComponent {
|
|
117
|
+
readonly label = input.required<string>();
|
|
118
|
+
readonly count = input(0);
|
|
119
|
+
readonly increment = output<void>();
|
|
120
|
+
readonly showCount = computed(() => this.count() > 0);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Example 2: Service with `inject()`
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { Injectable, inject } from "@angular/core";
|
|
128
|
+
import { HttpClient } from "@angular/common/http";
|
|
129
|
+
|
|
130
|
+
@Injectable({ providedIn: "root" })
|
|
131
|
+
export class ProfileService {
|
|
132
|
+
private readonly http = inject(HttpClient);
|
|
133
|
+
|
|
134
|
+
getProfile() {
|
|
135
|
+
return this.http.get("/api/profile");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Review Checklist
|
|
143
|
+
|
|
144
|
+
- Latest stable Angular preflight was completed before code generation/refactor.
|
|
145
|
+
- New artifacts use standalone-first + signal-first patterns where applicable.
|
|
146
|
+
- Template control flow uses modern block syntax.
|
|
147
|
+
- DI and forms follow modern typed Angular practices.
|
|
148
|
+
- Output avoids deprecated Angular APIs unless needed for compatibility.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Commands
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Latest stable Angular version
|
|
156
|
+
npm view @angular/core version
|
|
157
|
+
|
|
158
|
+
# Workspace Angular version
|
|
159
|
+
npm ls @angular/core
|
|
160
|
+
|
|
161
|
+
# Create a standalone component
|
|
162
|
+
ng generate component <name> --standalone
|
|
163
|
+
|
|
164
|
+
# Apply Angular framework updates in a workspace
|
|
165
|
+
ng update @angular/core @angular/cli
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Resources
|
|
171
|
+
|
|
172
|
+
- Angular docs: https://angular.dev
|
|
173
|
+
- Angular API reference: https://angular.dev/api
|
|
174
|
+
- Angular update guide: https://angular.dev/update-guide
|
|
175
|
+
- Angular blog (official releases): https://blog.angular.dev
|
|
176
|
+
- Angular GitHub releases: https://github.com/angular/angular/releases
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Angular Tester
|
|
2
|
+
|
|
3
|
+
## When to Use
|
|
4
|
+
|
|
5
|
+
Use this sub-agent when:
|
|
6
|
+
|
|
7
|
+
- Adding or updating Angular component/service tests.
|
|
8
|
+
- Reviewing Angular tests for correctness and modern framework usage.
|
|
9
|
+
- Choosing test patterns for async, signal-based, or HTTP-driven behaviors.
|
|
10
|
+
|
|
11
|
+
Do not use this sub-agent for:
|
|
12
|
+
|
|
13
|
+
- Component implementation guidance without tests (use `component-creator`).
|
|
14
|
+
- Framework-agnostic testing standards not tied to Angular behavior.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Runner Policy
|
|
19
|
+
|
|
20
|
+
- Default: Vitest-first in Angular workspaces.
|
|
21
|
+
- Fallback: Use Jasmine/Karma patterns only when the workspace is already configured for Jasmine.
|
|
22
|
+
- Do not force runner migration inside unrelated feature tasks.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Core Testing Patterns
|
|
27
|
+
|
|
28
|
+
- Configure `TestBed` with standalone imports for component tests.
|
|
29
|
+
- Test signal and computed behavior directly when possible.
|
|
30
|
+
- Test signal inputs/outputs using component instance APIs and DOM-triggered events.
|
|
31
|
+
- Cover async behavior with `fakeAsync`/`tick`/`flush` or `waitForAsync`/`whenStable`.
|
|
32
|
+
- For HTTP paths, use `HttpClientTestingModule`/`HttpTestingController` and verify requests.
|
|
33
|
+
- For services, prefer explicit DI setup and clear mocking boundaries.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Command Matrix
|
|
38
|
+
|
|
39
|
+
| Scenario | Preferred Command | Fallback |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| Run all unit tests | `ng test` | Existing workspace test command |
|
|
42
|
+
| Watch mode | `ng test --watch` | Workspace equivalent |
|
|
43
|
+
| Coverage | `ng test --code-coverage` | Workspace equivalent |
|
|
44
|
+
| CI-style single run | `ng test --watch=false` | Workspace equivalent |
|
|
45
|
+
|
|
46
|
+
If the workspace uses Jasmine/Karma, keep command behavior aligned with existing runner configuration.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Decision Tree
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
Is the target under test a standalone component?
|
|
54
|
+
YES -> Use TestBed with imports: [ComponentUnderTest]
|
|
55
|
+
NO -> Continue
|
|
56
|
+
|
|
57
|
+
Is this primarily service logic?
|
|
58
|
+
YES -> Configure TestBed providers/injection and mock dependencies
|
|
59
|
+
NO -> Continue
|
|
60
|
+
|
|
61
|
+
Is behavior async (timers, promises, stabilization)?
|
|
62
|
+
YES -> Use fakeAsync/tick or waitForAsync/whenStable
|
|
63
|
+
NO -> Continue
|
|
64
|
+
|
|
65
|
+
Does behavior involve HTTP or resource loading?
|
|
66
|
+
YES -> Use HttpTestingController and assert request + response flow
|
|
67
|
+
NO -> Use direct synchronous assertions
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Test Snippets
|
|
73
|
+
|
|
74
|
+
### Standalone Component Test
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
|
78
|
+
import { describe, expect, it, beforeEach } from "vitest";
|
|
79
|
+
import { CounterComponent } from "./counter.component";
|
|
80
|
+
|
|
81
|
+
describe("CounterComponent", () => {
|
|
82
|
+
let fixture: ComponentFixture<CounterComponent>;
|
|
83
|
+
|
|
84
|
+
beforeEach(async () => {
|
|
85
|
+
await TestBed.configureTestingModule({
|
|
86
|
+
imports: [CounterComponent]
|
|
87
|
+
}).compileComponents();
|
|
88
|
+
fixture = TestBed.createComponent(CounterComponent);
|
|
89
|
+
fixture.detectChanges();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("renders default count", () => {
|
|
93
|
+
expect(fixture.componentInstance.count()).toBe(0);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### HTTP Service Test
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { TestBed } from "@angular/core/testing";
|
|
102
|
+
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
|
|
103
|
+
|
|
104
|
+
describe("ProfileService", () => {
|
|
105
|
+
it("requests profile", () => {
|
|
106
|
+
TestBed.configureTestingModule({
|
|
107
|
+
imports: [HttpClientTestingModule]
|
|
108
|
+
});
|
|
109
|
+
const service = TestBed.inject(ProfileService);
|
|
110
|
+
const httpMock = TestBed.inject(HttpTestingController);
|
|
111
|
+
|
|
112
|
+
service.getProfile().subscribe();
|
|
113
|
+
|
|
114
|
+
const req = httpMock.expectOne("/api/profile");
|
|
115
|
+
expect(req.request.method).toBe("GET");
|
|
116
|
+
req.flush({ id: "1" });
|
|
117
|
+
httpMock.verify();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Review Checklist
|
|
125
|
+
|
|
126
|
+
- Tests follow the workspace runner policy (Vitest-first, Jasmine fallback only when preconfigured).
|
|
127
|
+
- Standalone Angular artifacts are tested with appropriate TestBed setup.
|
|
128
|
+
- Signal/input/output behavior is asserted explicitly.
|
|
129
|
+
- Async and HTTP behavior use deterministic Angular testing utilities.
|
|
130
|
+
- Assertions verify user-visible behavior or contract-level outcomes, not incidental internals.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Angular Component Creator
|
|
2
|
+
|
|
3
|
+
## When to Use
|
|
4
|
+
|
|
5
|
+
Use this sub-agent when:
|
|
6
|
+
|
|
7
|
+
- Creating a new Angular component.
|
|
8
|
+
- Refactoring an existing Angular component to modern patterns.
|
|
9
|
+
- Reviewing component implementation details and template patterns.
|
|
10
|
+
|
|
11
|
+
Do not use this sub-agent for:
|
|
12
|
+
|
|
13
|
+
- Directive, pipe, or service implementation.
|
|
14
|
+
- Repository-wide architecture decisions.
|
|
15
|
+
- Test-only tasks (use `angular-tester`).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Core Rules
|
|
20
|
+
|
|
21
|
+
- Keep explicit `standalone: true` in generated component metadata.
|
|
22
|
+
- Use signal APIs for component IO and state (`input`, `output`, `signal`, `computed`).
|
|
23
|
+
- Use native template control flow blocks (`@if`, `@for`, `@switch`).
|
|
24
|
+
- Prefer `host` object bindings/events over decorator-based host metadata.
|
|
25
|
+
- Use OnPush-friendly reactive patterns and avoid zone-dependent assumptions.
|
|
26
|
+
- For interactive components, include keyboard and ARIA semantics in host bindings.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Template Snippets
|
|
31
|
+
|
|
32
|
+
### Minimal Standalone Component
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
|
36
|
+
|
|
37
|
+
@Component({
|
|
38
|
+
selector: "app-example",
|
|
39
|
+
standalone: true,
|
|
40
|
+
template: `<p>Example works</p>`,
|
|
41
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
42
|
+
})
|
|
43
|
+
export class ExampleComponent {}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Signal Inputs and Outputs
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { Component, input, output, computed } from "@angular/core";
|
|
50
|
+
|
|
51
|
+
@Component({
|
|
52
|
+
selector: "app-counter",
|
|
53
|
+
standalone: true,
|
|
54
|
+
template: `<button (click)="increment.emit()">{{ label() }} {{ count() }}</button>`
|
|
55
|
+
})
|
|
56
|
+
export class CounterComponent {
|
|
57
|
+
readonly label = input.required<string>();
|
|
58
|
+
readonly count = input(0);
|
|
59
|
+
readonly increment = output<void>();
|
|
60
|
+
readonly isNonZero = computed(() => this.count() > 0);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Host Bindings and Events
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
@Component({
|
|
68
|
+
selector: "button[appActionButton]",
|
|
69
|
+
standalone: true,
|
|
70
|
+
template: `<ng-content />`,
|
|
71
|
+
host: {
|
|
72
|
+
"role": "button",
|
|
73
|
+
"[attr.aria-disabled]": "disabled() ? 'true' : null",
|
|
74
|
+
"[class.is-disabled]": "disabled()",
|
|
75
|
+
"(click)": "onClick($event)",
|
|
76
|
+
"(keydown.enter)": "onClick($event)"
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
export class ActionButtonComponent {
|
|
80
|
+
readonly disabled = input(false);
|
|
81
|
+
readonly pressed = output<void>();
|
|
82
|
+
|
|
83
|
+
onClick(event: Event) {
|
|
84
|
+
if (this.disabled()) {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.pressed.emit();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Content Projection and Render-Safe Hooks
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { Component, afterNextRender } from "@angular/core";
|
|
97
|
+
|
|
98
|
+
@Component({
|
|
99
|
+
selector: "app-card",
|
|
100
|
+
standalone: true,
|
|
101
|
+
template: `
|
|
102
|
+
<header><ng-content select="[card-header]" /></header>
|
|
103
|
+
<section><ng-content /></section>
|
|
104
|
+
<footer><ng-content select="[card-footer]" /></footer>
|
|
105
|
+
`
|
|
106
|
+
})
|
|
107
|
+
export class CardComponent {
|
|
108
|
+
constructor() {
|
|
109
|
+
afterNextRender(() => {
|
|
110
|
+
// Run post-render DOM logic only when needed.
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Review Checklist
|
|
119
|
+
|
|
120
|
+
- Component metadata includes explicit `standalone: true`.
|
|
121
|
+
- Inputs/outputs use modern signal-based APIs where applicable.
|
|
122
|
+
- Templates use native control flow blocks, not legacy structural directives.
|
|
123
|
+
- Host interaction includes required ARIA and keyboard semantics for interactive UI.
|
|
124
|
+
- Change detection and state flow are OnPush-friendly and predictable.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "angular-guidelines",
|
|
3
|
+
"title": "Angular Guidelines",
|
|
4
|
+
"description": "Guide Angular code generation and review using latest stable Angular verification and modern framework best practices.",
|
|
5
|
+
"portable": true,
|
|
6
|
+
"tags": ["angular", "frontend", "workflow", "best-practices"],
|
|
7
|
+
"detectors": ["angular"],
|
|
8
|
+
"detectionTriggers": ["auto"],
|
|
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.1.1",
|
|
16
|
+
"changelog": "Added allowed-modes metadata to declare angular-guidelines sub-agent routing targets; improves discoverability of component-creator and angular-tester delegation modes; affects angular-guidelines manifest metadata",
|
|
17
|
+
"auto-invoke": "Generating, reviewing, or refactoring Angular code artifacts in Angular projects",
|
|
18
|
+
"allowed-modes": [
|
|
19
|
+
"component-creator",
|
|
20
|
+
"angular-tester"
|
|
21
|
+
],
|
|
22
|
+
"allowed-tools": [
|
|
23
|
+
"Read",
|
|
24
|
+
"Edit",
|
|
25
|
+
"Write",
|
|
26
|
+
"Glob",
|
|
27
|
+
"Grep",
|
|
28
|
+
"Bash",
|
|
29
|
+
"WebFetch",
|
|
30
|
+
"WebSearch",
|
|
31
|
+
"Task",
|
|
32
|
+
"SubAgent"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
{ "path": "SKILL.md", "kind": "instruction" },
|
|
37
|
+
{ "path": "agents/component-creator.md", "kind": "instruction" },
|
|
38
|
+
{ "path": "agents/angular-tester.md", "kind": "instruction" }
|
|
39
|
+
],
|
|
40
|
+
"dependencies": []
|
|
41
|
+
}
|
|
@@ -12,9 +12,15 @@
|
|
|
12
12
|
"author": "skilly-hand",
|
|
13
13
|
"last-edit": "2026-04-03",
|
|
14
14
|
"license": "Apache-2.0",
|
|
15
|
-
"version": "1.0.
|
|
16
|
-
"changelog": "Added
|
|
15
|
+
"version": "1.0.1",
|
|
16
|
+
"changelog": "Added allowed-modes metadata to declare figma-mcp-0to1 sub-agent routing targets; improves discoverability of install-auth, tool-function-catalog, canvas-creation-playbook, and troubleshooting-ops delegation modes; affects figma-mcp-0to1 manifest metadata",
|
|
17
17
|
"auto-invoke": "Installing, configuring, or using Figma MCP from setup through first canvas creation",
|
|
18
|
+
"allowed-modes": [
|
|
19
|
+
"install-auth",
|
|
20
|
+
"tool-function-catalog",
|
|
21
|
+
"canvas-creation-playbook",
|
|
22
|
+
"troubleshooting-ops"
|
|
23
|
+
],
|
|
18
24
|
"allowed-tools": [
|
|
19
25
|
"Read",
|
|
20
26
|
"Edit",
|
|
@@ -33,6 +33,31 @@ Recommended task size:
|
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
|
+
## OpenSpec Complementary Support
|
|
37
|
+
|
|
38
|
+
Default execution SHOULD remain the local `.sdd` workflow.
|
|
39
|
+
|
|
40
|
+
Recur to OpenSpec support when the task needs complementary structure for:
|
|
41
|
+
|
|
42
|
+
- Multi-session continuity where planning context must persist across chats.
|
|
43
|
+
- Shareable planning artifacts for review before implementation.
|
|
44
|
+
- Requirement-delta clarity that benefits from explicit change proposals.
|
|
45
|
+
|
|
46
|
+
Routing rules:
|
|
47
|
+
|
|
48
|
+
- Keep the local `.sdd/active/<feature-name>/spec.md` as the execution source of truth unless the team explicitly standardizes on OpenSpec paths.
|
|
49
|
+
- If OpenSpec is unavailable, continue in `.sdd` and document assumptions directly in the active spec.
|
|
50
|
+
|
|
51
|
+
| Use local SDD only | Use OpenSpec support |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| Single-session or straightforward work with clear requirements | Work spans multiple sessions and needs persistent planning context |
|
|
54
|
+
| Existing `.sdd` artifacts already provide enough review clarity | Team needs proposal/design/tasks artifacts for async review |
|
|
55
|
+
| Requirement changes are small and easy to track in-place | Requirement deltas are complex and need explicit change framing |
|
|
56
|
+
|
|
57
|
+
Reference (informational): [https://openspec.dev/](https://openspec.dev/)
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
36
61
|
## Spec Structure
|
|
37
62
|
|
|
38
63
|
A practical spec includes:
|
|
@@ -12,9 +12,15 @@
|
|
|
12
12
|
"author": "skilly-hand",
|
|
13
13
|
"last-edit": "2026-04-03",
|
|
14
14
|
"license": "Apache-2.0",
|
|
15
|
-
"version": "1.0.
|
|
16
|
-
"changelog": "Added
|
|
15
|
+
"version": "1.0.3",
|
|
16
|
+
"changelog": "Added OpenSpec complementary support routing guidance to spec-driven-development instructions; improves planning continuity and review clarity when local SDD needs reinforcement; affects spec-driven-development SKILL guidance and manifest metadata",
|
|
17
17
|
"auto-invoke": "Planning or executing feature work, bug fixes, and multi-phase implementation",
|
|
18
|
+
"allowed-modes": [
|
|
19
|
+
"plan",
|
|
20
|
+
"apply",
|
|
21
|
+
"verify",
|
|
22
|
+
"orchestrate"
|
|
23
|
+
],
|
|
18
24
|
"allowed-tools": [
|
|
19
25
|
"Read",
|
|
20
26
|
"Edit",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skilly-hand/skilly-hand",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "CC-BY-NC-4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -40,5 +40,8 @@
|
|
|
40
40
|
"detect": "node ./packages/cli/src/bin.js detect",
|
|
41
41
|
"list": "node ./packages/cli/src/bin.js list",
|
|
42
42
|
"doctor": "node ./packages/cli/src/bin.js doctor"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@inquirer/prompts": "^7.10.1"
|
|
43
46
|
}
|
|
44
47
|
}
|
package/packages/cli/src/bin.js
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { checkbox as inquirerCheckbox, confirm as inquirerConfirm, select as inquirerSelect } from "@inquirer/prompts";
|
|
4
6
|
import { loadAllSkills } from "../../catalog/src/index.js";
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_AGENTS,
|
|
9
|
+
installProject,
|
|
10
|
+
resolveSkillSelection,
|
|
11
|
+
runDoctor,
|
|
12
|
+
uninstallProject
|
|
13
|
+
} from "../../core/src/index.js";
|
|
6
14
|
import { createTerminalRenderer } from "../../core/src/terminal.js";
|
|
7
15
|
import { detectProject } from "../../detectors/src/index.js";
|
|
8
16
|
|
|
9
17
|
const require = createRequire(import.meta.url);
|
|
10
18
|
const { version } = require("../../../package.json");
|
|
11
19
|
|
|
12
|
-
|
|
20
|
+
function isExecutedDirectly(metaUrl, argv1) {
|
|
21
|
+
if (!argv1) return false;
|
|
22
|
+
return metaUrl === pathToFileURL(argv1).href;
|
|
23
|
+
}
|
|
13
24
|
|
|
14
|
-
function parseArgs(argv) {
|
|
25
|
+
export function parseArgs(argv) {
|
|
15
26
|
const args = [...argv];
|
|
16
27
|
const positional = [];
|
|
17
28
|
const flags = {
|
|
@@ -54,9 +65,10 @@ function parseArgs(argv) {
|
|
|
54
65
|
return { command: positional[0], flags };
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
function buildHelpText() {
|
|
68
|
+
function buildHelpText(renderer, appVersion) {
|
|
58
69
|
const usage = renderer.section("Usage", renderer.list([
|
|
59
|
-
"npx skilly-hand
|
|
70
|
+
"npx skilly-hand # interactive launcher when running in a TTY",
|
|
71
|
+
"npx skilly-hand install",
|
|
60
72
|
"npx skilly-hand detect",
|
|
61
73
|
"npx skilly-hand list",
|
|
62
74
|
"npx skilly-hand doctor",
|
|
@@ -66,7 +78,7 @@ function buildHelpText() {
|
|
|
66
78
|
const flags = renderer.section("Flags", renderer.list([
|
|
67
79
|
"--dry-run Show install plan without writing files",
|
|
68
80
|
"--json Emit stable JSON output for automation",
|
|
69
|
-
"--yes, -y
|
|
81
|
+
"--yes, -y Skip install/uninstall confirmations",
|
|
70
82
|
"--verbose, -v Reserved for future debug detail",
|
|
71
83
|
"--agent, -a <name> codex|claude|cursor|gemini|copilot (repeatable)",
|
|
72
84
|
"--cwd <path> Project root (defaults to current directory)",
|
|
@@ -76,21 +88,22 @@ function buildHelpText() {
|
|
|
76
88
|
], { bullet: "-" }));
|
|
77
89
|
|
|
78
90
|
const examples = renderer.section("Examples", renderer.list([
|
|
91
|
+
"npx skilly-hand",
|
|
79
92
|
"npx skilly-hand install --dry-run",
|
|
80
93
|
"npx skilly-hand detect --json",
|
|
81
94
|
"npx skilly-hand install --agent codex --agent claude",
|
|
82
|
-
"npx skilly-hand
|
|
95
|
+
"npx skilly-hand uninstall --yes"
|
|
83
96
|
], { bullet: "-" }));
|
|
84
97
|
|
|
85
98
|
return renderer.joinBlocks([
|
|
86
|
-
renderer.banner(
|
|
99
|
+
renderer.banner(appVersion),
|
|
87
100
|
usage,
|
|
88
101
|
flags,
|
|
89
102
|
examples
|
|
90
103
|
]);
|
|
91
104
|
}
|
|
92
105
|
|
|
93
|
-
function printInstallResult(result, flags) {
|
|
106
|
+
function printInstallResult(renderer, appVersion, result, flags) {
|
|
94
107
|
const mode = flags.dryRun ? "dry-run" : "apply";
|
|
95
108
|
const preflight = renderer.section(
|
|
96
109
|
"Install Preflight",
|
|
@@ -135,19 +148,19 @@ function printInstallResult(result, flags) {
|
|
|
135
148
|
|
|
136
149
|
const nextSteps = result.applied
|
|
137
150
|
? renderer.nextSteps([
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
151
|
+
"Review generated AGENTS and assistant instruction files.",
|
|
152
|
+
"Run `npx skilly-hand doctor` to validate installation health.",
|
|
153
|
+
"Use `npx skilly-hand uninstall` to restore backed-up files if needed."
|
|
154
|
+
])
|
|
142
155
|
: renderer.nextSteps([
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
156
|
+
"Run `npx skilly-hand install` to apply this plan.",
|
|
157
|
+
"Adjust `--include` and `--exclude` tags to tune skill selection."
|
|
158
|
+
]);
|
|
146
159
|
|
|
147
|
-
renderer.write(renderer.joinBlocks([renderer.banner(
|
|
160
|
+
renderer.write(renderer.joinBlocks([renderer.banner(appVersion), preflight, detections, skills, status, nextSteps]));
|
|
148
161
|
}
|
|
149
162
|
|
|
150
|
-
function printDetectResult(cwd, detections) {
|
|
163
|
+
function printDetectResult(renderer, cwd, detections) {
|
|
151
164
|
const summary = renderer.section(
|
|
152
165
|
"Detection Summary",
|
|
153
166
|
renderer.kv([
|
|
@@ -166,7 +179,7 @@ function printDetectResult(cwd, detections) {
|
|
|
166
179
|
renderer.write(renderer.joinBlocks([summary, findings]));
|
|
167
180
|
}
|
|
168
181
|
|
|
169
|
-
function printListResult(skills) {
|
|
182
|
+
function printListResult(renderer, skills) {
|
|
170
183
|
const summary = renderer.section(
|
|
171
184
|
"Catalog Summary",
|
|
172
185
|
renderer.kv([["Skills available", String(skills.length)]])
|
|
@@ -193,7 +206,7 @@ function printListResult(skills) {
|
|
|
193
206
|
renderer.write(renderer.joinBlocks([summary, table]));
|
|
194
207
|
}
|
|
195
208
|
|
|
196
|
-
function printDoctorResult(result) {
|
|
209
|
+
function printDoctorResult(renderer, result) {
|
|
197
210
|
const badge = renderer.healthBadge(result.installed);
|
|
198
211
|
|
|
199
212
|
const summary = renderer.section(
|
|
@@ -239,7 +252,7 @@ function printDoctorResult(result) {
|
|
|
239
252
|
renderer.write(renderer.joinBlocks([badge, summary, lock, issues, probes]));
|
|
240
253
|
}
|
|
241
254
|
|
|
242
|
-
function printUninstallResult(result) {
|
|
255
|
+
function printUninstallResult(renderer, result) {
|
|
243
256
|
if (result.removed) {
|
|
244
257
|
renderer.write(
|
|
245
258
|
renderer.joinBlocks([
|
|
@@ -261,34 +274,193 @@ function printUninstallResult(result) {
|
|
|
261
274
|
);
|
|
262
275
|
}
|
|
263
276
|
|
|
264
|
-
|
|
265
|
-
|
|
277
|
+
export function buildErrorHint(message) {
|
|
278
|
+
if (message.startsWith("Unknown command:")) {
|
|
279
|
+
return "Run `npx skilly-hand --help` to see available commands.";
|
|
280
|
+
}
|
|
281
|
+
if (message.startsWith("Unknown flag:") || message.startsWith("Missing value")) {
|
|
282
|
+
return "Check command flags with `npx skilly-hand --help`.";
|
|
283
|
+
}
|
|
284
|
+
return "Retry with `--verbose` for expanded context if needed.";
|
|
285
|
+
}
|
|
266
286
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
287
|
+
export function createPromptAdapter({ selectImpl, checkboxImpl, confirmImpl } = {}) {
|
|
288
|
+
return {
|
|
289
|
+
select: selectImpl || inquirerSelect,
|
|
290
|
+
checkbox: checkboxImpl || inquirerCheckbox,
|
|
291
|
+
confirm: confirmImpl || inquirerConfirm
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function createServices(overrides = {}) {
|
|
296
|
+
return {
|
|
297
|
+
loadAllSkills,
|
|
298
|
+
installProject,
|
|
299
|
+
resolveSkillSelection,
|
|
300
|
+
runDoctor,
|
|
301
|
+
uninstallProject,
|
|
302
|
+
detectProject,
|
|
303
|
+
defaultAgents: DEFAULT_AGENTS,
|
|
304
|
+
...overrides
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isInteractiveLauncherMode({ command, flags, stdout }) {
|
|
309
|
+
return !command && !flags.json && Boolean(stdout?.isTTY);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function runInteractiveInstall({
|
|
313
|
+
cwd,
|
|
314
|
+
renderer,
|
|
315
|
+
prompt,
|
|
316
|
+
services,
|
|
317
|
+
appVersion
|
|
318
|
+
}) {
|
|
319
|
+
const [catalog, detections] = await Promise.all([
|
|
320
|
+
services.loadAllSkills(),
|
|
321
|
+
services.detectProject(cwd)
|
|
322
|
+
]);
|
|
323
|
+
const portableCatalog = catalog.filter((skill) => skill.portable).sort((a, b) => a.id.localeCompare(b.id));
|
|
324
|
+
const preselected = new Set(
|
|
325
|
+
services
|
|
326
|
+
.resolveSkillSelection({ catalog, detections, includeTags: [], excludeTags: [] })
|
|
327
|
+
.map((skill) => skill.id)
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const selectedSkillIds = await prompt.checkbox({
|
|
331
|
+
message: "Select skills to install",
|
|
332
|
+
choices: portableCatalog.map((skill) => ({
|
|
333
|
+
value: skill.id,
|
|
334
|
+
name: `${skill.id} - ${skill.title}`,
|
|
335
|
+
checked: preselected.has(skill.id)
|
|
336
|
+
}))
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const selectedAgents = await prompt.checkbox({
|
|
340
|
+
message: "Select AI assistants to configure",
|
|
341
|
+
choices: services.defaultAgents.map((agent) => ({
|
|
342
|
+
value: agent,
|
|
343
|
+
name: agent,
|
|
344
|
+
checked: true
|
|
345
|
+
}))
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const preview = await services.installProject({
|
|
349
|
+
cwd,
|
|
350
|
+
agents: selectedAgents,
|
|
351
|
+
dryRun: true,
|
|
352
|
+
selectedSkillIds
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
printInstallResult(renderer, appVersion, preview, {
|
|
356
|
+
dryRun: true,
|
|
357
|
+
include: [],
|
|
358
|
+
exclude: []
|
|
359
|
+
});
|
|
282
360
|
|
|
283
|
-
|
|
361
|
+
const shouldApply = await prompt.confirm({
|
|
362
|
+
message: "Apply installation changes now?",
|
|
363
|
+
default: true
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (!shouldApply) {
|
|
367
|
+
renderer.write(renderer.status("info", "Installation cancelled.", "No files were written."));
|
|
284
368
|
return;
|
|
285
369
|
}
|
|
286
370
|
|
|
287
|
-
const
|
|
288
|
-
|
|
371
|
+
const applied = await services.installProject({
|
|
372
|
+
cwd,
|
|
373
|
+
agents: selectedAgents,
|
|
374
|
+
dryRun: false,
|
|
375
|
+
selectedSkillIds
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
printInstallResult(renderer, appVersion, applied, {
|
|
379
|
+
dryRun: false,
|
|
380
|
+
include: [],
|
|
381
|
+
exclude: []
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function runInteractiveSession({
|
|
386
|
+
cwd,
|
|
387
|
+
renderer,
|
|
388
|
+
prompt,
|
|
389
|
+
services,
|
|
390
|
+
appVersion
|
|
391
|
+
}) {
|
|
392
|
+
renderer.write(renderer.banner(appVersion));
|
|
393
|
+
|
|
394
|
+
while (true) {
|
|
395
|
+
const selection = await prompt.select({
|
|
396
|
+
message: "Select a command",
|
|
397
|
+
choices: [
|
|
398
|
+
{ value: "install", name: "Install" },
|
|
399
|
+
{ value: "detect", name: "Detect" },
|
|
400
|
+
{ value: "list", name: "List" },
|
|
401
|
+
{ value: "doctor", name: "Doctor" },
|
|
402
|
+
{ value: "uninstall", name: "Uninstall" },
|
|
403
|
+
{ value: "exit", name: "Exit" }
|
|
404
|
+
]
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (selection === "exit") {
|
|
408
|
+
renderer.write(renderer.status("info", "Exited skilly-hand interactive mode."));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (selection === "install") {
|
|
413
|
+
await runInteractiveInstall({ cwd, renderer, prompt, services, appVersion });
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (selection === "detect") {
|
|
418
|
+
const detections = await services.detectProject(cwd);
|
|
419
|
+
printDetectResult(renderer, cwd, detections);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (selection === "list") {
|
|
424
|
+
const skills = await services.loadAllSkills();
|
|
425
|
+
printListResult(renderer, skills);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (selection === "doctor") {
|
|
430
|
+
const result = await services.runDoctor(cwd);
|
|
431
|
+
printDoctorResult(renderer, result);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
289
434
|
|
|
290
|
-
|
|
291
|
-
|
|
435
|
+
if (selection === "uninstall") {
|
|
436
|
+
const confirmed = await prompt.confirm({
|
|
437
|
+
message: "Remove the skilly-hand installation from this project?",
|
|
438
|
+
default: false
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (!confirmed) {
|
|
442
|
+
renderer.write(renderer.status("info", "Uninstall cancelled."));
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const result = await services.uninstallProject(cwd);
|
|
447
|
+
printUninstallResult(renderer, result);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function runCommand({
|
|
453
|
+
command,
|
|
454
|
+
flags,
|
|
455
|
+
cwd,
|
|
456
|
+
stdout,
|
|
457
|
+
renderer,
|
|
458
|
+
prompt,
|
|
459
|
+
services,
|
|
460
|
+
appVersion
|
|
461
|
+
}) {
|
|
462
|
+
if (command === "detect") {
|
|
463
|
+
const detections = await services.detectProject(cwd);
|
|
292
464
|
if (flags.json) {
|
|
293
465
|
renderer.writeJson({
|
|
294
466
|
command: "detect",
|
|
@@ -298,12 +470,12 @@ async function main() {
|
|
|
298
470
|
});
|
|
299
471
|
return;
|
|
300
472
|
}
|
|
301
|
-
printDetectResult(cwd, detections);
|
|
473
|
+
printDetectResult(renderer, cwd, detections);
|
|
302
474
|
return;
|
|
303
475
|
}
|
|
304
476
|
|
|
305
|
-
if (
|
|
306
|
-
const skills = await loadAllSkills();
|
|
477
|
+
if (command === "list") {
|
|
478
|
+
const skills = await services.loadAllSkills();
|
|
307
479
|
if (flags.json) {
|
|
308
480
|
renderer.writeJson({
|
|
309
481
|
command: "list",
|
|
@@ -312,12 +484,12 @@ async function main() {
|
|
|
312
484
|
});
|
|
313
485
|
return;
|
|
314
486
|
}
|
|
315
|
-
printListResult(skills);
|
|
487
|
+
printListResult(renderer, skills);
|
|
316
488
|
return;
|
|
317
489
|
}
|
|
318
490
|
|
|
319
|
-
if (
|
|
320
|
-
const result = await runDoctor(cwd);
|
|
491
|
+
if (command === "doctor") {
|
|
492
|
+
const result = await services.runDoctor(cwd);
|
|
321
493
|
if (flags.json) {
|
|
322
494
|
renderer.writeJson({
|
|
323
495
|
command: "doctor",
|
|
@@ -325,12 +497,23 @@ async function main() {
|
|
|
325
497
|
});
|
|
326
498
|
return;
|
|
327
499
|
}
|
|
328
|
-
printDoctorResult(result);
|
|
500
|
+
printDoctorResult(renderer, result);
|
|
329
501
|
return;
|
|
330
502
|
}
|
|
331
503
|
|
|
332
|
-
if (
|
|
333
|
-
|
|
504
|
+
if (command === "uninstall") {
|
|
505
|
+
if (!flags.json && !flags.yes && Boolean(stdout?.isTTY)) {
|
|
506
|
+
const confirmed = await prompt.confirm({
|
|
507
|
+
message: "Remove the skilly-hand installation from this project?",
|
|
508
|
+
default: false
|
|
509
|
+
});
|
|
510
|
+
if (!confirmed) {
|
|
511
|
+
renderer.write(renderer.status("info", "Uninstall cancelled."));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const result = await services.uninstallProject(cwd);
|
|
334
517
|
if (flags.json) {
|
|
335
518
|
renderer.writeJson({
|
|
336
519
|
command: "uninstall",
|
|
@@ -338,12 +521,23 @@ async function main() {
|
|
|
338
521
|
});
|
|
339
522
|
return;
|
|
340
523
|
}
|
|
341
|
-
printUninstallResult(result);
|
|
524
|
+
printUninstallResult(renderer, result);
|
|
342
525
|
return;
|
|
343
526
|
}
|
|
344
527
|
|
|
345
|
-
if (
|
|
346
|
-
|
|
528
|
+
if (command === "install") {
|
|
529
|
+
if (!flags.dryRun && !flags.json && !flags.yes && Boolean(stdout?.isTTY)) {
|
|
530
|
+
const confirmed = await prompt.confirm({
|
|
531
|
+
message: "Apply installation changes to this project?",
|
|
532
|
+
default: true
|
|
533
|
+
});
|
|
534
|
+
if (!confirmed) {
|
|
535
|
+
renderer.write(renderer.status("info", "Installation cancelled.", "No files were written."));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const result = await services.installProject({
|
|
347
541
|
cwd,
|
|
348
542
|
agents: flags.agents,
|
|
349
543
|
dryRun: flags.dryRun,
|
|
@@ -361,43 +555,109 @@ async function main() {
|
|
|
361
555
|
return;
|
|
362
556
|
}
|
|
363
557
|
|
|
364
|
-
printInstallResult(result, flags);
|
|
558
|
+
printInstallResult(renderer, appVersion, result, flags);
|
|
365
559
|
return;
|
|
366
560
|
}
|
|
367
561
|
|
|
368
|
-
throw new Error(`Unknown command: ${
|
|
562
|
+
throw new Error(`Unknown command: ${command}`);
|
|
369
563
|
}
|
|
370
564
|
|
|
371
|
-
|
|
565
|
+
export async function runCli({
|
|
566
|
+
argv = process.argv.slice(2),
|
|
567
|
+
stdout = process.stdout,
|
|
568
|
+
stderr = process.stderr,
|
|
569
|
+
env = process.env,
|
|
570
|
+
platform = process.platform,
|
|
571
|
+
prompt = createPromptAdapter(),
|
|
572
|
+
services: providedServices = {},
|
|
573
|
+
appVersion = version,
|
|
574
|
+
cwdResolver = process.cwd
|
|
575
|
+
} = {}) {
|
|
576
|
+
const renderer = createTerminalRenderer({ stdout, stderr, env, platform });
|
|
577
|
+
const services = createServices(providedServices);
|
|
578
|
+
const { command, flags } = parseArgs(argv);
|
|
372
579
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
580
|
+
if (flags.help) {
|
|
581
|
+
if (flags.json) {
|
|
582
|
+
renderer.writeJson({
|
|
583
|
+
command: command || "install",
|
|
584
|
+
help: true,
|
|
585
|
+
usage: [
|
|
586
|
+
"npx skilly-hand",
|
|
587
|
+
"npx skilly-hand install",
|
|
588
|
+
"npx skilly-hand detect",
|
|
589
|
+
"npx skilly-hand list",
|
|
590
|
+
"npx skilly-hand doctor",
|
|
591
|
+
"npx skilly-hand uninstall"
|
|
592
|
+
]
|
|
593
|
+
});
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
380
596
|
|
|
381
|
-
|
|
382
|
-
renderer.writeErrorJson({
|
|
383
|
-
ok: false,
|
|
384
|
-
error: {
|
|
385
|
-
what: "skilly-hand command failed",
|
|
386
|
-
why: error.message,
|
|
387
|
-
hint
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
process.exitCode = 1;
|
|
597
|
+
renderer.write(buildHelpText(renderer, appVersion));
|
|
391
598
|
return;
|
|
392
599
|
}
|
|
393
600
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
601
|
+
const cwd = path.resolve(flags.cwd || cwdResolver());
|
|
602
|
+
|
|
603
|
+
if (isInteractiveLauncherMode({ command, flags, stdout })) {
|
|
604
|
+
try {
|
|
605
|
+
await runInteractiveSession({
|
|
606
|
+
cwd,
|
|
607
|
+
renderer,
|
|
608
|
+
prompt,
|
|
609
|
+
services,
|
|
610
|
+
appVersion
|
|
611
|
+
});
|
|
612
|
+
return;
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (error?.name === "ExitPromptError") {
|
|
615
|
+
renderer.write(renderer.status("info", "Interactive session cancelled."));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const effectiveCommand = command || "install";
|
|
623
|
+
await runCommand({
|
|
624
|
+
command: effectiveCommand,
|
|
625
|
+
flags,
|
|
626
|
+
cwd,
|
|
627
|
+
stdout,
|
|
628
|
+
renderer,
|
|
629
|
+
prompt,
|
|
630
|
+
services,
|
|
631
|
+
appVersion
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (isExecutedDirectly(import.meta.url, process.argv[1])) {
|
|
636
|
+
const jsonRequested = process.argv.includes("--json");
|
|
637
|
+
const renderer = createTerminalRenderer();
|
|
638
|
+
|
|
639
|
+
runCli().catch((error) => {
|
|
640
|
+
if (jsonRequested) {
|
|
641
|
+
renderer.writeErrorJson({
|
|
642
|
+
ok: false,
|
|
643
|
+
error: {
|
|
644
|
+
what: "skilly-hand command failed",
|
|
645
|
+
why: error.message,
|
|
646
|
+
hint: buildErrorHint(error.message)
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
process.exitCode = 1;
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
renderer.writeError(
|
|
654
|
+
renderer.error({
|
|
655
|
+
what: "skilly-hand command failed",
|
|
656
|
+
why: error.message,
|
|
657
|
+
hint: buildErrorHint(error.message),
|
|
658
|
+
exitCode: 1
|
|
659
|
+
})
|
|
660
|
+
);
|
|
661
|
+
process.exitCode = 1;
|
|
662
|
+
});
|
|
663
|
+
}
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { copySkillTo, loadAllSkills, renderAgentsMarkdown, verifyCatalogFiles } from "../../catalog/src/index.js";
|
|
4
4
|
import { detectProject, inspectProjectFiles } from "../../detectors/src/index.js";
|
|
5
5
|
|
|
6
|
-
const DEFAULT_AGENTS = ["codex", "claude", "cursor", "gemini", "copilot"];
|
|
6
|
+
export const DEFAULT_AGENTS = ["codex", "claude", "cursor", "gemini", "copilot"];
|
|
7
7
|
const MANAGED_MARKER = "<!-- Managed by skilly-hand.";
|
|
8
8
|
|
|
9
9
|
function uniq(values) {
|
|
@@ -44,6 +44,37 @@ function parseTags(input) {
|
|
|
44
44
|
return uniq((input || []).flatMap((value) => String(value).split(",")).map((value) => value.trim()).filter(Boolean));
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function parseSkillIds(input) {
|
|
48
|
+
return uniq((input || []).flatMap((value) => String(value).split(",")).map((value) => value.trim()).filter(Boolean));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveSkillSelectionByIds({ catalog, selectedSkillIds = [] }) {
|
|
52
|
+
const ids = parseSkillIds(selectedSkillIds);
|
|
53
|
+
const portableById = new Map(
|
|
54
|
+
catalog
|
|
55
|
+
.filter((skill) => skill.portable)
|
|
56
|
+
.map((skill) => [skill.id, skill])
|
|
57
|
+
);
|
|
58
|
+
const allById = new Map(catalog.map((skill) => [skill.id, skill]));
|
|
59
|
+
|
|
60
|
+
const invalid = [];
|
|
61
|
+
for (const id of ids) {
|
|
62
|
+
if (!allById.has(id)) {
|
|
63
|
+
invalid.push(`Unknown skill id: ${id}`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!portableById.has(id)) {
|
|
67
|
+
invalid.push(`Skill is not portable: ${id}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (invalid.length > 0) {
|
|
72
|
+
throw new Error(invalid.join("; "));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return ids.map((id) => portableById.get(id)).sort((a, b) => a.id.localeCompare(b.id));
|
|
76
|
+
}
|
|
77
|
+
|
|
47
78
|
export function resolveSkillSelection({ catalog, detections, includeTags = [], excludeTags = [] }) {
|
|
48
79
|
const coreSkills = catalog.filter((skill) => skill.tags.includes("core"));
|
|
49
80
|
const requested = new Set(coreSkills.map((skill) => skill.id));
|
|
@@ -168,17 +199,20 @@ export async function installProject({
|
|
|
168
199
|
agents,
|
|
169
200
|
dryRun = false,
|
|
170
201
|
includeTags = [],
|
|
171
|
-
excludeTags = []
|
|
202
|
+
excludeTags = [],
|
|
203
|
+
selectedSkillIds
|
|
172
204
|
}) {
|
|
173
205
|
const selectedAgents = normalizeAgentList(agents);
|
|
174
206
|
const catalog = await loadAllSkills();
|
|
175
207
|
const detections = await detectProject(cwd);
|
|
176
|
-
const skills =
|
|
177
|
-
catalog,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
208
|
+
const skills = selectedSkillIds !== undefined && selectedSkillIds !== null
|
|
209
|
+
? resolveSkillSelectionByIds({ catalog, selectedSkillIds })
|
|
210
|
+
: resolveSkillSelection({
|
|
211
|
+
catalog,
|
|
212
|
+
detections,
|
|
213
|
+
includeTags: parseTags(includeTags),
|
|
214
|
+
excludeTags: parseTags(excludeTags)
|
|
215
|
+
});
|
|
182
216
|
const plan = buildInstallPlan({ cwd, detections, skills, agents: selectedAgents });
|
|
183
217
|
|
|
184
218
|
if (dryRun) {
|