@oyster-lib/cli 2.0.0 → 2.0.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/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@oyster-lib/cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "oyster": "dist/index.js"
7
7
  },
8
+ "files": [
9
+ "dist/",
10
+ "README.md"
11
+ ],
8
12
  "publishConfig": {
9
13
  "access": "public"
10
14
  },
package/jest.config.ts DELETED
@@ -1,23 +0,0 @@
1
- import type { Config } from 'jest'
2
-
3
- const config: Config = {
4
- preset: 'ts-jest/presets/default-esm',
5
- testEnvironment: 'node',
6
- roots: ['<rootDir>/src'],
7
- testMatch: ['**/__tests__/**/*.test.ts'],
8
- moduleNameMapper: {
9
- '^(\\.{1,2}/.*)\\.js$': '$1'
10
- },
11
- transform: {
12
- '^.+\\.tsx?$': [
13
- 'ts-jest',
14
- {
15
- useESM: true,
16
- tsconfig: 'tsconfig.json'
17
- }
18
- ]
19
- },
20
- extensionsToTreatAsEsm: ['.ts']
21
- }
22
-
23
- export default config
@@ -1,283 +0,0 @@
1
- import fs from 'fs'
2
- import os from 'os'
3
- import path from 'path'
4
- import { generateFrontendFiles } from '../templates/frontend.js'
5
- import { generateBackendFiles } from '../templates/backend.js'
6
- import { generateAllFiles, generateAppFiles, writeFiles } from '../templates/index.js'
7
-
8
- // テスト用一時ディレクトリ
9
- let tmpDir: string
10
-
11
- beforeEach(() => {
12
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'oyster-cli-test-'))
13
- })
14
-
15
- afterEach(() => {
16
- fs.rmSync(tmpDir, { recursive: true, force: true })
17
- })
18
-
19
- // ============================================================
20
- // 1. フロントエンドテンプレート生成(正常系)
21
- // ============================================================
22
- describe('generateFrontendFiles', () => {
23
- // FE-N-01: 正しいファイルパスのマップが返る
24
- test('FE-N-01: returns correct file path map', () => {
25
- const files = generateFrontendFiles('example.com', 'myapp')
26
- const paths = Object.keys(files)
27
-
28
- expect(paths).toContain('frontend/example.com/src/apps/myapp/index.ts')
29
- expect(paths).toContain('frontend/example.com/src/shared/index.ts')
30
- expect(paths).toContain('frontend/example.com/src/index.css')
31
- expect(paths).toContain('frontend/example.com/package.json')
32
- expect(paths).toContain('frontend/example.com/tsconfig.json')
33
- expect(paths).toContain('frontend/example.com/tailwind.config.js')
34
- })
35
-
36
- // FE-N-02: index.ts が React コンポーネントをエクスポート
37
- test('FE-N-02: app index.ts exports a React component', () => {
38
- const files = generateFrontendFiles('example.com', 'myapp')
39
- const content = files['frontend/example.com/src/apps/myapp/index.ts']
40
-
41
- expect(content).toContain('export')
42
- expect(content).toContain('React')
43
- })
44
-
45
- // FE-N-03: shared/index.ts が空のエクスポートファイル
46
- test('FE-N-03: shared/index.ts is an empty export file', () => {
47
- const files = generateFrontendFiles('example.com', 'myapp')
48
- const content = files['frontend/example.com/src/shared/index.ts']
49
-
50
- expect(content).toBeDefined()
51
- expect(content.length).toBeGreaterThan(0)
52
- })
53
-
54
- // FE-N-04: index.css に @import 'tailwindcss' が含まれる
55
- test("FE-N-04: index.css contains @import 'tailwindcss'", () => {
56
- const files = generateFrontendFiles('example.com', 'myapp')
57
- const content = files['frontend/example.com/src/index.css']
58
-
59
- expect(content).toContain("@import 'tailwindcss'")
60
- })
61
-
62
- // FE-N-05: package.json が正しい JSON で react, react-dom を含む
63
- test('FE-N-05: package.json is valid JSON with react dependencies', () => {
64
- const files = generateFrontendFiles('example.com', 'myapp')
65
- const content = files['frontend/example.com/package.json']
66
-
67
- const pkg = JSON.parse(content)
68
- expect(pkg.dependencies).toBeDefined()
69
- expect(pkg.dependencies['react']).toBeDefined()
70
- expect(pkg.dependencies['react-dom']).toBeDefined()
71
- })
72
-
73
- // FE-N-06: tsconfig.json が正しい JSON で compilerOptions.paths を含む
74
- test('FE-N-06: tsconfig.json is valid JSON with compilerOptions.paths', () => {
75
- const files = generateFrontendFiles('example.com', 'myapp')
76
- const content = files['frontend/example.com/tsconfig.json']
77
-
78
- const tsconfig = JSON.parse(content)
79
- expect(tsconfig.compilerOptions).toBeDefined()
80
- expect(tsconfig.compilerOptions.paths).toBeDefined()
81
- })
82
-
83
- // FE-N-07: tailwind.config.js が正しい内容で生成される
84
- test('FE-N-07: tailwind.config.js is generated with correct content', () => {
85
- const files = generateFrontendFiles('example.com', 'myapp')
86
- const content = files['frontend/example.com/tailwind.config.js']
87
-
88
- expect(content).toContain('module.exports')
89
- expect(content).toContain('content')
90
- })
91
- })
92
-
93
- // ============================================================
94
- // 2. バックエンドテンプレート生成(正常系)
95
- // ============================================================
96
- describe('generateBackendFiles', () => {
97
- // BE-N-01: 正しいファイルパスのマップが返る
98
- test('BE-N-01: returns correct file path map', () => {
99
- const files = generateBackendFiles('example.com')
100
- const paths = Object.keys(files)
101
-
102
- expect(paths).toContain('backend/server.ts')
103
- expect(paths).toContain('backend/package.json')
104
- expect(paths).toContain('backend/tsconfig.json')
105
- expect(paths).toContain('backend/prisma/example.com/schema.prisma')
106
- })
107
-
108
- // BE-N-02: server.ts が Express ベースのスケルトン
109
- test('BE-N-02: server.ts is an Express-based skeleton', () => {
110
- const files = generateBackendFiles('example.com')
111
- const content = files['backend/server.ts']
112
-
113
- expect(content).toContain('express')
114
- expect(content).toContain('listen')
115
- })
116
-
117
- // BE-N-03: package.json が正しい JSON で必要な依存を含む
118
- test('BE-N-03: package.json has required dependencies', () => {
119
- const files = generateBackendFiles('example.com')
120
- const content = files['backend/package.json']
121
-
122
- const pkg = JSON.parse(content)
123
- expect(pkg.dependencies).toBeDefined()
124
- expect(pkg.dependencies['express']).toBeDefined()
125
- expect(pkg.devDependencies).toBeDefined()
126
- expect(pkg.devDependencies['typescript']).toBeDefined()
127
- expect(pkg.devDependencies['ts-node']).toBeDefined()
128
- })
129
-
130
- // BE-N-04: tsconfig.json が正しい JSON
131
- test('BE-N-04: tsconfig.json is valid JSON', () => {
132
- const files = generateBackendFiles('example.com')
133
- const content = files['backend/tsconfig.json']
134
-
135
- const tsconfig = JSON.parse(content)
136
- expect(tsconfig.compilerOptions).toBeDefined()
137
- })
138
-
139
- // BE-N-05: schema.prisma が正しいフォーマット
140
- test('BE-N-05: schema.prisma has correct format', () => {
141
- const files = generateBackendFiles('example.com')
142
- const content = files['backend/prisma/example.com/schema.prisma']
143
-
144
- expect(content).toContain('generator client')
145
- expect(content).toContain('datasource db')
146
- })
147
- })
148
-
149
- // ============================================================
150
- // 3. 共通ファイル生成(正常系)
151
- // ============================================================
152
- describe('generateAllFiles - common files', () => {
153
- // CM-N-01: .gitignore が正しい内容
154
- test('CM-N-01: .gitignore includes node_modules, .build, .env, dist', () => {
155
- const files = generateAllFiles('example.com', 'myapp')
156
-
157
- expect(files['.gitignore']).toContain('node_modules')
158
- expect(files['.gitignore']).toContain('.build')
159
- expect(files['.gitignore']).toContain('.env')
160
- expect(files['.gitignore']).toContain('dist')
161
- })
162
-
163
- // CM-N-02: README.md がドメイン名とアプリ名を含む
164
- test('CM-N-02: README.md includes domain and app name', () => {
165
- const files = generateAllFiles('example.com', 'myapp')
166
-
167
- expect(files['README.md']).toContain('example.com')
168
- expect(files['README.md']).toContain('myapp')
169
- })
170
- })
171
-
172
- // ============================================================
173
- // 4. テンプレート生成(異常系・境界値)
174
- // ============================================================
175
- describe('template generation - edge cases', () => {
176
- // TE-E-01: ドメインに特殊文字を含む場合
177
- test('TE-E-01: domain with dots and hyphens generates correct paths', () => {
178
- const files = generateFrontendFiles('my-site.example.com', 'myapp')
179
- const paths = Object.keys(files)
180
-
181
- expect(paths).toContain('frontend/my-site.example.com/src/apps/myapp/index.ts')
182
- expect(paths).toContain('frontend/my-site.example.com/package.json')
183
- })
184
-
185
- // TE-E-02: appName が空文字の場合エラー
186
- test('TE-E-02: empty appName throws error', () => {
187
- expect(() => generateFrontendFiles('example.com', '')).toThrow()
188
- })
189
-
190
- // TE-E-03: domain が空文字の場合エラー
191
- test('TE-E-03: empty domain throws error', () => {
192
- expect(() => generateFrontendFiles('', 'myapp')).toThrow()
193
- })
194
- })
195
-
196
- // ============================================================
197
- // 5. 2つ目以降のアプリ追加(正常系)
198
- // ============================================================
199
- describe('generateAppFiles - additional app', () => {
200
- // AD-N-01: 新しいアプリ用の index.ts のみ返る
201
- test('AD-N-01: returns only new app index.ts', () => {
202
- const files = generateAppFiles('example.com', 'secondapp')
203
- const paths = Object.keys(files)
204
-
205
- expect(paths).toContain('frontend/example.com/src/apps/secondapp/index.ts')
206
- // 1ファイルのみ
207
- expect(paths.length).toBe(1)
208
- })
209
-
210
- // AD-N-02: 既存の package.json 等のパスは含まれない
211
- test('AD-N-02: does not include existing config files', () => {
212
- const files = generateAppFiles('example.com', 'secondapp')
213
- const paths = Object.keys(files)
214
-
215
- expect(paths).not.toContain('frontend/example.com/package.json')
216
- expect(paths).not.toContain('frontend/example.com/tsconfig.json')
217
- expect(paths).not.toContain('backend/server.ts')
218
- })
219
- })
220
-
221
- // ============================================================
222
- // 6. ファイル書き込みロジック
223
- // ============================================================
224
- describe('writeFiles', () => {
225
- // WR-N-01: 空のディレクトリにすべてのファイルが書き込まれる
226
- test('WR-N-01: writes all files to empty directory', () => {
227
- const files: Record<string, string> = {
228
- 'frontend/example.com/src/apps/myapp/index.ts': 'export default {}',
229
- 'backend/server.ts': 'console.log("hello")'
230
- }
231
-
232
- const result = writeFiles(tmpDir, files, false)
233
-
234
- expect(result.created).toContain('frontend/example.com/src/apps/myapp/index.ts')
235
- expect(result.created).toContain('backend/server.ts')
236
- expect(fs.existsSync(path.join(tmpDir, 'frontend/example.com/src/apps/myapp/index.ts'))).toBe(true)
237
- expect(fs.existsSync(path.join(tmpDir, 'backend/server.ts'))).toBe(true)
238
- })
239
-
240
- // WR-N-02: 既存ファイルがある場合、force=false ではスキップ
241
- test('WR-N-02: skips existing files when force=false', () => {
242
- // 事前にファイルを作成
243
- const filePath = path.join(tmpDir, 'backend/server.ts')
244
- fs.mkdirSync(path.dirname(filePath), { recursive: true })
245
- fs.writeFileSync(filePath, 'original content')
246
-
247
- const files: Record<string, string> = {
248
- 'backend/server.ts': 'new content'
249
- }
250
-
251
- const result = writeFiles(tmpDir, files, false)
252
-
253
- expect(result.skipped).toContain('backend/server.ts')
254
- expect(fs.readFileSync(filePath, 'utf-8')).toBe('original content')
255
- })
256
-
257
- // WR-N-03: 既存ファイルがある場合、force=true では上書き
258
- test('WR-N-03: overwrites existing files when force=true', () => {
259
- const filePath = path.join(tmpDir, 'backend/server.ts')
260
- fs.mkdirSync(path.dirname(filePath), { recursive: true })
261
- fs.writeFileSync(filePath, 'original content')
262
-
263
- const files: Record<string, string> = {
264
- 'backend/server.ts': 'new content'
265
- }
266
-
267
- const result = writeFiles(tmpDir, files, true)
268
-
269
- expect(result.created).toContain('backend/server.ts')
270
- expect(fs.readFileSync(filePath, 'utf-8')).toBe('new content')
271
- })
272
-
273
- // WR-N-04: ディレクトリが存在しない場合、再帰的に作成
274
- test('WR-N-04: creates directories recursively', () => {
275
- const files: Record<string, string> = {
276
- 'deep/nested/dir/file.ts': 'content'
277
- }
278
-
279
- writeFiles(tmpDir, files, false)
280
-
281
- expect(fs.existsSync(path.join(tmpDir, 'deep/nested/dir/file.ts'))).toBe(true)
282
- })
283
- })
@@ -1,109 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import ora from 'ora'
4
- import FormData from 'form-data'
5
- import { loadConfig, requireLogin } from '../config.js'
6
- import { apiPostStream } from '../utils/http.js'
7
- import { chooseDomainAsync, chooseAppAsync } from '../utils/input.js'
8
-
9
- export function setDeploy(program: Command) {
10
- program
11
- .command('deploy')
12
- .description('アプリケーションをデプロイする')
13
- .option('-f, --frontend', 'フロントエンドのみデプロイ')
14
- .option('-b, --backend', 'バックエンドのみデプロイ')
15
- .option('-s, --staging', 'ステージング環境にデプロイ')
16
- .option('-d, --domain <name>', 'ドメイン名を直接指定')
17
- .option('-a, --app <name>', 'アプリ名を直接指定')
18
- .action(async (options) => {
19
- try {
20
- requireLogin()
21
- const config = loadConfig()
22
-
23
- // --- ドメイン選択 ---
24
- let domainName = options.domain
25
- if (!domainName) {
26
- const domain = await chooseDomainAsync()
27
- if (!domain) {
28
- console.log(chalk.yellow('キャンセルしました'))
29
- return
30
- }
31
- domainName = domain.name
32
- }
33
-
34
- // --- アプリ選択 ---
35
- let appName = options.app
36
- if (!appName) {
37
- const app = await chooseAppAsync(domainName)
38
- if (!app) {
39
- console.log(chalk.yellow('キャンセルしました'))
40
- return
41
- }
42
- appName = app.name
43
- }
44
-
45
- // --- デプロイ対象の決定 ---
46
- const specifiedOperations: string[] = []
47
- if (options.frontend) specifiedOperations.push('frontend')
48
- if (options.backend) specifiedOperations.push('backend')
49
- // --frontend も --backend も指定しなければ両方
50
- const targets =
51
- specifiedOperations.length > 0
52
- ? specifiedOperations.join(' + ')
53
- : 'frontend + backend'
54
-
55
- const isStaging: boolean = options.staging ?? false
56
- const stage = isStaging ? 'staging' : 'release'
57
-
58
- console.log()
59
- console.log(chalk.cyan('デプロイ設定:'))
60
- console.log(chalk.gray(` ドメイン : ${domainName}`))
61
- console.log(chalk.gray(` アプリ : ${appName}`))
62
- console.log(chalk.gray(` 環境 : ${stage}`))
63
- console.log(chalk.gray(` 対象 : ${targets}`))
64
- console.log()
65
-
66
- // --- FormData 構築 ---
67
- const formData = new FormData()
68
- const data: Record<string, any> = {
69
- domain: domainName,
70
- appName,
71
- email: config.user?.email ?? '',
72
- id: config.user?.id ?? '',
73
- user: config.user?.name ?? '',
74
- isStaging
75
- }
76
- if (specifiedOperations.length > 0) {
77
- data.specifiedOperations = specifiedOperations
78
- }
79
- formData.append('data', JSON.stringify(data))
80
-
81
- // --- デプロイ実行(SSEストリーム受信) ---
82
- const spinner = ora(`デプロイ中... (${targets})`).start()
83
- try {
84
- await apiPostStream('deploy', formData, (event) => {
85
- if (event.type === 'complete') {
86
- // 完了イベントは apiPostStream が正常終了して処理される
87
- return
88
- }
89
- // 進捗メッセージでスピナーテキストを更新
90
- if (event.message) {
91
- const prefix = event.step === 'frontend' ? '[Frontend] '
92
- : event.step === 'backend' ? '[Backend] '
93
- : ''
94
- spinner.text = `${prefix}${event.message}`
95
- }
96
- }, { timeout: 600_000 })
97
- spinner.succeed(chalk.green('デプロイが完了しました'))
98
- } catch (error: any) {
99
- spinner.fail(chalk.red('デプロイに失敗しました'))
100
- const msg = error.message ?? 'Unknown error'
101
- console.error(chalk.red(` ${msg}`))
102
- process.exit(1)
103
- }
104
- } catch (error: any) {
105
- console.error(chalk.red(`✗ ${error.message}`))
106
- process.exit(1)
107
- }
108
- })
109
- }
@@ -1,154 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import prompts from 'prompts'
4
- import { execSync } from 'child_process'
5
- import { requireLogin } from '../config.js'
6
- import { generateAllFiles, generateAppFiles, writeFiles } from '../templates/index.js'
7
-
8
- export function setInit(program: Command) {
9
- program
10
- .command('init')
11
- .description('リポジトリを Oyster アプリ構成で初期化する')
12
- .option('-d, --domain <name>', 'ドメイン名を指定')
13
- .option('-a, --app <name>', 'アプリ名を指定')
14
- .option('--force', '既存ファイルを上書きする', false)
15
- .option('--add-app', '既存リポジトリに新しいアプリを追加する', false)
16
- .action(async (options) => {
17
- try {
18
- // ログイン確認
19
- requireLogin()
20
-
21
- // git リポジトリかチェック
22
- try {
23
- execSync('git rev-parse --is-inside-work-tree', {
24
- stdio: ['pipe', 'pipe', 'pipe']
25
- })
26
- } catch {
27
- console.error(
28
- chalk.red('✗ カレントディレクトリは git リポジトリではありません。')
29
- )
30
- console.error(
31
- chalk.gray(' 先に `git init` を実行してください。')
32
- )
33
- process.exit(1)
34
- }
35
-
36
- // ドメイン名の取得
37
- let domainName = options.domain
38
- if (!domainName) {
39
- const { value } = await prompts({
40
- type: 'text',
41
- name: 'value',
42
- message: 'ドメイン名:',
43
- validate: (v: string) =>
44
- v.length > 0 ? true : 'ドメイン名は必須です'
45
- })
46
- if (!value) {
47
- console.log(chalk.yellow('キャンセルしました'))
48
- return
49
- }
50
- domainName = value
51
- }
52
-
53
- // アプリ名の取得
54
- let appName = options.app
55
- if (!appName) {
56
- const { value } = await prompts({
57
- type: 'text',
58
- name: 'value',
59
- message: 'アプリ名:',
60
- validate: (v: string) =>
61
- v.length > 0 ? true : 'アプリ名は必須です'
62
- })
63
- if (!value) {
64
- console.log(chalk.yellow('キャンセルしました'))
65
- return
66
- }
67
- appName = value
68
- }
69
-
70
- // ファイル生成
71
- const isAddApp: boolean = options.addApp ?? false
72
- const files = isAddApp
73
- ? generateAppFiles(domainName, appName)
74
- : generateAllFiles(domainName, appName)
75
-
76
- const cwd = process.cwd()
77
- const force: boolean = options.force ?? false
78
-
79
- console.log()
80
- console.log(chalk.cyan('初期化設定:'))
81
- console.log(chalk.gray(` ドメイン : ${domainName}`))
82
- console.log(chalk.gray(` アプリ : ${appName}`))
83
- console.log(
84
- chalk.gray(` モード : ${isAddApp ? 'アプリ追加' : '初回初期化'}`)
85
- )
86
- console.log(chalk.gray(` 上書き : ${force ? 'はい' : 'いいえ'}`))
87
- console.log()
88
-
89
- // ファイル書き込み
90
- const result = writeFiles(cwd, files, force)
91
-
92
- // 結果表示
93
- if (result.created.length > 0) {
94
- console.log(chalk.green('作成されたファイル:'))
95
- for (const f of result.created) {
96
- console.log(chalk.green(` + ${f}`))
97
- }
98
- }
99
-
100
- if (result.skipped.length > 0) {
101
- console.log()
102
- console.log(chalk.yellow('スキップされたファイル(既存):'))
103
- for (const f of result.skipped) {
104
- console.log(chalk.yellow(` ~ ${f}`))
105
- }
106
- console.log(
107
- chalk.gray(
108
- ' 上書きするには --force オプションを使用してください'
109
- )
110
- )
111
- }
112
-
113
- // git add
114
- if (result.created.length > 0) {
115
- console.log()
116
- try {
117
- // 各ファイルを個別に git add(shell injection 防御)
118
- for (const f of result.created) {
119
- execSync('git add -- ' + JSON.stringify(f), {
120
- stdio: ['pipe', 'pipe', 'pipe']
121
- })
122
- }
123
- console.log(
124
- chalk.green(
125
- `✓ ${result.created.length} ファイルを git に追加しました`
126
- )
127
- )
128
- } catch {
129
- console.log(
130
- chalk.yellow(
131
- '⚠ git add に失敗しました。手動で追加してください。'
132
- )
133
- )
134
- }
135
- console.log()
136
- console.log(chalk.cyan('次のステップ:'))
137
- console.log(
138
- chalk.gray(
139
- ` git commit -m "Initialize oyster app: ${appName}"`
140
- )
141
- )
142
- console.log(chalk.gray(' git push'))
143
- console.log(
144
- chalk.gray(
145
- ` oyster deploy -d ${domainName} -a ${appName}`
146
- )
147
- )
148
- }
149
- } catch (error: any) {
150
- console.error(chalk.red(`✗ ${error.message}`))
151
- process.exit(1)
152
- }
153
- })
154
- }
@@ -1,78 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import prompts from 'prompts'
4
- import axios from 'axios'
5
- import { getServerUrl, loadConfig, saveConfig } from '../config.js'
6
-
7
- export function setLogin(program: Command) {
8
- program
9
- .command('login')
10
- .description('Oyster にログインして API キーを取得する')
11
- .option('-s, --server <url>', 'サーバー URL を指定')
12
- .action(async (options) => {
13
- try {
14
- // サーバー URL の設定(オプションで指定された場合は保存)
15
- if (options.server) {
16
- const config = loadConfig()
17
- config.server_url = options.server
18
- saveConfig(config)
19
- }
20
-
21
- const serverUrl = options.server ?? getServerUrl()
22
- console.log(chalk.cyan(`サーバー: ${serverUrl}`))
23
- console.log()
24
-
25
- // メールアドレス入力
26
- const { email } = await prompts({
27
- type: 'text',
28
- name: 'email',
29
- message: 'メールアドレス:'
30
- })
31
- if (!email) {
32
- console.log(chalk.yellow('キャンセルしました'))
33
- return
34
- }
35
-
36
- // パスワード入力
37
- const { password } = await prompts({
38
- type: 'password',
39
- name: 'password',
40
- message: 'パスワード:'
41
- })
42
- if (!password) {
43
- console.log(chalk.yellow('キャンセルしました'))
44
- return
45
- }
46
-
47
- // ログインリクエスト
48
- const loginUrl = `${serverUrl}/api/v1/cli/login`
49
- const response = await axios.post(loginUrl, { email, password })
50
- const { api_key, user } = response.data
51
-
52
- // 設定ファイルに保存
53
- const config = loadConfig()
54
- config.api_key = api_key
55
- config.server_url = serverUrl
56
- config.user = user
57
- saveConfig(config)
58
-
59
- console.log()
60
- console.log(chalk.green('✓ ログインに成功しました'))
61
- console.log(chalk.gray(` ユーザー: ${user.name} (${user.email})`))
62
- console.log(chalk.gray(` 設定は ~/.config/oyster/config.json に保存されました`))
63
- } catch (error: any) {
64
- if (axios.isAxiosError(error) && error.response) {
65
- const status = error.response.status
66
- const message = error.response.data?.message ?? 'Unknown error'
67
- if (status === 401) {
68
- console.error(chalk.red('✗ メールアドレスまたはパスワードが正しくありません'))
69
- } else {
70
- console.error(chalk.red(`✗ ログイン失敗: ${message} (HTTP ${status})`))
71
- }
72
- } else {
73
- console.error(chalk.red(`✗ ログイン失敗: ${error.message}`))
74
- }
75
- process.exit(1)
76
- }
77
- })
78
- }