@minus-ai/create-skill 0.1.0-beta.1
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/index.mjs +658 -0
- package/package.json +12 -0
- package/templates/README.md.tpl +123 -0
- package/templates/asin/en-US.json.tpl +7 -0
- package/templates/asin/main.tsx.tpl +95 -0
- package/templates/asin/pipeline.py.tpl +11 -0
- package/templates/asin/zh-CN.json.tpl +7 -0
- package/templates/custom/en-US.json.tpl +8 -0
- package/templates/custom/main.tsx.tpl +93 -0
- package/templates/custom/pipeline.py.tpl +9 -0
- package/templates/custom/zh-CN.json.tpl +8 -0
- package/templates/default/en-US.json.tpl +4 -0
- package/templates/default/main.tsx.tpl +109 -0
- package/templates/default/pipeline.py.tpl +8 -0
- package/templates/default/zh-CN.json.tpl +4 -0
- package/templates/embed.html.tpl +13 -0
- package/templates/en-US.json.tpl +10 -0
- package/templates/env.example.tpl +3 -0
- package/templates/file/en-US.json.tpl +10 -0
- package/templates/file/main.tsx.tpl +104 -0
- package/templates/file/pipeline.py.tpl +11 -0
- package/templates/file/zh-CN.json.tpl +10 -0
- package/templates/frontend-package.json.tpl +22 -0
- package/templates/gitignore.tpl +10 -0
- package/templates/keyword/en-US.json.tpl +7 -0
- package/templates/keyword/main.tsx.tpl +95 -0
- package/templates/keyword/pipeline.py.tpl +11 -0
- package/templates/keyword/zh-CN.json.tpl +7 -0
- package/templates/main.tsx.tpl +193 -0
- package/templates/pipeline.py.tpl +12 -0
- package/templates/pnpm-workspace.yaml.tpl +2 -0
- package/templates/pyproject.toml.tpl +13 -0
- package/templates/root-package.json.tpl +18 -0
- package/templates/server.py.tpl +4 -0
- package/templates/tsconfig.json.tpl +15 -0
- package/templates/vite.config.ts.tpl +23 -0
- package/templates/zh-CN.json.tpl +10 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createInterface } from 'node:readline/promises'
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
5
|
+
import { execSync } from 'node:child_process'
|
|
6
|
+
import { createServer } from 'node:net'
|
|
7
|
+
import { join, dirname } from 'node:path'
|
|
8
|
+
import { homedir } from 'node:os'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const TEMPLATES_DIR = join(__dirname, 'templates')
|
|
13
|
+
const CREDENTIALS_DIR = join(homedir(), '.minus')
|
|
14
|
+
const CREDENTIALS_FILE = join(CREDENTIALS_DIR, 'credentials.json')
|
|
15
|
+
|
|
16
|
+
const DEFAULT_PLATFORM_URL = 'http://47.107.144.22:18990'
|
|
17
|
+
|
|
18
|
+
function findAvailablePort(start = 4001) {
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
const server = createServer()
|
|
21
|
+
server.listen(start, '127.0.0.1', () => {
|
|
22
|
+
server.close(() => resolve(start))
|
|
23
|
+
})
|
|
24
|
+
server.on('error', () => resolve(findAvailablePort(start + 1)))
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Naming helpers ──────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function idToFolder(id) {
|
|
31
|
+
return id.replace(/[-\s]+/g, '_')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function idToClassName(id) {
|
|
35
|
+
return (
|
|
36
|
+
id
|
|
37
|
+
.split(/[-_\s]+/)
|
|
38
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
39
|
+
.join('') + 'Pipeline'
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Template rendering ──────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function render(template, vars) {
|
|
46
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? '')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readTemplate(name, subdir) {
|
|
50
|
+
if (subdir) {
|
|
51
|
+
const sub = join(TEMPLATES_DIR, subdir, name)
|
|
52
|
+
if (existsSync(sub)) return readFileSync(sub, 'utf-8')
|
|
53
|
+
}
|
|
54
|
+
return readFileSync(join(TEMPLATES_DIR, name), 'utf-8')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeOut(path, content) {
|
|
58
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
59
|
+
writeFileSync(path, content)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Credentials cache ───────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function loadCredentials() {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8'))
|
|
67
|
+
} catch {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function saveCredentials(platformUrl, sid, extra = {}) {
|
|
73
|
+
mkdirSync(CREDENTIALS_DIR, { recursive: true })
|
|
74
|
+
writeFileSync(
|
|
75
|
+
CREDENTIALS_FILE,
|
|
76
|
+
JSON.stringify(
|
|
77
|
+
{
|
|
78
|
+
session_id: sid,
|
|
79
|
+
user_id: extra.userId || null,
|
|
80
|
+
team_id: extra.teamId || null,
|
|
81
|
+
api_base: platformUrl
|
|
82
|
+
},
|
|
83
|
+
null,
|
|
84
|
+
2
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── HTTP helpers ────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
async function api(platformUrl, method, path, body, sid) {
|
|
92
|
+
const url = `${platformUrl}${path}`
|
|
93
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
94
|
+
if (sid) headers['Cookie'] = `MINUS_AI_SID=${sid}`
|
|
95
|
+
|
|
96
|
+
const res = await fetch(url, {
|
|
97
|
+
method,
|
|
98
|
+
headers,
|
|
99
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
100
|
+
redirect: 'manual'
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const setCookie = res.headers.get('set-cookie') || ''
|
|
104
|
+
const sidMatch = setCookie.match(/MINUS_AI_SID=([^;]+)/)
|
|
105
|
+
const newSid = sidMatch ? sidMatch[1] : null
|
|
106
|
+
|
|
107
|
+
let data = null
|
|
108
|
+
const text = await res.text()
|
|
109
|
+
if (text) {
|
|
110
|
+
try {
|
|
111
|
+
data = JSON.parse(text)
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { status: res.status, data, sid: newSid }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function validateSession(platformUrl, sid) {
|
|
119
|
+
try {
|
|
120
|
+
const { status } = await api(platformUrl, 'GET', '/api/me', null, sid)
|
|
121
|
+
return status === 200
|
|
122
|
+
} catch {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Auth flow ───────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
function detectChannel(identifier) {
|
|
130
|
+
return identifier.includes('@') ? 'email' : 'phone'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeIdentifier(identifier, channel) {
|
|
134
|
+
if (channel === 'phone' && !identifier.startsWith('+')) {
|
|
135
|
+
return `+86${identifier}`
|
|
136
|
+
}
|
|
137
|
+
return identifier
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function login(rl, platformUrl) {
|
|
141
|
+
const identifier = (await rl.question(' ? 手机号或邮箱: ')).trim()
|
|
142
|
+
if (!identifier) {
|
|
143
|
+
console.error('\n ✗ 手机号或邮箱不能为空')
|
|
144
|
+
process.exit(1)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const channel = detectChannel(identifier)
|
|
148
|
+
const target = normalizeIdentifier(identifier, channel)
|
|
149
|
+
|
|
150
|
+
console.log(' → 发送验证码...')
|
|
151
|
+
const vcodeRes = await api(platformUrl, 'POST', '/api/auth/vcode/send', {
|
|
152
|
+
channel,
|
|
153
|
+
target,
|
|
154
|
+
purpose: 'login'
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
if (vcodeRes.status === 429) {
|
|
158
|
+
console.error('\n ✗ 请求过于频繁,请稍后再试')
|
|
159
|
+
process.exit(1)
|
|
160
|
+
}
|
|
161
|
+
if (vcodeRes.status >= 400) {
|
|
162
|
+
console.error(`\n ✗ 发送验证码失败: ${vcodeRes.data?.message || '未知错误'}`)
|
|
163
|
+
process.exit(1)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(' ✓ 验证码已发送')
|
|
167
|
+
const code = (await rl.question(' ? 验证码: ')).trim()
|
|
168
|
+
|
|
169
|
+
const grantType = channel === 'email' ? 'email_code' : 'phone_code'
|
|
170
|
+
const loginRes = await api(platformUrl, 'POST', '/api/auth/login', {
|
|
171
|
+
grantType,
|
|
172
|
+
identifier: target,
|
|
173
|
+
credential: code,
|
|
174
|
+
rememberMe: true
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
if (loginRes.status !== 200 || !loginRes.sid) {
|
|
178
|
+
const msg = loginRes.data?.code === 'VCODE_INVALID' ? '验证码错误' : loginRes.data?.code === 'VCODE_EXPIRED' ? '验证码已过期' : loginRes.data?.code === 'IDENTITY_NOT_FOUND' ? '该账号未注册,请先在平台注册' : loginRes.data?.message || '登录失败'
|
|
179
|
+
console.error(`\n ✗ ${msg}`)
|
|
180
|
+
process.exit(1)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(' ✓ 登录成功')
|
|
184
|
+
saveCredentials(platformUrl, loginRes.sid, { userId: loginRes.data?.userId })
|
|
185
|
+
return loginRes.sid
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function ensureAuth(rl, platformUrl) {
|
|
189
|
+
const creds = loadCredentials()
|
|
190
|
+
|
|
191
|
+
if (creds?.session_id) {
|
|
192
|
+
const valid = await validateSession(platformUrl, creds.session_id)
|
|
193
|
+
if (valid) {
|
|
194
|
+
console.log(' ✓ 已使用缓存的登录态')
|
|
195
|
+
return creds.session_id
|
|
196
|
+
}
|
|
197
|
+
console.log(' → 登录态已过期,需要重新登录')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return login(rl, platformUrl)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Create skill on platform ────────────────────────────
|
|
204
|
+
|
|
205
|
+
async function createSkillOnPlatform(platformUrl, sid, { displayName, description }) {
|
|
206
|
+
console.log(' → 正在向平台注册 skill...')
|
|
207
|
+
const body = {
|
|
208
|
+
displayName,
|
|
209
|
+
description: description
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let res
|
|
213
|
+
try {
|
|
214
|
+
res = await api(platformUrl, 'POST', '/api/skills', body, sid)
|
|
215
|
+
} catch (e) {
|
|
216
|
+
console.error(`\n ✗ 网络连接失败,请检查网络后重试`)
|
|
217
|
+
console.error(` 错误详情:${e.message}`)
|
|
218
|
+
process.exit(1)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (res.status === 201) {
|
|
222
|
+
return res.data
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (res.status >= 500) {
|
|
226
|
+
console.error(`\n ✗ Minus 平台暂时有问题,请稍后重试`)
|
|
227
|
+
} else if (res.data?.code === 'FORBIDDEN') {
|
|
228
|
+
console.error(`\n ✗ 当前账号无创建 Skill 的权限`)
|
|
229
|
+
} else if (res.data?.code === 'UNAUTHORIZED') {
|
|
230
|
+
console.error(`\n ✗ 登录已过期,请重新登录后再试`)
|
|
231
|
+
} else {
|
|
232
|
+
console.error(`\n ✗ 创建失败:${res.data?.message || `HTTP ${res.status}`}`)
|
|
233
|
+
}
|
|
234
|
+
process.exit(1)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── CLI ─────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
const BOOLEAN_FLAGS = new Set(['non-interactive'])
|
|
240
|
+
|
|
241
|
+
function parseFlags(args) {
|
|
242
|
+
const result = { positional: [], flags: {} }
|
|
243
|
+
for (let i = 0; i < args.length; i++) {
|
|
244
|
+
if (args[i].startsWith('--')) {
|
|
245
|
+
const key = args[i].slice(2)
|
|
246
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
247
|
+
result.flags[key] = true
|
|
248
|
+
} else if (i + 1 < args.length) {
|
|
249
|
+
result.flags[key] = args[++i]
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
result.positional.push(args[i])
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return result
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── Sub-commands for non-interactive (Claude) usage ─────
|
|
259
|
+
|
|
260
|
+
async function cmdCheckSession(flags) {
|
|
261
|
+
const platformUrl = flags.platform || DEFAULT_PLATFORM_URL
|
|
262
|
+
|
|
263
|
+
// Check --sid first
|
|
264
|
+
if (flags.sid) {
|
|
265
|
+
const valid = await validateSession(platformUrl, flags.sid)
|
|
266
|
+
if (valid) {
|
|
267
|
+
console.log(JSON.stringify({ ok: true, sid: flags.sid }))
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
console.log(JSON.stringify({ ok: false, reason: 'sid_invalid' }))
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check cached credentials
|
|
275
|
+
const creds = loadCredentials()
|
|
276
|
+
if (creds?.session_id) {
|
|
277
|
+
const valid = await validateSession(platformUrl, creds.session_id)
|
|
278
|
+
if (valid) {
|
|
279
|
+
console.log(JSON.stringify({ ok: true, sid: creds.session_id }))
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log(JSON.stringify({ ok: false, reason: 'no_session' }))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function cmdSendVcode(flags) {
|
|
288
|
+
const platformUrl = flags.platform || DEFAULT_PLATFORM_URL
|
|
289
|
+
const identifier = flags.identifier
|
|
290
|
+
if (!identifier) {
|
|
291
|
+
console.error(JSON.stringify({ ok: false, error: 'missing --identifier' }))
|
|
292
|
+
process.exit(1)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const channel = detectChannel(identifier)
|
|
296
|
+
const target = normalizeIdentifier(identifier, channel)
|
|
297
|
+
|
|
298
|
+
const purpose = flags.purpose || 'login'
|
|
299
|
+
const res = await api(platformUrl, 'POST', '/api/auth/vcode/send', {
|
|
300
|
+
channel,
|
|
301
|
+
target,
|
|
302
|
+
purpose
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
if (res.status === 429) {
|
|
306
|
+
console.log(JSON.stringify({ ok: false, error: 'rate_limited', message: '请求过于频繁,请稍后再试' }))
|
|
307
|
+
process.exit(1)
|
|
308
|
+
}
|
|
309
|
+
if (res.status >= 400) {
|
|
310
|
+
console.log(JSON.stringify({ ok: false, error: 'send_failed', message: res.data?.message || '发送失败' }))
|
|
311
|
+
process.exit(1)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log(JSON.stringify({ ok: true, channel, target }))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function cmdLogin(flags) {
|
|
318
|
+
const platformUrl = flags.platform || DEFAULT_PLATFORM_URL
|
|
319
|
+
const identifier = flags.identifier
|
|
320
|
+
const credential = flags.credential
|
|
321
|
+
const grantType = flags['grant-type']
|
|
322
|
+
|
|
323
|
+
if (!identifier || !credential) {
|
|
324
|
+
console.error(JSON.stringify({ ok: false, error: 'missing --identifier and/or --credential' }))
|
|
325
|
+
process.exit(1)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const channel = detectChannel(identifier)
|
|
329
|
+
const target = normalizeIdentifier(identifier, channel)
|
|
330
|
+
const resolvedGrantType = grantType || (channel === 'email' ? 'email_code' : 'phone_code')
|
|
331
|
+
|
|
332
|
+
const res = await api(platformUrl, 'POST', '/api/auth/login', {
|
|
333
|
+
grantType: resolvedGrantType,
|
|
334
|
+
identifier: target,
|
|
335
|
+
credential,
|
|
336
|
+
rememberMe: true
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
if (res.status !== 200 || !res.sid) {
|
|
340
|
+
const msg = res.data?.code === 'VCODE_INVALID' ? '验证码错误' : res.data?.code === 'VCODE_EXPIRED' ? '验证码已过期,请重新发送' : res.data?.code === 'IDENTITY_NOT_FOUND' ? '该账号未注册' : res.data?.message || '登录失败'
|
|
341
|
+
console.log(JSON.stringify({ ok: false, error: res.data?.code || 'login_failed', message: msg }))
|
|
342
|
+
process.exit(1)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
saveCredentials(platformUrl, res.sid, { userId: res.data?.userId })
|
|
346
|
+
console.log(JSON.stringify({ ok: true, sid: res.sid }))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function cmdRegister(flags) {
|
|
350
|
+
const platformUrl = flags.platform || DEFAULT_PLATFORM_URL
|
|
351
|
+
const identifier = flags.identifier
|
|
352
|
+
const code = flags.credential
|
|
353
|
+
const password = flags.password
|
|
354
|
+
|
|
355
|
+
if (!identifier || !code) {
|
|
356
|
+
console.error(JSON.stringify({ ok: false, error: 'missing --identifier and/or --credential' }))
|
|
357
|
+
process.exit(1)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const channel = detectChannel(identifier)
|
|
361
|
+
const target = normalizeIdentifier(identifier, channel)
|
|
362
|
+
|
|
363
|
+
const body = { channel, target, code }
|
|
364
|
+
if (password) body.password = password
|
|
365
|
+
|
|
366
|
+
const res = await api(platformUrl, 'POST', '/api/auth/register', body)
|
|
367
|
+
|
|
368
|
+
if (res.status === 409) {
|
|
369
|
+
console.log(JSON.stringify({ ok: false, error: 'IDENTITY_TAKEN', message: '该手机号/邮箱已注册,请直接登录' }))
|
|
370
|
+
process.exit(1)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (res.status !== 200 || !res.sid) {
|
|
374
|
+
const msg = res.data?.code === 'VCODE_INVALID' ? '验证码错误' : res.data?.code === 'VCODE_EXPIRED' ? '验证码已过期,请重新发送' : res.data?.message || '注册失败'
|
|
375
|
+
console.log(JSON.stringify({ ok: false, error: res.data?.code || 'register_failed', message: msg }))
|
|
376
|
+
process.exit(1)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
saveCredentials(platformUrl, res.sid, { userId: res.data?.userId })
|
|
380
|
+
console.log(JSON.stringify({ ok: true, sid: res.sid, userId: res.data?.userId }))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Main (create flow) ─────────────────────────────────
|
|
384
|
+
|
|
385
|
+
async function main() {
|
|
386
|
+
const { positional, flags } = parseFlags(process.argv.slice(2))
|
|
387
|
+
|
|
388
|
+
// Sub-commands: non-interactive helpers for Claude
|
|
389
|
+
const subcommand = positional[0]
|
|
390
|
+
if (subcommand === 'check-session') {
|
|
391
|
+
return cmdCheckSession(flags)
|
|
392
|
+
}
|
|
393
|
+
if (subcommand === 'send-vcode') {
|
|
394
|
+
return cmdSendVcode(flags)
|
|
395
|
+
}
|
|
396
|
+
if (subcommand === 'login') {
|
|
397
|
+
return cmdLogin(flags)
|
|
398
|
+
}
|
|
399
|
+
if (subcommand === 'register') {
|
|
400
|
+
return cmdRegister(flags)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Main create flow
|
|
404
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
405
|
+
|
|
406
|
+
console.log('\n @minus/create-skill — 创建新的 Skill 项目\n')
|
|
407
|
+
|
|
408
|
+
// 1. Collect skill info — 只问名称,问完立刻创建
|
|
409
|
+
let displayName
|
|
410
|
+
|
|
411
|
+
if (positional.length >= 1) {
|
|
412
|
+
displayName = positional[0]
|
|
413
|
+
} else {
|
|
414
|
+
displayName = (await rl.question(' ? 给你的 Skill 项目起个名字?(这会作为项目文件夹名): ')).trim()
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!displayName) {
|
|
418
|
+
console.error('\n ✗ 项目名称不能为空')
|
|
419
|
+
process.exit(1)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const description = flags.description || ''
|
|
423
|
+
const INPUT_TYPES = ['asin', 'keyword', 'file', 'custom', 'default']
|
|
424
|
+
const inputType = flags['input-type'] || 'asin'
|
|
425
|
+
if (!INPUT_TYPES.includes(inputType)) {
|
|
426
|
+
console.error(`\n ✗ 无效的输入类型: ${inputType}(可选: ${INPUT_TYPES.join(', ')})`)
|
|
427
|
+
process.exit(1)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 2. Platform auth + register skill
|
|
431
|
+
const platformUrl = flags.platform || process.env.MINUS_AI_PLATFORM_URL || DEFAULT_PLATFORM_URL
|
|
432
|
+
const nonInteractive = flags['non-interactive'] === true
|
|
433
|
+
console.log(`\n 平台: ${platformUrl}\n`)
|
|
434
|
+
|
|
435
|
+
let sid
|
|
436
|
+
if (flags.sid) {
|
|
437
|
+
const valid = await validateSession(platformUrl, flags.sid)
|
|
438
|
+
if (!valid) {
|
|
439
|
+
console.error('\n ✗ 提供的 --sid 无效或已过期')
|
|
440
|
+
process.exit(1)
|
|
441
|
+
}
|
|
442
|
+
sid = flags.sid
|
|
443
|
+
console.log(' ✓ 已使用 --sid 登录态')
|
|
444
|
+
} else if (nonInteractive) {
|
|
445
|
+
const creds = loadCredentials()
|
|
446
|
+
if (creds?.session_id && (await validateSession(platformUrl, creds.session_id))) {
|
|
447
|
+
sid = creds.session_id
|
|
448
|
+
console.log(' ✓ 已使用缓存的登录态')
|
|
449
|
+
} else {
|
|
450
|
+
console.error('\n ✗ 非交互模式下没有有效的登录态')
|
|
451
|
+
console.error(' 请先运行: node create-skill/index.mjs login --identifier <手机号或邮箱> --credential <验证码>')
|
|
452
|
+
process.exit(1)
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
sid = await ensureAuth(rl, platformUrl)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const skillResult = await createSkillOnPlatform(platformUrl, sid, { displayName, description })
|
|
459
|
+
|
|
460
|
+
console.log(` ✓ 注册成功`)
|
|
461
|
+
console.log(` skill_id: ${skillResult.id}`)
|
|
462
|
+
console.log(` api_key: ${skillResult.apiKey}`)
|
|
463
|
+
|
|
464
|
+
rl.close()
|
|
465
|
+
|
|
466
|
+
// 3. Generate project files
|
|
467
|
+
const folder = idToFolder(displayName)
|
|
468
|
+
const targetDir = join(process.cwd(), folder)
|
|
469
|
+
if (existsSync(targetDir)) {
|
|
470
|
+
console.error(`\n ✗ 目录 ${folder}/ 已存在`)
|
|
471
|
+
process.exit(1)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const className = idToClassName(skillResult.id)
|
|
475
|
+
const namespace = folder
|
|
476
|
+
const port = String(await findAvailablePort(4001))
|
|
477
|
+
const apiKey = skillResult.apiKey
|
|
478
|
+
|
|
479
|
+
const vars = { skillId: skillResult.id, folder, className, displayName, description, namespace, port }
|
|
480
|
+
|
|
481
|
+
// Backend files (pipeline.py from input-type subdir)
|
|
482
|
+
writeOut(join(targetDir, 'pipeline.py'), render(readTemplate('pipeline.py.tpl', inputType), vars))
|
|
483
|
+
writeOut(join(targetDir, 'server.py'), render(readTemplate('server.py.tpl'), vars))
|
|
484
|
+
writeOut(join(targetDir, 'pyproject.toml'), render(readTemplate('pyproject.toml.tpl'), vars))
|
|
485
|
+
writeOut(join(targetDir, '.env.example'), render(readTemplate('env.example.tpl'), vars))
|
|
486
|
+
writeOut(join(targetDir, '.env.local'), `MINUS_AI_SKILL_API_KEY=${apiKey}\nMINUS_AI_PLATFORM_URL=${platformUrl}\n`)
|
|
487
|
+
writeOut(join(targetDir, '.gitignore'), render(readTemplate('gitignore.tpl'), vars))
|
|
488
|
+
|
|
489
|
+
writeOut(
|
|
490
|
+
join(targetDir, '.minus', 'skill.json'),
|
|
491
|
+
JSON.stringify(
|
|
492
|
+
{
|
|
493
|
+
skillId: skillResult.id,
|
|
494
|
+
version: skillResult.draftVersion ?? skillResult.version ?? '1.0-alpha.1'
|
|
495
|
+
},
|
|
496
|
+
null,
|
|
497
|
+
2
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
// CLAUDE.md — Claude Code 项目指令(根据 inputType 动态生成)
|
|
502
|
+
const templateDocs = {
|
|
503
|
+
asin: `## 模板能力(inputType: asin)
|
|
504
|
+
- 输入组件:\`AmazonSearchBar\` + \`CountrySelect\`(来自 @minus/platform-widgets)
|
|
505
|
+
- 校验函数:\`validateAsins(value)\` — 校验 ASIN 格式,返回 { asins: string[], error? }
|
|
506
|
+
- 支持单个或多个 ASIN 输入(逗号/换行分隔)
|
|
507
|
+
- 修改输入验证规则:编辑 frontend/src/main.tsx 中的 handleSubmit 函数
|
|
508
|
+
- 修改 pipeline 步骤:编辑 pipeline.py 中的 step_N 方法`,
|
|
509
|
+
keyword: `## 模板能力(inputType: keyword)
|
|
510
|
+
- 输入组件:\`AmazonSearchBar\` + \`CountrySelect\`(来自 @minus/platform-widgets)
|
|
511
|
+
- 校验函数:\`validateKeywords(value)\` — 校验关键词格式,返回 { keywords: string[], error? }
|
|
512
|
+
- 支持单个或多个关键词输入(逗号/换行分隔)
|
|
513
|
+
- 修改输入验证规则:编辑 frontend/src/main.tsx 中的 handleSubmit 函数
|
|
514
|
+
- 修改 pipeline 步骤:编辑 pipeline.py 中的 step_N 方法`,
|
|
515
|
+
file: `## 模板能力(inputType: file)
|
|
516
|
+
- 输入组件:\`FilePicker\`(来自 @minus/platform-widgets)
|
|
517
|
+
- 上传函数:\`uploadFile(file)\` — 上传文件到平台
|
|
518
|
+
- 修改 pipeline 步骤:编辑 pipeline.py 中的 step_N 方法`,
|
|
519
|
+
custom: `## 模板能力(inputType: custom)
|
|
520
|
+
- 输入组件:纯文本输入框(自定义)
|
|
521
|
+
- 无预置校验,根据业务需求自行实现
|
|
522
|
+
- 修改 pipeline 步骤:编辑 pipeline.py 中的 step_N 方法`,
|
|
523
|
+
default: `## 模板能力(inputType: default)
|
|
524
|
+
- 默认页面:展示 Skill 名称、描述、适用场景、标签
|
|
525
|
+
- 输入组件:无(由三步法第一步确认后生成)
|
|
526
|
+
- 修改 pipeline 步骤:编辑 pipeline.py 中的 step_N 方法`
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
writeOut(
|
|
530
|
+
join(targetDir, 'CLAUDE.md'),
|
|
531
|
+
`# ${displayName}
|
|
532
|
+
|
|
533
|
+
Minus Skill Project
|
|
534
|
+
|
|
535
|
+
## Commands
|
|
536
|
+
- \`/minus\` — 进入开发模式
|
|
537
|
+
- \`/minus help\` — 查看帮助
|
|
538
|
+
- \`/minus publish\` — 校验、打包、发布
|
|
539
|
+
|
|
540
|
+
${templateDocs[inputType] || templateDocs.custom}
|
|
541
|
+
|
|
542
|
+
## 项目结构
|
|
543
|
+
- \`pipeline.py\` — 后端 pipeline 逻辑,每个步骤是一个 step_N 方法
|
|
544
|
+
- \`server.py\` — ASGI 服务入口
|
|
545
|
+
- \`frontend/src/main.tsx\` — 前端入口,包含输入表单和结果渲染
|
|
546
|
+
- \`frontend/src/locales/\` — 多语言文件
|
|
547
|
+
- \`.minus/skill.json\` — 项目标识(Skill ID)
|
|
548
|
+
- \`.env.local\` — 运行时凭证(MINUS_AI_SKILL_API_KEY)
|
|
549
|
+
- \`tests/\` — 测试用例
|
|
550
|
+
|
|
551
|
+
## SDK 使用规则(重要)
|
|
552
|
+
|
|
553
|
+
写代码前必须先了解 SDK 提供的能力,优先使用 SDK 已有的组件和方法,不要手写。
|
|
554
|
+
|
|
555
|
+
### 开发手册
|
|
556
|
+
写前端代码前,读以下开发手册:
|
|
557
|
+
- 读 \`${join(__dirname, '../../runtime/frontend-guide')}/*/doc.md\`
|
|
558
|
+
- 手册包含:前后端数据契约(StepOutcome → StepRenderCtx)、Widget 选型与用法(defineWidgetStep、内置 Interactive/Display Widget)、自定义 Widget 开发、多语言接入。
|
|
559
|
+
|
|
560
|
+
### 后端 SDK 参考
|
|
561
|
+
写 pipeline.py 前,读以下本地文件了解可用 API:
|
|
562
|
+
- \`StepOutcome\`:读 \`.venv/**/minus_ai_sdk/pipeline/outcome.py\`
|
|
563
|
+
- \`PipelineContext\`:读 \`.venv/**/minus_ai_sdk/pipeline/context.py\`
|
|
564
|
+
|
|
565
|
+
### 前端 SDK 参考
|
|
566
|
+
@minus/* 包通过平台 CDN 加载,本地无源码。写前端代码前,读以下文档了解可用 API:
|
|
567
|
+
- Widget 框架:读 \`${join(__dirname, '../../runtime/widget-framework')}/*/docs.md\`
|
|
568
|
+
- 平台组件:读 \`${join(__dirname, '../../runtime/platform-widgets')}/*/docs.md\`
|
|
569
|
+
|
|
570
|
+
修改前端代码后,同步更新 \`frontend/src/locales/\` 下的多语言文件。
|
|
571
|
+
|
|
572
|
+
## 开发约定
|
|
573
|
+
- 新增 pipeline 步骤:在 pipeline.py 中添加 \`async def step_N(self, ctx)\` 方法
|
|
574
|
+
- 新增前端步骤渲染:在 main.tsx 的 \`buildSteps\` 函数中添加步骤配置
|
|
575
|
+
- Skill 业务信息(名称、描述、步骤定义)存储在后端,通过 Plugin 的 \`skill_update\` MCP tool 管理
|
|
576
|
+
`
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
// README.md
|
|
580
|
+
writeOut(join(targetDir, 'README.md'), render(readTemplate('README.md.tpl'), vars))
|
|
581
|
+
|
|
582
|
+
// frontend/assets/ 和 tests/ 目录
|
|
583
|
+
writeOut(join(targetDir, 'frontend', 'assets', '.gitkeep'), '')
|
|
584
|
+
writeOut(join(targetDir, 'tests', '.gitkeep'), '')
|
|
585
|
+
|
|
586
|
+
const rootPkgContent = render(readTemplate('root-package.json.tpl'), vars)
|
|
587
|
+
const fePkgContent = render(readTemplate('frontend-package.json.tpl'), vars)
|
|
588
|
+
writeOut(join(targetDir, 'package.json'), rootPkgContent)
|
|
589
|
+
writeOut(join(targetDir, 'pnpm-workspace.yaml'), readTemplate('pnpm-workspace.yaml.tpl'))
|
|
590
|
+
|
|
591
|
+
// Frontend files
|
|
592
|
+
writeOut(join(targetDir, 'frontend/package.json'), fePkgContent)
|
|
593
|
+
writeOut(join(targetDir, 'frontend/vite.config.ts'), render(readTemplate('vite.config.ts.tpl'), vars))
|
|
594
|
+
writeOut(join(targetDir, 'frontend/embed.html'), render(readTemplate('embed.html.tpl'), vars))
|
|
595
|
+
writeOut(join(targetDir, 'frontend/tsconfig.json'), render(readTemplate('tsconfig.json.tpl'), vars))
|
|
596
|
+
writeOut(join(targetDir, 'frontend/src/main.tsx'), render(readTemplate('main.tsx.tpl', inputType), vars))
|
|
597
|
+
writeOut(join(targetDir, 'frontend/src/locales/zh-CN.json'), render(readTemplate('zh-CN.json.tpl', inputType), vars))
|
|
598
|
+
writeOut(join(targetDir, 'frontend/src/locales/en-US.json'), render(readTemplate('en-US.json.tpl', inputType), vars))
|
|
599
|
+
|
|
600
|
+
// 类型声明文件(让 tsc build 能识别 @minus/* 模块)
|
|
601
|
+
writeOut(join(targetDir, 'frontend/src/minus-runtime.d.ts'), `declare module '@minus/*';\n`)
|
|
602
|
+
|
|
603
|
+
// 4. git init (skip if git not installed)
|
|
604
|
+
try {
|
|
605
|
+
execSync('git --version', { stdio: 'pipe' })
|
|
606
|
+
execSync('git init', { cwd: targetDir, stdio: 'pipe' })
|
|
607
|
+
execSync('git add -A && git commit -m "init: scaffold by create-skill"', { cwd: targetDir, stdio: 'pipe' })
|
|
608
|
+
console.log(' ✓ Git 仓库已初始化')
|
|
609
|
+
} catch {}
|
|
610
|
+
|
|
611
|
+
// 5. 注册到 projects.json
|
|
612
|
+
try {
|
|
613
|
+
const projectsFile = join(homedir(), '.minus', 'projects.json')
|
|
614
|
+
let projects = { projects: [] }
|
|
615
|
+
try {
|
|
616
|
+
projects = JSON.parse(readFileSync(projectsFile, 'utf-8'))
|
|
617
|
+
} catch {}
|
|
618
|
+
if (!projects.projects) projects.projects = []
|
|
619
|
+
const now = new Date().toISOString()
|
|
620
|
+
if (!projects.projects.find(p => p.path === targetDir)) {
|
|
621
|
+
projects.projects.push({ name: displayName, path: targetDir, created_at: now, last_opened: now })
|
|
622
|
+
mkdirSync(dirname(projectsFile), { recursive: true })
|
|
623
|
+
writeFileSync(projectsFile, JSON.stringify(projects, null, 2))
|
|
624
|
+
console.log(' ✓ 已注册到项目列表')
|
|
625
|
+
}
|
|
626
|
+
} catch {
|
|
627
|
+
console.log(' ⚠ 注册到 projects.json 失败')
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// 依赖安装延迟到首次进入项目时(Phase 4),scaffold 阶段不安装
|
|
631
|
+
|
|
632
|
+
// Output JSON summary for non-interactive consumers
|
|
633
|
+
if (nonInteractive) {
|
|
634
|
+
console.log(
|
|
635
|
+
`\n__CREATE_RESULT__${JSON.stringify({
|
|
636
|
+
folder,
|
|
637
|
+
targetDir,
|
|
638
|
+
skillId: skillResult.id,
|
|
639
|
+
apiKey: skillResult.apiKey,
|
|
640
|
+
port,
|
|
641
|
+
inputType
|
|
642
|
+
})}`
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
console.log(`
|
|
647
|
+
✓ 项目已创建:${folder}/
|
|
648
|
+
位置:${targetDir}
|
|
649
|
+
|
|
650
|
+
下一步:打开项目文件夹开始开发
|
|
651
|
+
依赖会在首次进入项目时自动安装。
|
|
652
|
+
`)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
main().catch(e => {
|
|
656
|
+
console.error(e)
|
|
657
|
+
process.exit(1)
|
|
658
|
+
})
|