saeeol 1.0.2 → 1.0.4
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/cli/cmd/init.ts +189 -207
- package/src/index.ts +2 -0
package/package.json
CHANGED
package/src/cli/cmd/init.ts
CHANGED
|
@@ -5,141 +5,43 @@ import * as prompts from "@clack/prompts"
|
|
|
5
5
|
import { UI } from "../ui"
|
|
6
6
|
import { Global } from "@saeeol/core/global"
|
|
7
7
|
import { Instance } from "../../project/instance"
|
|
8
|
-
import { Auth } from "../../auth"
|
|
9
8
|
import { AppRuntime } from "../../effect/app-runtime"
|
|
10
9
|
import { Config } from "@/config/config"
|
|
11
10
|
import { Effect } from "effect"
|
|
12
11
|
import { put } from "./providers-auth"
|
|
12
|
+
import { ModelsDev } from "@/provider/models"
|
|
13
|
+
import { map, pipe, sortBy, values } from "remeda"
|
|
13
14
|
|
|
14
|
-
const { TEXT_HIGHLIGHT: H, TEXT_NORMAL: N, TEXT_DIM: D, TEXT_SUCCESS: S, TEXT_NORMAL_BOLD: B
|
|
15
|
+
const { TEXT_HIGHLIGHT: H, TEXT_NORMAL: N, TEXT_DIM: D, TEXT_SUCCESS: S, TEXT_NORMAL_BOLD: B } = UI.Style
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
label: string
|
|
19
|
-
provider: string
|
|
20
|
-
envVar: string
|
|
21
|
-
models: Array<{ id: string; label: string }>
|
|
22
|
-
hint: string
|
|
23
|
-
defaultModel?: string
|
|
24
|
-
}
|
|
17
|
+
const getModels = () => AppRuntime.runPromise(ModelsDev.Service.use((s) => s.get()))
|
|
18
|
+
const refreshModels = () => AppRuntime.runPromise(ModelsDev.Service.use((s) => s.refresh(true)))
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
id: "anthropic",
|
|
29
|
-
label: "Anthropic (Claude)",
|
|
30
|
-
provider: "anthropic",
|
|
31
|
-
envVar: "ANTHROPIC_API_KEY",
|
|
32
|
-
models: [
|
|
33
|
-
{ id: "anthropic/claude-sonnet-4", label: "Claude Sonnet 4 (추천)" },
|
|
34
|
-
{ id: "anthropic/claude-haiku-4-5", label: "Claude Haiku 4.5 (빠름/저렴)" },
|
|
35
|
-
{ id: "anthropic/claude-opus-4", label: "Claude Opus 4 (강력)" },
|
|
36
|
-
],
|
|
37
|
-
hint: "https://console.anthropic.com/settings/keys",
|
|
38
|
-
defaultModel: "anthropic/claude-sonnet-4",
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
id: "openai",
|
|
42
|
-
label: "OpenAI (GPT)",
|
|
43
|
-
provider: "openai",
|
|
44
|
-
envVar: "OPENAI_API_KEY",
|
|
45
|
-
models: [
|
|
46
|
-
{ id: "openai/gpt-5", label: "GPT-5" },
|
|
47
|
-
{ id: "openai/gpt-5-mini", label: "GPT-5 Mini (빠름)" },
|
|
48
|
-
{ id: "openai/o3", label: "o3 (추론)" },
|
|
49
|
-
],
|
|
50
|
-
hint: "https://platform.openai.com/api-keys",
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
id: "google",
|
|
54
|
-
label: "Google (Gemini)",
|
|
55
|
-
provider: "google",
|
|
56
|
-
envVar: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
57
|
-
models: [
|
|
58
|
-
{ id: "google/gemini-2.5-pro", label: "Gemini 2.5 Pro" },
|
|
59
|
-
{ id: "google/gemini-2.5-flash", label: "Gemini 2.5 Flash (빠름)" },
|
|
60
|
-
],
|
|
61
|
-
hint: "https://aistudio.google.com/apikey",
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
id: "openrouter",
|
|
65
|
-
label: "OpenRouter (멀티 provider)",
|
|
66
|
-
provider: "openrouter",
|
|
67
|
-
envVar: "OPENROUTER_API_KEY",
|
|
68
|
-
models: [
|
|
69
|
-
{ id: "openrouter/anthropic/claude-sonnet-4", label: "Claude Sonnet 4 via OpenRouter" },
|
|
70
|
-
{ id: "openrouter/openai/gpt-5", label: "GPT-5 via OpenRouter" },
|
|
71
|
-
{ id: "openrouter/google/gemini-2.5-pro", label: "Gemini 2.5 Pro via OpenRouter" },
|
|
72
|
-
],
|
|
73
|
-
hint: "https://openrouter.ai/keys",
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
id: "groq",
|
|
77
|
-
label: "Groq (초고속 추론)",
|
|
78
|
-
provider: "groq",
|
|
79
|
-
envVar: "GROQ_API_KEY",
|
|
80
|
-
models: [
|
|
81
|
-
{ id: "groq/llama-4-maverick-17b-128e-instruct", label: "Llama 4 Maverick" },
|
|
82
|
-
],
|
|
83
|
-
hint: "https://console.groq.com/keys",
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
id: "github-copilot",
|
|
87
|
-
label: "GitHub Copilot (무료)",
|
|
88
|
-
provider: "github-copilot",
|
|
89
|
-
envVar: "",
|
|
90
|
-
models: [
|
|
91
|
-
{ id: "github-copilot/gpt-5", label: "GPT-5 via Copilot" },
|
|
92
|
-
{ id: "github-copilot/claude-sonnet-4", label: "Claude Sonnet 4 via Copilot" },
|
|
93
|
-
],
|
|
94
|
-
hint: "GitHub 계정으로 OAuth 로그인",
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: "bedrock",
|
|
98
|
-
label: "Amazon Bedrock",
|
|
99
|
-
provider: "amazon-bedrock",
|
|
100
|
-
envVar: "AWS_ACCESS_KEY_ID",
|
|
101
|
-
models: [
|
|
102
|
-
{ id: "amazon-bedrock/anthropic.claude-sonnet-4-20250514", label: "Claude Sonnet 4" },
|
|
103
|
-
],
|
|
104
|
-
hint: "AWS 자격 증명 필요",
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
id: "custom",
|
|
108
|
-
label: "직접 입력 (OpenAI 호환)",
|
|
109
|
-
provider: "",
|
|
110
|
-
envVar: "",
|
|
111
|
-
models: [],
|
|
112
|
-
hint: "",
|
|
113
|
-
},
|
|
114
|
-
]
|
|
20
|
+
type ProviderInfo = { id: string; name: string; env: string[]; models: Record<string, unknown> }
|
|
115
21
|
|
|
116
22
|
export const InitCommand = cmd({
|
|
117
23
|
command: "init",
|
|
118
24
|
describe: "최초 설정 — provider, API key, 모델 선택",
|
|
119
25
|
builder: (y) =>
|
|
120
26
|
y
|
|
121
|
-
.option("
|
|
27
|
+
.option("provider", {
|
|
122
28
|
type: "string",
|
|
123
|
-
|
|
124
|
-
|
|
29
|
+
alias: "p",
|
|
30
|
+
describe: "provider id (예: anthropic, openai, google)",
|
|
125
31
|
})
|
|
126
32
|
.option("api-key", {
|
|
127
33
|
type: "string",
|
|
128
|
-
|
|
34
|
+
alias: "k",
|
|
35
|
+
describe: "API key",
|
|
129
36
|
})
|
|
130
37
|
.option("model", {
|
|
131
38
|
type: "string",
|
|
39
|
+
alias: "m",
|
|
132
40
|
describe: "기본 모델 (예: anthropic/claude-sonnet-4)",
|
|
133
41
|
})
|
|
134
42
|
.option("base-url", {
|
|
135
43
|
type: "string",
|
|
136
|
-
describe: "커스텀 API base URL (
|
|
137
|
-
})
|
|
138
|
-
.option("yes", {
|
|
139
|
-
type: "boolean",
|
|
140
|
-
alias: "y",
|
|
141
|
-
describe: "기본값으로 자동 진행",
|
|
142
|
-
default: false,
|
|
44
|
+
describe: "커스텀 API base URL (OpenAI 호환 엔드포인트용)",
|
|
143
45
|
}),
|
|
144
46
|
handler: async (args) => {
|
|
145
47
|
await Instance.provide({
|
|
@@ -151,36 +53,90 @@ export const InitCommand = cmd({
|
|
|
151
53
|
prompts.intro(`${B}SAEEOL 초기 설정${N}`)
|
|
152
54
|
UI.empty()
|
|
153
55
|
|
|
154
|
-
|
|
155
|
-
const preset = resolvePreset(args.preset)
|
|
156
|
-
const selected = preset ?? (await promptPreset())
|
|
157
|
-
if (!selected) return
|
|
56
|
+
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
|
158
57
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
58
|
+
// 1. Provider 목록 동적 로드
|
|
59
|
+
const s = prompts.spinner()
|
|
60
|
+
s.start("provider 목록 로드 중...")
|
|
61
|
+
await refreshModels().catch(() => {})
|
|
62
|
+
const database = await getModels()
|
|
63
|
+
s.stop(`${Object.keys(database).length}개 provider 로드 완료`)
|
|
64
|
+
|
|
65
|
+
// 2. Provider 선택
|
|
66
|
+
const disabled = new Set(config.disabled_providers ?? [])
|
|
67
|
+
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
|
68
|
+
const providers = Object.entries(database)
|
|
69
|
+
.filter(([key]) => (enabled ? enabled.has(key) : true) && !disabled.has(key))
|
|
70
|
+
.map(([id, info]) => ({ id, name: info.name || id, env: info.env, models: info.models || {} } as ProviderInfo))
|
|
71
|
+
|
|
72
|
+
const priority: Record<string, number> = {
|
|
73
|
+
anthropic: 0, openai: 1, google: 2, "github-copilot": 3, openrouter: 4, groq: 5,
|
|
74
|
+
"amazon-bedrock": 6, azure: 7, mistral: 8, deepinfra: 9, xai: 10, cerebras: 11,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let providerId: string
|
|
78
|
+
if (args.provider) {
|
|
79
|
+
providerId = args.provider
|
|
80
|
+
const found = providers.find((p) => p.id === providerId)
|
|
81
|
+
if (!found) {
|
|
82
|
+
// 커스텀 provider — base-url 필요
|
|
83
|
+
if (!args["base-url"]) {
|
|
84
|
+
prompts.log.warn(`"${providerId}"은(는) 알려진 provider가 아닙니다. --base-url이 필요합니다.`)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
175
87
|
}
|
|
88
|
+
} else {
|
|
89
|
+
const sorted = pipe(
|
|
90
|
+
providers,
|
|
91
|
+
sortBy((x) => priority[x.id] ?? 99, (x) => x.name),
|
|
92
|
+
)
|
|
93
|
+
const selected = await prompts.select({
|
|
94
|
+
message: "사용할 AI Provider를 선택하세요",
|
|
95
|
+
maxItems: 12,
|
|
96
|
+
options: [
|
|
97
|
+
...sorted.map((p) => ({ label: p.name, value: p.id })),
|
|
98
|
+
{ label: "직접 입력 (OpenAI 호환)", value: "__custom__" },
|
|
99
|
+
],
|
|
100
|
+
})
|
|
101
|
+
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
|
102
|
+
providerId = selected as string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 3. 커스텀 엔드포인트
|
|
106
|
+
if (providerId === "__custom__") {
|
|
107
|
+
await handleCustom(args, database)
|
|
108
|
+
return
|
|
176
109
|
}
|
|
177
110
|
|
|
178
|
-
//
|
|
179
|
-
const
|
|
111
|
+
// 4. API Key 입력
|
|
112
|
+
const providerInfo = providers.find((p) => p.id === providerId)
|
|
113
|
+
const envVar = providerInfo?.env?.[0]
|
|
114
|
+
const existingKey = envVar ? process.env[envVar] : undefined
|
|
115
|
+
|
|
116
|
+
if (existingKey) {
|
|
117
|
+
prompts.log.info(`${D}${envVar} 환경변수가 이미 설정되어 있습니다.${N}`)
|
|
118
|
+
} else if (providerId === "github-copilot") {
|
|
119
|
+
prompts.log.info("GitHub Copilot은 OAuth 로그인이 필요합니다.")
|
|
120
|
+
prompts.log.info(` ${H}saeeol auth login -p github-copilot${N} 으로 로그인하세요.`)
|
|
121
|
+
} else {
|
|
122
|
+
const hint = providerHint(providerId)
|
|
123
|
+
if (hint) prompts.log.info(`${D}API key 발급: ${hint}${N}`)
|
|
124
|
+
|
|
125
|
+
const key = args["api-key"] ?? (await prompts.password({
|
|
126
|
+
message: `${providerInfo?.name ?? providerId} API Key`,
|
|
127
|
+
validate: (x) => (x && x.length > 0 ? undefined : "필수 입력입니다"),
|
|
128
|
+
}))
|
|
129
|
+
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
|
130
|
+
await put(providerId, { type: "api", key })
|
|
131
|
+
prompts.log.success("API key 저장 완료")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 5. 모델 목록 동적 조회
|
|
135
|
+
const model = args.model ?? (await pickModel(providerId, database))
|
|
180
136
|
if (!model) return
|
|
181
137
|
|
|
182
|
-
//
|
|
183
|
-
await
|
|
138
|
+
// 6. 설정 저장
|
|
139
|
+
await saveConfigFile({ $schema: "https://app.saeeol.ai/config.json", model })
|
|
184
140
|
|
|
185
141
|
UI.empty()
|
|
186
142
|
prompts.outro(`${S}설정 완료!${N}`)
|
|
@@ -194,111 +150,137 @@ export const InitCommand = cmd({
|
|
|
194
150
|
},
|
|
195
151
|
})
|
|
196
152
|
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
"
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return lines.map((l) => `${H}${l}${N}`).join("\n")
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function resolvePreset(id?: string): Preset | undefined {
|
|
210
|
-
if (!id) return
|
|
211
|
-
return presets.find((p) => p.id === id)
|
|
212
|
-
}
|
|
153
|
+
async function pickModel(
|
|
154
|
+
providerId: string,
|
|
155
|
+
database: Record<string, any>,
|
|
156
|
+
): Promise<string | undefined> {
|
|
157
|
+
const provider = database[providerId]
|
|
158
|
+
if (!provider?.models) {
|
|
159
|
+
prompts.log.warn("해당 provider의 모델 목록을 가져올 수 없습니다.")
|
|
160
|
+
return `${providerId}/default`
|
|
161
|
+
}
|
|
213
162
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
})),
|
|
222
|
-
})
|
|
223
|
-
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
|
224
|
-
return presets.find((p) => p.id === selected)
|
|
225
|
-
}
|
|
163
|
+
const models = Object.entries(provider.models as Record<string, any>)
|
|
164
|
+
.filter(([, m]) => m.status !== "deprecated" && m.status !== "alpha")
|
|
165
|
+
.map(([id, m]) => ({
|
|
166
|
+
id: `${providerId}/${id}`,
|
|
167
|
+
name: m.name || id,
|
|
168
|
+
context: m.limit?.context,
|
|
169
|
+
}))
|
|
226
170
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
validate: (x) => (x && x.length > 0 ? undefined : "필수 입력입니다"),
|
|
232
|
-
})
|
|
233
|
-
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
|
234
|
-
return key
|
|
235
|
-
}
|
|
171
|
+
if (models.length === 0) {
|
|
172
|
+
prompts.log.warn("사용 가능한 모델이 없습니다.")
|
|
173
|
+
return `${providerId}/default`
|
|
174
|
+
}
|
|
236
175
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (preset.models.length === 1 && preset.defaultModel) return preset.defaultModel
|
|
176
|
+
// 컨텍스트 길이 기준 내림차순 정렬 (큰 게 보통 더 강력)
|
|
177
|
+
models.sort((a, b) => (b.context ?? 0) - (a.context ?? 0))
|
|
240
178
|
|
|
241
179
|
const selected = await prompts.select({
|
|
242
180
|
message: "기본 모델을 선택하세요",
|
|
243
|
-
|
|
244
|
-
|
|
181
|
+
maxItems: 10,
|
|
182
|
+
options: models.map((m) => ({
|
|
183
|
+
label: m.name,
|
|
245
184
|
value: m.id,
|
|
185
|
+
hint: m.context ? `${(m.context / 1000).toFixed(0)}K ctx` : undefined,
|
|
246
186
|
})),
|
|
247
187
|
})
|
|
248
188
|
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
|
249
189
|
return selected as string
|
|
250
190
|
}
|
|
251
191
|
|
|
252
|
-
async function handleCustom(args: Record<string, unknown>) {
|
|
192
|
+
async function handleCustom(args: Record<string, unknown>, database: Record<string, any>) {
|
|
253
193
|
const baseUrl = (args["base-url"] as string) ?? (await prompts.text({
|
|
254
|
-
message: "API Base URL
|
|
194
|
+
message: "API Base URL",
|
|
255
195
|
placeholder: "https://api.example.com/v1",
|
|
256
|
-
validate: (x) => (x && x.startsWith("http") ? undefined : "http
|
|
196
|
+
validate: (x) => (x && x.startsWith("http") ? undefined : "http(s)://로 시작해야 합니다"),
|
|
257
197
|
}))
|
|
258
198
|
if (prompts.isCancel(baseUrl)) throw new UI.CancelledError()
|
|
259
199
|
|
|
260
200
|
const key = (args["api-key"] as string) ?? (await prompts.password({
|
|
261
|
-
message: "API Key
|
|
201
|
+
message: "API Key",
|
|
262
202
|
validate: (x) => (x && x.length > 0 ? undefined : "필수 입력입니다"),
|
|
263
203
|
}))
|
|
264
204
|
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
|
265
205
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
206
|
+
// /v1/models 호출로 동적 모델 조회
|
|
207
|
+
let modelId: string | undefined
|
|
208
|
+
try {
|
|
209
|
+
const resp = await fetch(`${baseUrl.replace(/\/+$/, "")}/models`, {
|
|
210
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
211
|
+
signal: AbortSignal.timeout(10000),
|
|
212
|
+
})
|
|
213
|
+
if (resp.ok) {
|
|
214
|
+
const body = (await resp.json()) as { data?: Array<{ id: string }> }
|
|
215
|
+
const remoteModels = body.data ?? []
|
|
216
|
+
if (remoteModels.length > 0) {
|
|
217
|
+
const selected = await prompts.select({
|
|
218
|
+
message: `${remoteModels.length}개 모델 발견. 기본 모델 선택`,
|
|
219
|
+
maxItems: 10,
|
|
220
|
+
options: remoteModels.slice(0, 50).map((m) => ({ label: m.id, value: m.id })),
|
|
221
|
+
})
|
|
222
|
+
if (!prompts.isCancel(selected)) modelId = selected as string
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// /models 엔드포인트 없으면 수동 입력
|
|
227
|
+
}
|
|
272
228
|
|
|
273
|
-
|
|
274
|
-
|
|
229
|
+
if (!modelId) {
|
|
230
|
+
modelId = (args.model as string) ?? (await prompts.text({
|
|
231
|
+
message: "모델 ID",
|
|
232
|
+
placeholder: "gpt-4o",
|
|
233
|
+
validate: (x) => (x && x.length > 0 ? undefined : "필수 입력입니다"),
|
|
234
|
+
})) as string
|
|
235
|
+
if (prompts.isCancel(modelId)) throw new UI.CancelledError()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const pid = `custom-${Date.now().toString(36)}`
|
|
239
|
+
await put(pid, { type: "api", key })
|
|
275
240
|
|
|
276
|
-
|
|
277
|
-
|
|
241
|
+
await saveConfigFile({
|
|
242
|
+
$schema: "https://app.saeeol.ai/config.json",
|
|
243
|
+
model: `${pid}/${modelId}`,
|
|
244
|
+
provider: {
|
|
245
|
+
[pid]: {
|
|
246
|
+
name: "Custom",
|
|
247
|
+
npm: "@ai-sdk/openai-compatible",
|
|
248
|
+
api: baseUrl,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
})
|
|
278
252
|
|
|
279
253
|
UI.empty()
|
|
280
254
|
prompts.outro(`${S}설정 완료!${N}`)
|
|
281
255
|
}
|
|
282
256
|
|
|
283
|
-
function
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
257
|
+
function providerHint(id: string): string | undefined {
|
|
258
|
+
const hints: Record<string, string> = {
|
|
259
|
+
anthropic: "https://console.anthropic.com/settings/keys",
|
|
260
|
+
openai: "https://platform.openai.com/api-keys",
|
|
261
|
+
google: "https://aistudio.google.com/apikey",
|
|
262
|
+
openrouter: "https://openrouter.ai/keys",
|
|
263
|
+
groq: "https://console.groq.com/keys",
|
|
264
|
+
"amazon-bedrock": "AWS 자격 증명 필요",
|
|
265
|
+
azure: "Azure OpenAI 리소스 필요",
|
|
266
|
+
mistral: "https://console.mistral.ai/api-keys",
|
|
267
|
+
deepinfra: "https://deepinfra.com/dash/api_keys",
|
|
268
|
+
xai: "https://console.x.ai",
|
|
269
|
+
cerebras: "https://cloud.cerebras.ai",
|
|
290
270
|
}
|
|
291
|
-
return
|
|
271
|
+
return hints[id]
|
|
292
272
|
}
|
|
293
273
|
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
274
|
+
function logo(): string {
|
|
275
|
+
const lines = [
|
|
276
|
+
" ██████╗ █████╗ ███████╗███████╗██╗ ██╗",
|
|
277
|
+
" ██╔══██╗██╔══██╗██╔════╝██╔════╝██║ ██╔╝",
|
|
278
|
+
" ██████╔╝███████║███████╗███████╗█████╔╝ ",
|
|
279
|
+
" ██╔═══╝ ██╔══██║╚════██║╚════██║██╔═██╗ ",
|
|
280
|
+
" ██║ ██║ ██║███████║███████║██║ ██╗",
|
|
281
|
+
" ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝",
|
|
282
|
+
]
|
|
283
|
+
return lines.map((l) => `${H}${l}${N}`).join("\n")
|
|
302
284
|
}
|
|
303
285
|
|
|
304
286
|
async function saveConfigFile(config: Record<string, unknown>) {
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,7 @@ import { JsonMigration } from "@/storage/json-migration"
|
|
|
58
58
|
import { Database } from "@/storage/db"
|
|
59
59
|
import { errorMessage } from "./util/error"
|
|
60
60
|
import { PluginCommand } from "./cli/cmd/plug"
|
|
61
|
+
import { InitCommand } from "./cli/cmd/init"
|
|
61
62
|
import { Heap } from "./cli/heap"
|
|
62
63
|
import { drizzle } from "drizzle-orm/bun-sqlite"
|
|
63
64
|
import { ensureProcessMetadata } from "@saeeol/core/util/saeeol-process"
|
|
@@ -232,6 +233,7 @@ let cli = yargs(args)
|
|
|
232
233
|
.command(RemoteCommand)
|
|
233
234
|
.command(ConfigCLICommand)
|
|
234
235
|
.command(PluginCommand)
|
|
236
|
+
.command(InitCommand)
|
|
235
237
|
.command(DbCommand)
|
|
236
238
|
if (InstallationBuildKind !== "release") {
|
|
237
239
|
cli = cli.command(DevSetupCommand).command(DevAliasCommand)
|