@superdoc-dev/sdk 1.0.0-alpha.1 → 1.0.0-alpha.3

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/src/skills.ts CHANGED
@@ -1,10 +1,34 @@
1
- import { readFileSync, readdirSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { SuperDocCliError } from './runtime/errors';
5
6
 
6
7
  const skillsDir = path.resolve(fileURLToPath(new URL('../skills', import.meta.url)));
7
8
  const SKILL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
9
+ const SUPPORTED_SKILL_RUNTIMES = ['claude'] as const;
10
+ const SUPPORTED_INSTALL_SCOPES = ['project', 'user'] as const;
11
+
12
+ type SkillRuntime = (typeof SUPPORTED_SKILL_RUNTIMES)[number];
13
+ type SkillInstallScope = (typeof SUPPORTED_INSTALL_SCOPES)[number];
14
+
15
+ export interface InstallSkillOptions {
16
+ runtime?: SkillRuntime;
17
+ scope?: SkillInstallScope;
18
+ targetDir?: string;
19
+ cwd?: string;
20
+ homeDir?: string;
21
+ overwrite?: boolean;
22
+ }
23
+
24
+ export interface InstalledSkillResult {
25
+ name: string;
26
+ runtime: SkillRuntime;
27
+ scope: SkillInstallScope | 'custom';
28
+ path: string;
29
+ written: boolean;
30
+ overwritten: boolean;
31
+ }
8
32
 
9
33
  function resolveSkillFilePath(skillName: string): string {
10
34
  const filePath = path.resolve(skillsDir, `${skillName}.md`);
@@ -18,6 +42,23 @@ function resolveSkillFilePath(skillName: string): string {
18
42
  return filePath;
19
43
  }
20
44
 
45
+ function normalizeSkillName(name: string): string {
46
+ const normalized = name.trim();
47
+ if (!normalized || !SKILL_NAME_RE.test(normalized)) {
48
+ throw new SuperDocCliError('Skill name is required.', {
49
+ code: 'INVALID_ARGUMENT',
50
+ details: { name },
51
+ });
52
+ }
53
+ return normalized;
54
+ }
55
+
56
+ /**
57
+ * List the names of all SDK skills bundled with this package.
58
+ *
59
+ * @returns Sorted array of skill names (without the `.md` extension).
60
+ * @throws {SuperDocCliError} With code `SKILL_IO_ERROR` if the skills directory cannot be read.
61
+ */
21
62
  export function listSkills(): string[] {
22
63
  try {
23
64
  return readdirSync(skillsDir)
@@ -35,14 +76,17 @@ export function listSkills(): string[] {
35
76
  }
36
77
  }
37
78
 
79
+ /**
80
+ * Read the content of a bundled SDK skill by name.
81
+ *
82
+ * @param name - Skill name (e.g. `"editing-docx"`). Must match `[A-Za-z0-9][A-Za-z0-9_-]*`.
83
+ * @returns The skill file content as a UTF-8 string.
84
+ * @throws {SuperDocCliError} With code `INVALID_ARGUMENT` if the name is empty or contains invalid characters.
85
+ * @throws {SuperDocCliError} With code `SKILL_NOT_FOUND` if no skill with that name exists.
86
+ * @throws {SuperDocCliError} With code `SKILL_IO_ERROR` for other file-system read failures.
87
+ */
38
88
  export function getSkill(name: string): string {
39
- const normalized = name.trim();
40
- if (!normalized || !SKILL_NAME_RE.test(normalized)) {
41
- throw new SuperDocCliError('Skill name is required.', {
42
- code: 'INVALID_ARGUMENT',
43
- details: { name },
44
- });
45
- }
89
+ const normalized = normalizeSkillName(name);
46
90
 
47
91
  const filePath = resolveSkillFilePath(normalized);
48
92
  try {
@@ -74,3 +118,78 @@ export function getSkill(name: string): string {
74
118
  });
75
119
  }
76
120
  }
121
+
122
+ /**
123
+ * Install a bundled SDK skill into an agent runtime directory.
124
+ *
125
+ * Defaults to Claude's project-local skill path: `./.claude/skills/<name>/SKILL.md`.
126
+ */
127
+ export function installSkill(name: string, options: InstallSkillOptions = {}): InstalledSkillResult {
128
+ const normalizedName = normalizeSkillName(name);
129
+ const runtime = options.runtime ?? 'claude';
130
+ if (!SUPPORTED_SKILL_RUNTIMES.includes(runtime)) {
131
+ throw new SuperDocCliError('Unsupported skill runtime.', {
132
+ code: 'INVALID_ARGUMENT',
133
+ details: { runtime, supportedRuntimes: SUPPORTED_SKILL_RUNTIMES },
134
+ });
135
+ }
136
+
137
+ const scope = options.scope ?? 'project';
138
+ if (!SUPPORTED_INSTALL_SCOPES.includes(scope)) {
139
+ throw new SuperDocCliError('Unsupported skill install scope.', {
140
+ code: 'INVALID_ARGUMENT',
141
+ details: { scope, supportedScopes: SUPPORTED_INSTALL_SCOPES },
142
+ });
143
+ }
144
+
145
+ const skillsRoot =
146
+ options.targetDir !== undefined
147
+ ? path.resolve(options.targetDir)
148
+ : scope === 'user'
149
+ ? path.resolve(options.homeDir ?? os.homedir(), '.claude', 'skills')
150
+ : path.resolve(options.cwd ?? process.cwd(), '.claude', 'skills');
151
+ const skillFile = path.join(skillsRoot, normalizedName, 'SKILL.md');
152
+ const overwrite = options.overwrite ?? true;
153
+ const alreadyExists = existsSync(skillFile);
154
+
155
+ if (!overwrite && alreadyExists) {
156
+ return {
157
+ name: normalizedName,
158
+ runtime,
159
+ scope: options.targetDir !== undefined ? 'custom' : scope,
160
+ path: skillFile,
161
+ written: false,
162
+ overwritten: false,
163
+ };
164
+ }
165
+
166
+ try {
167
+ const content = getSkill(name);
168
+ mkdirSync(path.dirname(skillFile), { recursive: true });
169
+ writeFileSync(skillFile, content, 'utf8');
170
+ } catch (error) {
171
+ if (error instanceof SuperDocCliError) {
172
+ throw error;
173
+ }
174
+
175
+ throw new SuperDocCliError('Unable to install SDK skill.', {
176
+ code: 'SKILL_IO_ERROR',
177
+ details: {
178
+ name: normalizedName,
179
+ runtime,
180
+ scope: options.targetDir !== undefined ? 'custom' : scope,
181
+ path: skillFile,
182
+ message: error instanceof Error ? error.message : String(error),
183
+ },
184
+ });
185
+ }
186
+
187
+ return {
188
+ name: normalizedName,
189
+ runtime,
190
+ scope: options.targetDir !== undefined ? 'custom' : scope,
191
+ path: skillFile,
192
+ written: true,
193
+ overwritten: alreadyExists,
194
+ };
195
+ }