@syphin/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/init.js +120 -43
- package/src/commands/switch.js +98 -0
- package/src/index.js +2 -0
- package/src/lib/api.js +1 -0
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -3,21 +3,20 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import ora from 'ora'
|
|
6
|
+
import prompts from 'prompts'
|
|
6
7
|
import { basename } from 'path'
|
|
7
8
|
import { readFileSync, writeFileSync, existsSync } from 'fs'
|
|
8
9
|
import { loadToken, saveProjectConfig } from '../lib/config.js'
|
|
9
10
|
import { createApiClient } from '../lib/api.js'
|
|
10
|
-
import { success, fail, info, teal } from '../lib/output.js'
|
|
11
|
+
import { success, fail, info, teal, dim } from '../lib/output.js'
|
|
11
12
|
|
|
12
13
|
function detectProjectName() {
|
|
13
|
-
// Try package.json
|
|
14
14
|
if (existsSync('package.json')) {
|
|
15
15
|
try {
|
|
16
16
|
const pkg = JSON.parse(readFileSync('package.json', 'utf-8'))
|
|
17
17
|
if (pkg.name) return pkg.name.replace(/^@[^/]+\//, '')
|
|
18
18
|
} catch {}
|
|
19
19
|
}
|
|
20
|
-
// Fall back to directory name
|
|
21
20
|
return basename(process.cwd())
|
|
22
21
|
}
|
|
23
22
|
|
|
@@ -33,15 +32,96 @@ function updateMcpJson() {
|
|
|
33
32
|
|
|
34
33
|
if (!mcp.mcpServers) mcp.mcpServers = {}
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
type: 'stdio'
|
|
40
|
-
|
|
35
|
+
const isWindows = process.platform === 'win32'
|
|
36
|
+
|
|
37
|
+
mcp.mcpServers.syphin = isWindows
|
|
38
|
+
? { command: 'cmd', args: ['/c', 'npx', '@syphin/mcp'], type: 'stdio' }
|
|
39
|
+
: { command: 'npx', args: ['@syphin/mcp'], type: 'stdio' }
|
|
41
40
|
|
|
42
41
|
writeFileSync(mcpPath, JSON.stringify(mcp, null, 2))
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
async function pickProject(api, skipPrompts) {
|
|
45
|
+
const spinner = ora('Fetching your projects...').start()
|
|
46
|
+
let projects
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
projects = await api.listProjects()
|
|
50
|
+
spinner.stop()
|
|
51
|
+
} catch (err) {
|
|
52
|
+
spinner.fail('Failed to fetch projects')
|
|
53
|
+
fail(err.message)
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (projects.length === 0) {
|
|
58
|
+
console.log('')
|
|
59
|
+
info('No projects found. Creating one from this directory.')
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (skipPrompts) {
|
|
64
|
+
// --yes: match by directory name
|
|
65
|
+
const detected = detectProjectName().toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
66
|
+
const match = projects.find(p => p.slug === detected)
|
|
67
|
+
if (match) return match
|
|
68
|
+
info(`No project matching "${detected}" — creating it.`)
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log('')
|
|
73
|
+
const choices = [
|
|
74
|
+
...projects.map(p => ({
|
|
75
|
+
title: `${p.name} ${dim(`(${p.slug})`)}`,
|
|
76
|
+
value: p,
|
|
77
|
+
})),
|
|
78
|
+
{ title: teal('+ Create new project'), value: null },
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
const { project } = await prompts({
|
|
82
|
+
type: 'select',
|
|
83
|
+
name: 'project',
|
|
84
|
+
message: 'Connect to a project',
|
|
85
|
+
choices,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (project === undefined) {
|
|
89
|
+
// User cancelled
|
|
90
|
+
process.exit(0)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return project
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function createNewProject(api, skipPrompts) {
|
|
97
|
+
const detected = detectProjectName()
|
|
98
|
+
let name = detected
|
|
99
|
+
|
|
100
|
+
if (!skipPrompts) {
|
|
101
|
+
const response = await prompts({
|
|
102
|
+
type: 'text',
|
|
103
|
+
name: 'name',
|
|
104
|
+
message: 'Project name',
|
|
105
|
+
initial: detected,
|
|
106
|
+
})
|
|
107
|
+
if (!response.name) process.exit(0)
|
|
108
|
+
name = response.name
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
112
|
+
const spinner = ora('Creating project...').start()
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const project = await api.createProject(name, slug)
|
|
116
|
+
spinner.succeed(`Created ${teal(name)}`)
|
|
117
|
+
return project
|
|
118
|
+
} catch (err) {
|
|
119
|
+
spinner.fail('Failed to create project')
|
|
120
|
+
fail(err.message)
|
|
121
|
+
process.exit(1)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
45
125
|
export function registerInit(program) {
|
|
46
126
|
program
|
|
47
127
|
.command('init')
|
|
@@ -55,49 +135,46 @@ export function registerInit(program) {
|
|
|
55
135
|
process.exit(1)
|
|
56
136
|
}
|
|
57
137
|
|
|
58
|
-
const
|
|
59
|
-
const slug = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
60
|
-
const env = opts.env
|
|
61
|
-
|
|
62
|
-
info(`Project: ${teal(projectName)} (${slug})`)
|
|
63
|
-
info(`Environment: ${env}`)
|
|
64
|
-
|
|
65
|
-
const spinner = ora('Connecting to Syphin...').start()
|
|
138
|
+
const api = createApiClient(token)
|
|
66
139
|
|
|
140
|
+
// Verify auth
|
|
141
|
+
const spinner = ora('Authenticating...').start()
|
|
67
142
|
try {
|
|
68
|
-
const api = createApiClient(token)
|
|
69
|
-
|
|
70
|
-
// Verify auth first
|
|
71
143
|
await api.verify()
|
|
144
|
+
spinner.stop()
|
|
145
|
+
} catch (err) {
|
|
146
|
+
spinner.fail('Authentication failed')
|
|
147
|
+
fail(err.message)
|
|
148
|
+
process.exit(1)
|
|
149
|
+
}
|
|
72
150
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Update .mcp.json
|
|
80
|
-
updateMcpJson()
|
|
151
|
+
// Pick or create project
|
|
152
|
+
let project = await pickProject(api, opts.yes)
|
|
153
|
+
if (!project) {
|
|
154
|
+
project = await createNewProject(api, opts.yes)
|
|
155
|
+
}
|
|
81
156
|
|
|
82
|
-
|
|
157
|
+
const env = opts.env
|
|
83
158
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
info(`Total always-on tokens: ${teal('~' + manifest.totalTokens)}`)
|
|
91
|
-
} catch {
|
|
92
|
-
info('No manifest yet — the project may need skills bound to it.')
|
|
93
|
-
}
|
|
159
|
+
// Save config
|
|
160
|
+
saveProjectConfig({
|
|
161
|
+
projectSlug: project.slug,
|
|
162
|
+
environment: env,
|
|
163
|
+
})
|
|
164
|
+
updateMcpJson()
|
|
94
165
|
|
|
166
|
+
// Show manifest summary
|
|
167
|
+
try {
|
|
168
|
+
const manifest = await api.getManifest(project.slug, env)
|
|
95
169
|
console.log('')
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
170
|
+
info(`Always-on skills: ${teal(manifest.alwaysOn.length.toString())}`)
|
|
171
|
+
info(`Available skills: ${teal(manifest.available.length.toString())}`)
|
|
172
|
+
info(`Total always-on tokens: ${teal('~' + manifest.totalTokens)}`)
|
|
173
|
+
} catch {
|
|
174
|
+
info('No manifest yet — bind skills to this project in the dashboard.')
|
|
101
175
|
}
|
|
176
|
+
|
|
177
|
+
console.log('')
|
|
178
|
+
success(`Connected to ${teal(project.name || project.slug)} (${env}). Restart Claude Code to activate Syphin.`)
|
|
102
179
|
})
|
|
103
180
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syphin switch
|
|
3
|
+
*
|
|
4
|
+
* Quick project switcher for an already-initialized directory.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import ora from 'ora'
|
|
8
|
+
import prompts from 'prompts'
|
|
9
|
+
import { loadToken, loadProjectConfig, saveProjectConfig } from '../lib/config.js'
|
|
10
|
+
import { createApiClient } from '../lib/api.js'
|
|
11
|
+
import { success, fail, info, teal, dim } from '../lib/output.js'
|
|
12
|
+
|
|
13
|
+
export function registerSwitch(program) {
|
|
14
|
+
program
|
|
15
|
+
.command('switch')
|
|
16
|
+
.description('Switch to a different Syphin project')
|
|
17
|
+
.option('--env <env>', 'Change environment too')
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
const token = loadToken()
|
|
20
|
+
if (!token) {
|
|
21
|
+
fail('Not authenticated. Run `syphin login` first.')
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const config = loadProjectConfig()
|
|
26
|
+
if (!config) {
|
|
27
|
+
fail('No project config found. Run `syphin init` first.')
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const api = createApiClient(token)
|
|
32
|
+
|
|
33
|
+
const spinner = ora('Fetching your projects...').start()
|
|
34
|
+
let projects
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
projects = await api.listProjects()
|
|
38
|
+
spinner.stop()
|
|
39
|
+
} catch (err) {
|
|
40
|
+
spinner.fail('Failed to fetch projects')
|
|
41
|
+
fail(err.message)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (projects.length === 0) {
|
|
46
|
+
fail('No projects found. Create one in the dashboard first.')
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log('')
|
|
51
|
+
info(`Current: ${teal(config.projectSlug)} (${config.environment})`)
|
|
52
|
+
console.log('')
|
|
53
|
+
|
|
54
|
+
const choices = projects.map(p => ({
|
|
55
|
+
title: p.slug === config.projectSlug
|
|
56
|
+
? `${p.name} ${dim(`(${p.slug})`)} ${teal('← current')}`
|
|
57
|
+
: `${p.name} ${dim(`(${p.slug})`)}`,
|
|
58
|
+
value: p,
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
const { project } = await prompts({
|
|
62
|
+
type: 'select',
|
|
63
|
+
name: 'project',
|
|
64
|
+
message: 'Switch to',
|
|
65
|
+
choices,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (project === undefined) {
|
|
69
|
+
process.exit(0)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (project.slug === config.projectSlug && !opts.env) {
|
|
73
|
+
info('Already connected to this project.')
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const env = opts.env || config.environment
|
|
78
|
+
|
|
79
|
+
saveProjectConfig({
|
|
80
|
+
projectSlug: project.slug,
|
|
81
|
+
environment: env,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Show manifest summary
|
|
85
|
+
try {
|
|
86
|
+
const manifest = await api.getManifest(project.slug, env)
|
|
87
|
+
console.log('')
|
|
88
|
+
info(`Always-on skills: ${teal(manifest.alwaysOn.length.toString())}`)
|
|
89
|
+
info(`Available skills: ${teal(manifest.available.length.toString())}`)
|
|
90
|
+
info(`Total always-on tokens: ${teal('~' + manifest.totalTokens)}`)
|
|
91
|
+
} catch {
|
|
92
|
+
info('No manifest yet — bind skills to this project in the dashboard.')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('')
|
|
96
|
+
success(`Switched to ${teal(project.name)} (${env}). Restart Claude Code to activate.`)
|
|
97
|
+
})
|
|
98
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { Command } from 'commander'
|
|
7
7
|
import { registerLogin } from './commands/login.js'
|
|
8
8
|
import { registerInit } from './commands/init.js'
|
|
9
|
+
import { registerSwitch } from './commands/switch.js'
|
|
9
10
|
import { registerAuth } from './commands/auth.js'
|
|
10
11
|
import { registerCache } from './commands/cache.js'
|
|
11
12
|
import { loadToken, loadProjectConfig } from './lib/config.js'
|
|
@@ -21,6 +22,7 @@ program
|
|
|
21
22
|
|
|
22
23
|
registerLogin(program)
|
|
23
24
|
registerInit(program)
|
|
25
|
+
registerSwitch(program)
|
|
24
26
|
registerAuth(program)
|
|
25
27
|
registerCache(program)
|
|
26
28
|
|
package/src/lib/api.js
CHANGED
|
@@ -49,6 +49,7 @@ export function createApiClient(token, apiUrl) {
|
|
|
49
49
|
|
|
50
50
|
return {
|
|
51
51
|
verify: () => request('POST', '/api/auth/verify'),
|
|
52
|
+
listProjects: () => request('GET', '/api/projects'),
|
|
52
53
|
getManifest: (slug, env) => request('GET', `/api/projects/${slug}/manifest?env=${env}`),
|
|
53
54
|
createProject: (name, slug) => request('POST', '/api/projects', { name, slug }),
|
|
54
55
|
requestDeviceCode: () => requestNoAuth('POST', '/api/auth/device/code'),
|