@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syphin/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Syphin CLI — centralized AI agent context",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- mcp.mcpServers.syphin = {
37
- command: 'npx',
38
- args: ['@syphin/mcp'],
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 projectName = detectProjectName()
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
- // Save project config
74
- saveProjectConfig({
75
- projectSlug: slug,
76
- environment: env,
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
- spinner.succeed('Connected!')
157
+ const env = opts.env
83
158
 
84
- // Try to fetch manifest as connection test
85
- try {
86
- const manifest = await api.getManifest(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 — 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
- success('Project connected. Restart Claude Code to activate Syphin.')
97
- } catch (err) {
98
- spinner.fail('Connection failed')
99
- fail(err.message)
100
- process.exit(1)
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'),