@syphin/cli 0.1.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/README.md +65 -0
- package/package.json +28 -0
- package/src/commands/auth.js +39 -0
- package/src/commands/cache.js +59 -0
- package/src/commands/init.js +103 -0
- package/src/commands/login.js +49 -0
- package/src/index.js +72 -0
- package/src/lib/api.js +36 -0
- package/src/lib/config.js +60 -0
- package/src/lib/output.js +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# syphin
|
|
2
|
+
|
|
3
|
+
CLI for [Syphin](https://syphin.dev) — centralized AI agent context.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g syphin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
### `syphin login`
|
|
14
|
+
|
|
15
|
+
Authenticate with Syphin.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
syphin login --token <your-token>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Get a token from the [Syphin dashboard](https://app.syphin.dev/settings/tokens).
|
|
22
|
+
|
|
23
|
+
### `syphin init`
|
|
24
|
+
|
|
25
|
+
Connect the current directory to Syphin. Creates `.syphin/config.json` and adds the MCP server to `.mcp.json`.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
syphin init
|
|
29
|
+
syphin init --env staging
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `syphin status`
|
|
33
|
+
|
|
34
|
+
Check connection status and show the live manifest.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
syphin status
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### `syphin whoami`
|
|
41
|
+
|
|
42
|
+
Show current auth context (org, plan, token type).
|
|
43
|
+
|
|
44
|
+
### `syphin logout`
|
|
45
|
+
|
|
46
|
+
Clear stored credentials.
|
|
47
|
+
|
|
48
|
+
### `syphin cache`
|
|
49
|
+
|
|
50
|
+
Show local cache stats (cached skills and manifests).
|
|
51
|
+
|
|
52
|
+
### `syphin cache clear`
|
|
53
|
+
|
|
54
|
+
Clear the local cache.
|
|
55
|
+
|
|
56
|
+
## Environment variables
|
|
57
|
+
|
|
58
|
+
| Variable | Description |
|
|
59
|
+
|----------|-------------|
|
|
60
|
+
| `SYPHIN_TOKEN` | Auth token (alternative to `syphin login`) |
|
|
61
|
+
| `SYPHIN_API_URL` | API base URL (default: `https://syphin.vercel.app`) |
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@syphin/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Syphin CLI — centralized AI agent context",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "syphin": "src/index.js" },
|
|
8
|
+
"files": ["src", "README.md"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/index.js",
|
|
11
|
+
"dev": "node --watch src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["syphin", "cli", "ai", "agent", "context", "mcp", "claude"],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/dbigby/syphin.git",
|
|
17
|
+
"directory": "packages/cli"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://syphin.dev",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^12.0.0",
|
|
22
|
+
"ora": "^8.0.1",
|
|
23
|
+
"chalk": "^5.3.0",
|
|
24
|
+
"prompts": "^2.4.2",
|
|
25
|
+
"open": "^10.1.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": { "node": ">=18.0.0" }
|
|
28
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syphin whoami / syphin logout
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { loadToken, clearToken } from '../lib/config.js'
|
|
6
|
+
import { createApiClient } from '../lib/api.js'
|
|
7
|
+
import { success, fail, info, teal } from '../lib/output.js'
|
|
8
|
+
|
|
9
|
+
export function registerAuth(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('whoami')
|
|
12
|
+
.description('Show current auth context')
|
|
13
|
+
.action(async () => {
|
|
14
|
+
const token = loadToken()
|
|
15
|
+
if (!token) {
|
|
16
|
+
fail('Not authenticated. Run `syphin login` first.')
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const api = createApiClient(token)
|
|
22
|
+
const result = await api.verify()
|
|
23
|
+
info(`Organization: ${teal(result.orgName)} (${result.orgSlug})`)
|
|
24
|
+
info(`Plan: ${result.orgPlan}`)
|
|
25
|
+
info(`Token type: ${result.tokenType}`)
|
|
26
|
+
} catch (err) {
|
|
27
|
+
fail(`Token invalid: ${err.message}`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('logout')
|
|
34
|
+
.description('Clear stored credentials')
|
|
35
|
+
.action(() => {
|
|
36
|
+
clearToken()
|
|
37
|
+
success('Logged out.')
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syphin cache / syphin cache clear
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readdirSync, rmSync, statSync } from 'fs'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { homedir } from 'os'
|
|
8
|
+
import { success, info, teal } from '../lib/output.js'
|
|
9
|
+
|
|
10
|
+
const CACHE_DIR = join(homedir(), '.cache', 'syphin')
|
|
11
|
+
|
|
12
|
+
function countFiles(dir) {
|
|
13
|
+
if (!existsSync(dir)) return 0
|
|
14
|
+
return readdirSync(dir).length
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function dirSize(dir) {
|
|
18
|
+
if (!existsSync(dir)) return 0
|
|
19
|
+
let total = 0
|
|
20
|
+
for (const f of readdirSync(dir)) {
|
|
21
|
+
const fp = join(dir, f)
|
|
22
|
+
try {
|
|
23
|
+
total += statSync(fp).size
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
return total
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatBytes(bytes) {
|
|
30
|
+
if (bytes < 1024) return `${bytes} B`
|
|
31
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
32
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function registerCache(program) {
|
|
36
|
+
const cache = program
|
|
37
|
+
.command('cache')
|
|
38
|
+
.description('Show cache stats')
|
|
39
|
+
.action(() => {
|
|
40
|
+
const skills = countFiles(join(CACHE_DIR, 'skills'))
|
|
41
|
+
const manifests = countFiles(join(CACHE_DIR, 'manifests'))
|
|
42
|
+
const size = dirSize(join(CACHE_DIR, 'skills')) + dirSize(join(CACHE_DIR, 'manifests'))
|
|
43
|
+
|
|
44
|
+
info(`Cache directory: ${CACHE_DIR}`)
|
|
45
|
+
info(`Cached skills: ${teal(skills.toString())}`)
|
|
46
|
+
info(`Cached manifests: ${teal(manifests.toString())}`)
|
|
47
|
+
info(`Total size: ${formatBytes(size)}`)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
cache
|
|
51
|
+
.command('clear')
|
|
52
|
+
.description('Clear the local cache')
|
|
53
|
+
.action(() => {
|
|
54
|
+
if (existsSync(CACHE_DIR)) {
|
|
55
|
+
rmSync(CACHE_DIR, { recursive: true })
|
|
56
|
+
}
|
|
57
|
+
success('Cache cleared.')
|
|
58
|
+
})
|
|
59
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syphin init [--yes] [--env <env>]
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import ora from 'ora'
|
|
6
|
+
import { basename } from 'path'
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs'
|
|
8
|
+
import { loadToken, saveProjectConfig } from '../lib/config.js'
|
|
9
|
+
import { createApiClient } from '../lib/api.js'
|
|
10
|
+
import { success, fail, info, teal } from '../lib/output.js'
|
|
11
|
+
|
|
12
|
+
function detectProjectName() {
|
|
13
|
+
// Try package.json
|
|
14
|
+
if (existsSync('package.json')) {
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf-8'))
|
|
17
|
+
if (pkg.name) return pkg.name.replace(/^@[^/]+\//, '')
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
20
|
+
// Fall back to directory name
|
|
21
|
+
return basename(process.cwd())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function updateMcpJson() {
|
|
25
|
+
const mcpPath = '.mcp.json'
|
|
26
|
+
let mcp = {}
|
|
27
|
+
|
|
28
|
+
if (existsSync(mcpPath)) {
|
|
29
|
+
try {
|
|
30
|
+
mcp = JSON.parse(readFileSync(mcpPath, 'utf-8'))
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!mcp.mcpServers) mcp.mcpServers = {}
|
|
35
|
+
|
|
36
|
+
mcp.mcpServers.syphin = {
|
|
37
|
+
command: 'npx',
|
|
38
|
+
args: ['@syphin/mcp'],
|
|
39
|
+
type: 'stdio',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
writeFileSync(mcpPath, JSON.stringify(mcp, null, 2))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function registerInit(program) {
|
|
46
|
+
program
|
|
47
|
+
.command('init')
|
|
48
|
+
.description('Connect current directory to Syphin')
|
|
49
|
+
.option('--yes', 'Skip prompts')
|
|
50
|
+
.option('--env <env>', 'Environment', 'dev')
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
const token = loadToken()
|
|
53
|
+
if (!token) {
|
|
54
|
+
fail('Not authenticated. Run `syphin login` first.')
|
|
55
|
+
process.exit(1)
|
|
56
|
+
}
|
|
57
|
+
|
|
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()
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const api = createApiClient(token)
|
|
69
|
+
|
|
70
|
+
// Verify auth first
|
|
71
|
+
await api.verify()
|
|
72
|
+
|
|
73
|
+
// Save project config
|
|
74
|
+
saveProjectConfig({
|
|
75
|
+
projectSlug: slug,
|
|
76
|
+
environment: env,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Update .mcp.json
|
|
80
|
+
updateMcpJson()
|
|
81
|
+
|
|
82
|
+
spinner.succeed('Connected!')
|
|
83
|
+
|
|
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
|
+
}
|
|
94
|
+
|
|
95
|
+
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)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syphin login [--token <t>] [--force]
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import ora from 'ora'
|
|
6
|
+
import { saveToken, loadToken } from '../lib/config.js'
|
|
7
|
+
import { createApiClient } from '../lib/api.js'
|
|
8
|
+
import { success, fail, info, teal } from '../lib/output.js'
|
|
9
|
+
|
|
10
|
+
export function registerLogin(program) {
|
|
11
|
+
program
|
|
12
|
+
.command('login')
|
|
13
|
+
.description('Authenticate with Syphin')
|
|
14
|
+
.option('--token <token>', 'Direct token authentication')
|
|
15
|
+
.option('--force', 'Re-authenticate even if already logged in')
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
const existing = loadToken()
|
|
18
|
+
if (existing && !opts.force && !opts.token) {
|
|
19
|
+
info('Already authenticated. Use --force to re-authenticate.')
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const token = opts.token
|
|
24
|
+
if (!token) {
|
|
25
|
+
fail('Token required. Use: syphin login --token <your-token>')
|
|
26
|
+
info('Get a token from the Syphin dashboard at https://app.syphin.dev/settings/tokens')
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const spinner = ora('Verifying token...').start()
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const api = createApiClient(token)
|
|
34
|
+
const result = await api.verify()
|
|
35
|
+
|
|
36
|
+
saveToken(token)
|
|
37
|
+
spinner.succeed('Authenticated successfully!')
|
|
38
|
+
|
|
39
|
+
console.log('')
|
|
40
|
+
info(`Organization: ${teal(result.orgName)} (${result.orgSlug})`)
|
|
41
|
+
info(`Plan: ${result.orgPlan}`)
|
|
42
|
+
console.log('')
|
|
43
|
+
} catch (err) {
|
|
44
|
+
spinner.fail('Authentication failed')
|
|
45
|
+
fail(err.message)
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* syphin CLI — login, init, status, cache management.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Command } from 'commander'
|
|
7
|
+
import { registerLogin } from './commands/login.js'
|
|
8
|
+
import { registerInit } from './commands/init.js'
|
|
9
|
+
import { registerAuth } from './commands/auth.js'
|
|
10
|
+
import { registerCache } from './commands/cache.js'
|
|
11
|
+
import { loadToken, loadProjectConfig } from './lib/config.js'
|
|
12
|
+
import { createApiClient } from './lib/api.js'
|
|
13
|
+
import { info, fail, teal, dim } from './lib/output.js'
|
|
14
|
+
|
|
15
|
+
const program = new Command()
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.name('syphin')
|
|
19
|
+
.description('Syphin CLI — centralized AI agent context')
|
|
20
|
+
.version('0.1.0')
|
|
21
|
+
|
|
22
|
+
registerLogin(program)
|
|
23
|
+
registerInit(program)
|
|
24
|
+
registerAuth(program)
|
|
25
|
+
registerCache(program)
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('status')
|
|
29
|
+
.description('Check connection status and live manifest')
|
|
30
|
+
.action(async () => {
|
|
31
|
+
const token = loadToken()
|
|
32
|
+
if (!token) {
|
|
33
|
+
fail('Not authenticated. Run `syphin login` first.')
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const config = loadProjectConfig()
|
|
38
|
+
if (!config) {
|
|
39
|
+
fail('No project config found. Run `syphin init` in your project directory.')
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
info(`Project: ${teal(config.projectSlug)}`)
|
|
44
|
+
info(`Environment: ${config.environment}`)
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const api = createApiClient(token)
|
|
48
|
+
const result = await api.verify()
|
|
49
|
+
info(`Organization: ${teal(result.orgName)} (${result.orgSlug})`)
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const manifest = await api.getManifest(config.projectSlug, config.environment)
|
|
53
|
+
info(`Always-on: ${teal(manifest.alwaysOn.length.toString())} skills (~${manifest.totalTokens} tokens)`)
|
|
54
|
+
info(`Available: ${teal(manifest.available.length.toString())} skills`)
|
|
55
|
+
|
|
56
|
+
if (manifest.alwaysOn.length > 0) {
|
|
57
|
+
console.log('')
|
|
58
|
+
info(dim('Always-on skills:'))
|
|
59
|
+
for (const s of manifest.alwaysOn) {
|
|
60
|
+
info(` ${s.name} ${dim(`[${s.type}]`)} ~${s.tokenCount} tokens`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
info('Manifest not available — project may need skills bound.')
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
fail(`Connection error: ${err.message}`)
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
program.parse()
|
package/src/lib/api.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API client for CLI.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const DEFAULT_API_URL = 'https://syphin.vercel.app'
|
|
6
|
+
|
|
7
|
+
export function createApiClient(token, apiUrl) {
|
|
8
|
+
const baseUrl = (apiUrl || process.env.SYPHIN_API_URL || DEFAULT_API_URL).replace(/\/+$/, '')
|
|
9
|
+
|
|
10
|
+
async function request(method, path, body) {
|
|
11
|
+
const url = `${baseUrl}${path}`
|
|
12
|
+
const opts = {
|
|
13
|
+
method,
|
|
14
|
+
headers: {
|
|
15
|
+
'Authorization': `Bearer ${token}`,
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
if (body) opts.body = JSON.stringify(body)
|
|
20
|
+
|
|
21
|
+
const res = await fetch(url, opts)
|
|
22
|
+
const json = await res.json()
|
|
23
|
+
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
throw new Error(json.error || `API error ${res.status}`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return json.data
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
verify: () => request('POST', '/api/auth/verify'),
|
|
33
|
+
getManifest: (slug, env) => request('GET', `/api/projects/${slug}/manifest?env=${env}`),
|
|
34
|
+
createProject: (name, slug) => request('POST', '/api/projects', { name, slug }),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config management — credentials and project config.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs'
|
|
6
|
+
import { join, dirname } from 'path'
|
|
7
|
+
import { homedir } from 'os'
|
|
8
|
+
|
|
9
|
+
const CONFIG_DIR = join(homedir(), '.config', 'syphin')
|
|
10
|
+
const CREDS_FILE = join(CONFIG_DIR, 'credentials.json')
|
|
11
|
+
|
|
12
|
+
function ensureDir(dir) {
|
|
13
|
+
if (!existsSync(dir)) {
|
|
14
|
+
mkdirSync(dir, { recursive: true })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function saveToken(token) {
|
|
19
|
+
ensureDir(CONFIG_DIR)
|
|
20
|
+
writeFileSync(CREDS_FILE, JSON.stringify({ token }, null, 2))
|
|
21
|
+
try {
|
|
22
|
+
chmodSync(CREDS_FILE, 0o600)
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadToken() {
|
|
27
|
+
if (process.env.SYPHIN_TOKEN) return process.env.SYPHIN_TOKEN
|
|
28
|
+
|
|
29
|
+
if (existsSync(CREDS_FILE)) {
|
|
30
|
+
try {
|
|
31
|
+
const raw = readFileSync(CREDS_FILE, 'utf-8')
|
|
32
|
+
return JSON.parse(raw).token || null
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function clearToken() {
|
|
41
|
+
if (existsSync(CREDS_FILE)) {
|
|
42
|
+
writeFileSync(CREDS_FILE, '{}')
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function loadProjectConfig() {
|
|
47
|
+
const configPath = join(process.cwd(), '.syphin', 'config.json')
|
|
48
|
+
if (!existsSync(configPath)) return null
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
51
|
+
} catch {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function saveProjectConfig(config) {
|
|
57
|
+
const dir = join(process.cwd(), '.syphin')
|
|
58
|
+
ensureDir(dir)
|
|
59
|
+
writeFileSync(join(dir, 'config.json'), JSON.stringify(config, null, 2))
|
|
60
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styled terminal output helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
|
|
7
|
+
export const teal = chalk.hex('#00d4c8')
|
|
8
|
+
export const dim = chalk.dim
|
|
9
|
+
export const bold = chalk.bold
|
|
10
|
+
export const red = chalk.red
|
|
11
|
+
export const green = chalk.green
|
|
12
|
+
export const yellow = chalk.yellow
|
|
13
|
+
|
|
14
|
+
export function success(msg) {
|
|
15
|
+
console.log(green(' ✓ ') + msg)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function fail(msg) {
|
|
19
|
+
console.log(red(' ✗ ') + msg)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function info(msg) {
|
|
23
|
+
console.log(dim(' → ') + msg)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function heading(msg) {
|
|
27
|
+
console.log('\n' + bold(msg))
|
|
28
|
+
}
|