@wabot-dev/create 0.0.3 → 2.0.0-beta.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.
@@ -2,7 +2,8 @@ import fs from 'fs/promises'
2
2
  import os from 'os'
3
3
  import path from 'path'
4
4
 
5
- import { getSkill, listSkillNames } from './registry.mjs'
5
+ import { getSkill, listSkillNames, listSkills } from './registry.mjs'
6
+ import { resolveFrameworkSkillsDir } from './framework-source.mjs'
6
7
 
7
8
  const supportedAgents = {
8
9
  codex: ['.codex', 'skills'],
@@ -10,12 +11,75 @@ const supportedAgents = {
10
11
  agents: ['.agents', 'skills'],
11
12
  }
12
13
 
14
+ export function listSupportedAgents() {
15
+ return Object.keys(supportedAgents)
16
+ }
17
+
18
+ export async function installSkillsInProject(projectDir, agents, skillNames) {
19
+ if (!projectDir) {
20
+ throw new Error('installSkillsInProject: projectDir is required')
21
+ }
22
+
23
+ const resolvedAgents = (agents && agents.length > 0 ? agents : ['claude']).map((agent) => {
24
+ if (!supportedAgents[agent]) {
25
+ throw new Error(
26
+ `Unsupported agent "${agent}". Supported: ${Object.keys(supportedAgents).join(', ')}`,
27
+ )
28
+ }
29
+ return agent
30
+ })
31
+
32
+ const skillsDir = resolveFrameworkSkillsDir(projectDir)
33
+ const skills =
34
+ skillNames && skillNames.length > 0
35
+ ? skillNames.map((name) => requireSkill(name, skillsDir))
36
+ : listSkills(skillsDir)
37
+
38
+ const installations = []
39
+
40
+ for (const agent of resolvedAgents) {
41
+ const baseDir = path.join(projectDir, ...supportedAgents[agent])
42
+ await fs.mkdir(baseDir, { recursive: true })
43
+
44
+ for (const skill of skills) {
45
+ const installDir = path.join(baseDir, skill.name)
46
+ try {
47
+ await fs.cp(skill.sourceDir, installDir, {
48
+ recursive: true,
49
+ force: false,
50
+ errorOnExist: true,
51
+ })
52
+ installations.push({ agent, skill: skill.name, installDir, status: 'installed' })
53
+ } catch (error) {
54
+ if (error?.code === 'ERR_FS_CP_EEXIST' || error?.code === 'EEXIST') {
55
+ installations.push({ agent, skill: skill.name, installDir, status: 'skipped' })
56
+ continue
57
+ }
58
+ throw error
59
+ }
60
+ }
61
+ }
62
+
63
+ return { agents: resolvedAgents, skills: skills.map((skill) => skill.name), installations }
64
+ }
65
+
66
+ function requireSkill(name, skillsDir) {
67
+ const skill = getSkill(name, skillsDir)
68
+ if (!skill) {
69
+ throw new Error(
70
+ `Unknown skill "${name}". Available skills: ${listSkillNames(skillsDir).join(', ')}`,
71
+ )
72
+ }
73
+ return skill
74
+ }
75
+
13
76
  export async function installSkill(skillName, options, cwd = process.cwd()) {
14
- const skill = getSkill(skillName)
77
+ const skillsDir = resolveFrameworkSkillsDir(cwd)
78
+ const skill = getSkill(skillName, skillsDir)
15
79
 
16
80
  if (!skill) {
17
81
  throw new Error(
18
- `Unknown skill "${skillName}". Available skills: ${listSkillNames().join(', ')}`,
82
+ `Unknown skill "${skillName}". Available skills: ${listSkillNames(skillsDir).join(', ')}`,
19
83
  )
20
84
  }
21
85
 
@@ -1,17 +1,34 @@
1
- import { fileURLToPath } from 'url'
1
+ import fs from 'fs'
2
+ import path from 'path'
2
3
 
3
- const skillDefinitions = {
4
- 'wabot-framework': {
5
- name: 'wabot-framework',
6
- sourceDir: fileURLToPath(new URL('../../skills/wabot-framework', import.meta.url)),
7
- description: 'Wabot Framework packaged skill',
8
- },
4
+ import { resolveFrameworkSkillsDir } from './framework-source.mjs'
5
+
6
+ // Skills are owned and versioned by @wabot-dev/framework. The registry discovers
7
+ // them from the framework's packaged `skills/` directory rather than bundling its
8
+ // own copy, so adding a skill upstream requires no change here.
9
+
10
+ export function listSkillNames(skillsDir = resolveFrameworkSkillsDir()) {
11
+ return fs
12
+ .readdirSync(skillsDir, { withFileTypes: true })
13
+ .filter(
14
+ (entry) =>
15
+ entry.isDirectory() && fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')),
16
+ )
17
+ .map((entry) => entry.name)
18
+ .sort()
9
19
  }
10
20
 
11
- export function getSkill(skillName) {
12
- return skillDefinitions[skillName] ?? null
21
+ export function listSkills(skillsDir = resolveFrameworkSkillsDir()) {
22
+ return listSkillNames(skillsDir).map((name) => ({
23
+ name,
24
+ sourceDir: path.join(skillsDir, name),
25
+ }))
13
26
  }
14
27
 
15
- export function listSkillNames() {
16
- return Object.keys(skillDefinitions).sort()
17
- }
28
+ export function getSkill(name, skillsDir = resolveFrameworkSkillsDir()) {
29
+ const sourceDir = path.join(skillsDir, name)
30
+ if (!fs.existsSync(path.join(sourceDir, 'SKILL.md'))) {
31
+ return null
32
+ }
33
+ return { name, sourceDir }
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wabot-dev/create",
3
- "version": "0.0.3",
3
+ "version": "2.0.0-beta.0",
4
4
  "description": "Project creator for Wabot Framework",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -11,9 +11,11 @@
11
11
  },
12
12
  "files": [
13
13
  "bin",
14
- "lib",
15
- "skills"
14
+ "lib"
16
15
  ],
16
+ "scripts": {
17
+ "test:smoke": "node scripts/smoke-skills-bulk-install.mjs && node scripts/smoke-skill-installer.mjs"
18
+ },
17
19
  "dependencies": {
18
20
  "chalk": "^5.4.1",
19
21
  "commander": "^14.0.0",
@@ -1,32 +0,0 @@
1
- ---
2
- name: wabot-framework
3
- description: Use when building, reviewing, or debugging apps with @wabot-dev/framework. This skill is self-contained and includes offline examples for project setup, dependency injection, mindsets, external function modules, validation, persistence, chat controllers, REST and socket controllers, async commands, cron, auth, logger, and locking.
4
- ---
5
-
6
- # Wabot Framework
7
-
8
- Use this skill for Wabot application work. It is self-contained: read the bundled examples first and do not depend on web access for normal tasks.
9
-
10
- ## How To Use
11
-
12
- Load the reference file that matches the request:
13
-
14
- - `references/quickstart.md` for project bootstrap and first run
15
- - `references/core-patterns.md` for dependency injection and service lifecycles
16
- - `references/mindset-and-chat.md` for bot personality, tools, and chat routing
17
- - `references/validation-and-data.md` for DTOs, validation, and `Mapper`
18
- - `references/persistence.md` for entities, repositories, and PostgreSQL persistence
19
- - `references/controllers-and-auth.md` for REST, socket, and JWT auth
20
- - `references/async-and-cron.md` for commands and scheduled work
21
- - `references/ops.md` for logger and locking
22
- - `references/full-example.md` for an end-to-end sales bot app
23
-
24
- ## Rules
25
-
26
- - Keep prose in Spanish when explaining to the team.
27
- - Keep code identifiers in English.
28
- - Use `@wabot-dev/framework` imports in examples.
29
- - Do not invent APIs. If a detail is not covered by the bundled examples, say what is missing instead of guessing.
30
- - Keep controllers thin and push business logic into services or modules.
31
- - Use `@scoped()` only when request, chat, or socket isolation matters.
32
- - Prefer the smallest example that demonstrates the pattern.
@@ -1,5 +0,0 @@
1
- interface:
2
- display_name: "Wabot Framework"
3
- short_description: "Build apps with @wabot-dev/framework"
4
- default_prompt: "Use this skill to implement, review, or debug Wabot applications using the bundled offline examples and reference files."
5
-
@@ -1,57 +0,0 @@
1
- # Async And Cron
2
-
3
- Use this when scheduling work in the background or recurring jobs.
4
-
5
- ## Command and handler
6
-
7
- ```typescript
8
- import { command, commandHandler, type ICommandHandler } from '@wabot-dev/framework'
9
- import { isString } from '@wabot-dev/framework'
10
-
11
- @command('save-test-tag')
12
- export class SaveTestTag {
13
- @isString()
14
- value!: string
15
- }
16
-
17
- @commandHandler(SaveTestTag)
18
- export class SaveTestTagHandler implements ICommandHandler<SaveTestTag> {
19
- constructor(private tags: TagRepository) {}
20
-
21
- async handle(input: SaveTestTag) {
22
- await this.tags.save(input.value)
23
- }
24
- }
25
- ```
26
-
27
- ## Run or schedule a command
28
-
29
- ```typescript
30
- import { Async } from '@wabot-dev/framework'
31
-
32
- const job = await async.runCommand(SaveTestTag, { value: 'hello' })
33
- await async.scheduleCommand(SaveTestTag, { value: 'later' }, { minutes: 10 })
34
- ```
35
-
36
- ## Cron handler
37
-
38
- ```typescript
39
- import { cron } from '@wabot-dev/framework'
40
-
41
- @cron({ name: 'sync-leads', cron: '0 * * * *' })
42
- export class SyncLeadsCron {
43
- constructor(private leadService: LeadService) {}
44
-
45
- async handle() {
46
- await this.leadService.sync()
47
- }
48
- }
49
- ```
50
-
51
- ## Good practice
52
-
53
- - Use commands for one-off background work.
54
- - Use cron for repeated schedules.
55
- - Keep command DTOs validated and small.
56
- - Keep cron handlers idempotent because they can run many times.
57
-
@@ -1,104 +0,0 @@
1
- # Controllers And Auth
2
-
3
- Use this when exposing HTTP endpoints, socket events, or protected routes.
4
-
5
- ## REST controller
6
-
7
- ```typescript
8
- import { IncomingMessage } from 'http'
9
- import { onPost, restController } from '@wabot-dev/framework'
10
-
11
- class PaymentService {
12
- processWebhook(rawBody: string) {
13
- return { ok: true, rawBody }
14
- }
15
- }
16
-
17
- @restController('/webhooks/bold')
18
- export class BoldWebhookController {
19
- constructor(private paymentService: PaymentService) {}
20
-
21
- @onPost({
22
- disableJsonParser: true,
23
- disableUrlEncodedParser: true,
24
- })
25
- async receiveWebhook(request: IncomingMessage) {
26
- const rawBody = await this.readRawBody(request)
27
- return this.paymentService.processWebhook(rawBody)
28
- }
29
- }
30
- ```
31
-
32
- ## Socket controller
33
-
34
- ```typescript
35
- import { jwtHandshakeGuard, onSocketEvent, socketController } from '@wabot-dev/framework'
36
-
37
- @jwtHandshakeGuard()
38
- @socketController('/sales')
39
- export class SalesSocketController {
40
- @onSocketEvent('message')
41
- async onMessage(payload: { text: string }) {
42
- return { ok: true, received: payload.text }
43
- }
44
- }
45
- ```
46
-
47
- ## JWT and API key auth
48
-
49
- Use JWT when the client already has a signed token. Use API keys when the client should present a shared secret.
50
-
51
- ```typescript
52
- import {
53
- apiKeyGuard,
54
- apiKeyHandshakeGuard,
55
- jwtGuard,
56
- jwtHandshakeGuard,
57
- onGet,
58
- onPost,
59
- restController,
60
- socketController,
61
- } from '@wabot-dev/framework'
62
-
63
- class CreateLeadDto {
64
- name!: string
65
- }
66
-
67
- class LeadService {
68
- async create(body: CreateLeadDto) {
69
- return { created: true, lead: body }
70
- }
71
- }
72
-
73
- @restController('/api/leads')
74
- export class LeadApiController {
75
- constructor(private leadService: LeadService) {}
76
-
77
- @apiKeyGuard()
78
- @onGet()
79
- async listLeads() {
80
- return []
81
- }
82
-
83
- @jwtGuard()
84
- @onPost()
85
- async createLead(body: CreateLeadDto) {
86
- return this.leadService.create(body)
87
- }
88
- }
89
-
90
- @socketController('/lead-room')
91
- @jwtHandshakeGuard()
92
- export class LeadRoomSocketController {}
93
-
94
- @apiKeyHandshakeGuard()
95
- @socketController('/api-room')
96
- export class ApiRoomSocketController {}
97
- ```
98
-
99
- ## Rule of thumb
100
-
101
- - REST for request/response workflows, webhooks, and admin APIs.
102
- - Socket for live events and long-lived sessions.
103
- - JWT when the client already has a signed token.
104
- - API key when the client should present a shared secret.
@@ -1,83 +0,0 @@
1
- # Core Patterns
2
-
3
- Use this when deciding how to register and inject services.
4
-
5
- ## Lifecycles
6
-
7
- | Decorator | Use for | Example |
8
- | --- | --- | --- |
9
- | `@singleton()` | Shared clients, repositories, config, caches | one DB repository for the app |
10
- | `@injectable()` | Stateless services and modules | calculators, adapters, helpers |
11
- | `@scoped(Lifecycle.ContainerScoped)` | Request/chat/socket-local state | auth context, request context |
12
-
13
- ## Singleton example
14
-
15
- ```typescript
16
- import { singleton } from '@wabot-dev/framework'
17
-
18
- @singleton()
19
- export class ProductRepository {
20
- private products = new Map<string, string>()
21
-
22
- save(id: string, name: string) {
23
- this.products.set(id, name)
24
- }
25
- }
26
- ```
27
-
28
- ## Injectable example
29
-
30
- ```typescript
31
- import { injectable } from '@wabot-dev/framework'
32
-
33
- @injectable()
34
- export class QuoteCalculator {
35
- total(base: number, discount: number) {
36
- return base - base * discount
37
- }
38
- }
39
- ```
40
-
41
- ## Scoped example
42
-
43
- ```typescript
44
- import { Lifecycle, scoped } from '@wabot-dev/framework'
45
-
46
- @scoped(Lifecycle.ContainerScoped)
47
- export class RequestContext {
48
- private userId?: string
49
-
50
- setUserId(userId: string) {
51
- this.userId = userId
52
- }
53
-
54
- requireUserId() {
55
- if (!this.userId) throw new Error('Unauthorized')
56
- return this.userId
57
- }
58
- }
59
- ```
60
-
61
- ## Token injection
62
-
63
- ```typescript
64
- import { container, inject, injectable } from '@wabot-dev/framework'
65
-
66
- container.register('SMTP_HOST', { useValue: 'smtp.example.com' })
67
- container.register('SMTP_PORT', { useValue: 587 })
68
-
69
- @injectable()
70
- export class Mailer {
71
- constructor(
72
- @inject('SMTP_HOST') private smtpHost: string,
73
- @inject('SMTP_PORT') private smtpPort: number,
74
- ) {}
75
- }
76
- ```
77
-
78
- ## Rule of thumb
79
-
80
- - Use singleton for shared infrastructure.
81
- - Use injectable for pure business logic.
82
- - Use scoped only when state must not leak across requests or chats.
83
-