@yoonion/mimi-seed-mcp 0.3.18 → 0.3.20

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.
@@ -0,0 +1,22 @@
1
+ export type CiProvider = 'github' | 'gitlab';
2
+ export interface CiConfig {
3
+ provider: CiProvider;
4
+ token: string;
5
+ owner: string;
6
+ repo: string;
7
+ host?: string;
8
+ }
9
+ export declare function loadCiConfig(): CiConfig | null;
10
+ export declare function saveCiConfig(config: CiConfig): void;
11
+ export declare function requireCiConfig(): CiConfig;
12
+ export interface NormalizedBuild {
13
+ id: number | string;
14
+ name?: string;
15
+ workflow?: string;
16
+ status: string;
17
+ branch: string;
18
+ commit?: string;
19
+ url: string;
20
+ createdAt: string;
21
+ updatedAt: string;
22
+ }
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ const CONFIG_DIR = path.join(os.homedir(), '.mimi-seed');
5
+ const CI_CONFIG_PATH = path.join(CONFIG_DIR, 'ci.json');
6
+ export function loadCiConfig() {
7
+ try {
8
+ return JSON.parse(fs.readFileSync(CI_CONFIG_PATH, 'utf-8'));
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export function saveCiConfig(config) {
15
+ if (!fs.existsSync(CONFIG_DIR)) {
16
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
17
+ }
18
+ fs.writeFileSync(CI_CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
19
+ }
20
+ export function requireCiConfig() {
21
+ const cfg = loadCiConfig();
22
+ if (!cfg) {
23
+ throw new Error('CI 설정이 없습니다.\n' +
24
+ 'ci_save_config 도구로 먼저 설정해주세요.\n' +
25
+ '예시:\n' +
26
+ ' GitHub: ci_save_config(provider="github", token="ghp_...", owner="my-org", repo="my-app")\n' +
27
+ ' GitLab: ci_save_config(provider="gitlab", token="glpat-...", owner="my-group", repo="my-app")');
28
+ }
29
+ return cfg;
30
+ }
@@ -0,0 +1,12 @@
1
+ import type { CiConfig, NormalizedBuild } from './config.js';
2
+ export declare function listWorkflows(cfg: CiConfig): Promise<{
3
+ id: any;
4
+ name: any;
5
+ file: string;
6
+ state: any;
7
+ url: any;
8
+ }[]>;
9
+ export declare function triggerBuild(cfg: CiConfig, workflow: string, ref?: string, inputs?: Record<string, string>): Promise<NormalizedBuild | null>;
10
+ export declare function getBuildStatus(cfg: CiConfig, runId: string | number): Promise<NormalizedBuild>;
11
+ export declare function listRecentBuilds(cfg: CiConfig, workflow?: string, limit?: number): Promise<NormalizedBuild[]>;
12
+ export declare function cancelBuild(cfg: CiConfig, runId: string | number): Promise<void>;
@@ -0,0 +1,87 @@
1
+ const BASE = 'https://api.github.com';
2
+ function headers(token) {
3
+ return {
4
+ Authorization: `Bearer ${token}`,
5
+ Accept: 'application/vnd.github+json',
6
+ 'X-GitHub-Api-Version': '2022-11-28',
7
+ 'Content-Type': 'application/json',
8
+ };
9
+ }
10
+ async function ghFetch(token, endpoint, options) {
11
+ const res = await fetch(`${BASE}${endpoint}`, {
12
+ ...options,
13
+ headers: { ...headers(token), ...(options?.headers ?? {}) },
14
+ });
15
+ if (res.status === 204)
16
+ return null;
17
+ const body = await res.text();
18
+ if (!res.ok)
19
+ throw new Error(`GitHub API ${res.status}: ${body}`);
20
+ return JSON.parse(body);
21
+ }
22
+ export async function listWorkflows(cfg) {
23
+ const data = await ghFetch(cfg.token, `/repos/${cfg.owner}/${cfg.repo}/actions/workflows`);
24
+ return data.workflows.map((w) => ({
25
+ id: w.id,
26
+ name: w.name,
27
+ file: w.path.replace('.github/workflows/', ''),
28
+ state: w.state,
29
+ url: w.html_url,
30
+ }));
31
+ }
32
+ export async function triggerBuild(cfg, workflow, ref = 'main', inputs = {}) {
33
+ // workflow: filename (deploy.yml) or numeric ID string
34
+ const wfId = /^\d+$/.test(workflow) ? Number(workflow) : workflow;
35
+ await ghFetch(cfg.token, `/repos/${cfg.owner}/${cfg.repo}/actions/workflows/${wfId}/dispatches`, {
36
+ method: 'POST',
37
+ body: JSON.stringify({ ref, inputs }),
38
+ });
39
+ // Dispatch returns 204 with no run ID — poll briefly then return latest run
40
+ await new Promise((r) => setTimeout(r, 3000));
41
+ const runs = await listRecentBuilds(cfg, workflow, 1);
42
+ return runs[0] ?? null;
43
+ }
44
+ export async function getBuildStatus(cfg, runId) {
45
+ const data = await ghFetch(cfg.token, `/repos/${cfg.owner}/${cfg.repo}/actions/runs/${runId}`);
46
+ return normalize(data);
47
+ }
48
+ export async function listRecentBuilds(cfg, workflow, limit = 10) {
49
+ let endpoint;
50
+ if (workflow) {
51
+ const wfId = /^\d+$/.test(workflow) ? Number(workflow) : workflow;
52
+ endpoint = `/repos/${cfg.owner}/${cfg.repo}/actions/workflows/${wfId}/runs?per_page=${limit}`;
53
+ }
54
+ else {
55
+ endpoint = `/repos/${cfg.owner}/${cfg.repo}/actions/runs?per_page=${limit}`;
56
+ }
57
+ const data = await ghFetch(cfg.token, endpoint);
58
+ return data.workflow_runs.map(normalize);
59
+ }
60
+ export async function cancelBuild(cfg, runId) {
61
+ await ghFetch(cfg.token, `/repos/${cfg.owner}/${cfg.repo}/actions/runs/${runId}/cancel`, {
62
+ method: 'POST',
63
+ });
64
+ }
65
+ function normalize(r) {
66
+ return {
67
+ id: r.id,
68
+ name: r.name,
69
+ workflow: r.path?.replace('.github/workflows/', '') ?? String(r.workflow_id),
70
+ status: normalizeStatus(r.status, r.conclusion),
71
+ branch: r.head_branch,
72
+ commit: r.head_sha?.slice(0, 7),
73
+ url: r.html_url,
74
+ createdAt: r.created_at,
75
+ updatedAt: r.updated_at,
76
+ };
77
+ }
78
+ function normalizeStatus(status, conclusion) {
79
+ if (status === 'completed')
80
+ return conclusion ?? 'completed';
81
+ // queued → pending, in_progress → running
82
+ if (status === 'queued' || status === 'waiting')
83
+ return 'pending';
84
+ if (status === 'in_progress')
85
+ return 'running';
86
+ return status;
87
+ }
@@ -0,0 +1,20 @@
1
+ import type { CiConfig, NormalizedBuild } from './config.js';
2
+ export interface GitLabWorkflowInfo {
3
+ schedules: Array<{
4
+ id: number;
5
+ description: string;
6
+ ref: string;
7
+ cron: string;
8
+ active: boolean;
9
+ }>;
10
+ triggers: Array<{
11
+ id: number;
12
+ description: string;
13
+ }>;
14
+ note: string;
15
+ }
16
+ export declare function listWorkflows(cfg: CiConfig): Promise<GitLabWorkflowInfo>;
17
+ export declare function triggerBuild(cfg: CiConfig, ref?: string, variables?: Record<string, string>): Promise<NormalizedBuild>;
18
+ export declare function getBuildStatus(cfg: CiConfig, pipelineId: string | number): Promise<NormalizedBuild>;
19
+ export declare function listRecentBuilds(cfg: CiConfig, ref?: string, limit?: number): Promise<NormalizedBuild[]>;
20
+ export declare function cancelBuild(cfg: CiConfig, pipelineId: string | number): Promise<void>;
@@ -0,0 +1,99 @@
1
+ function base(cfg) {
2
+ return `${cfg.host ?? 'https://gitlab.com'}/api/v4`;
3
+ }
4
+ // GitLab accepts both numeric project ID and "namespace%2Frepo" path
5
+ function projectId(cfg) {
6
+ return encodeURIComponent(`${cfg.owner}/${cfg.repo}`);
7
+ }
8
+ function headers(token) {
9
+ return {
10
+ 'PRIVATE-TOKEN': token,
11
+ 'Content-Type': 'application/json',
12
+ };
13
+ }
14
+ async function glFetch(cfg, endpoint, options) {
15
+ const res = await fetch(`${base(cfg)}${endpoint}`, {
16
+ ...options,
17
+ headers: { ...headers(cfg.token), ...(options?.headers ?? {}) },
18
+ });
19
+ if (res.status === 204)
20
+ return null;
21
+ const body = await res.text();
22
+ if (!res.ok)
23
+ throw new Error(`GitLab API ${res.status}: ${body}`);
24
+ return JSON.parse(body);
25
+ }
26
+ export async function listWorkflows(cfg) {
27
+ const [schedules, triggers] = await Promise.all([
28
+ glFetch(cfg, `/projects/${projectId(cfg)}/pipeline_schedules`),
29
+ glFetch(cfg, `/projects/${projectId(cfg)}/triggers`),
30
+ ]);
31
+ return {
32
+ schedules: schedules.map((s) => ({
33
+ id: s.id,
34
+ description: s.description,
35
+ ref: s.ref,
36
+ cron: s.cron,
37
+ active: s.active,
38
+ })),
39
+ triggers: triggers.map((t) => ({
40
+ id: t.id,
41
+ description: t.description ?? '',
42
+ })),
43
+ note: 'GitLab은 workflow 파일 개념이 없습니다. ci_trigger_build(ref="main")로 해당 브랜치의 .gitlab-ci.yml을 즉시 실행하세요.',
44
+ };
45
+ }
46
+ export async function triggerBuild(cfg, ref = 'main', variables = {}) {
47
+ const vars = Object.entries(variables).map(([key, value]) => ({ key, value }));
48
+ const body = { ref };
49
+ if (vars.length > 0)
50
+ body.variables = vars;
51
+ const data = await glFetch(cfg, `/projects/${projectId(cfg)}/pipeline`, {
52
+ method: 'POST',
53
+ body: JSON.stringify(body),
54
+ });
55
+ return normalize(data);
56
+ }
57
+ export async function getBuildStatus(cfg, pipelineId) {
58
+ const data = await glFetch(cfg, `/projects/${projectId(cfg)}/pipelines/${pipelineId}`);
59
+ return normalize(data);
60
+ }
61
+ export async function listRecentBuilds(cfg, ref, limit = 10) {
62
+ let endpoint = `/projects/${projectId(cfg)}/pipelines?per_page=${limit}&order_by=id&sort=desc`;
63
+ if (ref)
64
+ endpoint += `&ref=${encodeURIComponent(ref)}`;
65
+ const data = await glFetch(cfg, endpoint);
66
+ return data.map(normalize);
67
+ }
68
+ export async function cancelBuild(cfg, pipelineId) {
69
+ await glFetch(cfg, `/projects/${projectId(cfg)}/pipelines/${pipelineId}/cancel`, {
70
+ method: 'POST',
71
+ });
72
+ }
73
+ function normalize(p) {
74
+ return {
75
+ id: p.id,
76
+ status: normalizeStatus(p.status),
77
+ branch: p.ref,
78
+ commit: p.sha?.slice(0, 7),
79
+ url: p.web_url,
80
+ createdAt: p.created_at,
81
+ updatedAt: p.updated_at,
82
+ };
83
+ }
84
+ function normalizeStatus(status) {
85
+ const map = {
86
+ created: 'pending',
87
+ waiting_for_resource: 'pending',
88
+ preparing: 'pending',
89
+ pending: 'pending',
90
+ manual: 'pending',
91
+ scheduled: 'pending',
92
+ running: 'running',
93
+ success: 'success',
94
+ failed: 'failed',
95
+ canceled: 'cancelled',
96
+ skipped: 'cancelled',
97
+ };
98
+ return map[status] ?? status;
99
+ }
@@ -0,0 +1,8 @@
1
+ import { ensureFreshAccessToken } from './auth/google-auth.js';
2
+ export { ensureFreshAccessToken };
3
+ export declare function requireAuth(): import("google-auth-library").OAuth2Client;
4
+ export declare const PLAY_AUTH_HINT: string;
5
+ export declare const APPSTORE_AUTH_HINT: string;
6
+ export declare function requirePlayStoreAuth(packageName?: string): import("google-auth-library").JWT;
7
+ export declare function requireServiceAccountJson(packageName?: string): string;
8
+ export declare function requireAppStoreCreds(): import("./appstore/auth.js").AppStoreCredentials;
@@ -0,0 +1,58 @@
1
+ import { getAuthenticatedClient, ensureFreshAccessToken } from './auth/google-auth.js';
2
+ import { getServiceAccountClient, getServiceAccountJson } from './auth/playstore-auth.js';
3
+ import { getAppStoreCredentials } from './appstore/auth.js';
4
+ export { ensureFreshAccessToken };
5
+ export function requireAuth() {
6
+ const client = getAuthenticatedClient();
7
+ if (!client) {
8
+ throw new Error([
9
+ '❌ Google 계정이 연결되지 않았어.',
10
+ '',
11
+ '터미널에서 이것만 실행하면 돼:',
12
+ '',
13
+ ' npx -y @yoonion/mimi-seed-mcp mimi-seed-auth',
14
+ '',
15
+ '브라우저가 열리면 Google 로그인 → 끝.',
16
+ '그 다음에 다시 물어봐줘.',
17
+ ].join('\n'));
18
+ }
19
+ return client;
20
+ }
21
+ export const PLAY_AUTH_HINT = [
22
+ '❌ Google Play 서비스 계정이 연결되지 않았어.',
23
+ '',
24
+ '터미널에서 이것만 실행하면 돼:',
25
+ '',
26
+ ' npx -y @yoonion/mimi-seed-mcp mimi-seed-playstore-auth',
27
+ '',
28
+ '서비스 계정 JSON 파일 경로를 입력하면 저장 완료.',
29
+ '그 다음에 다시 물어봐줘.',
30
+ ].join('\n');
31
+ export const APPSTORE_AUTH_HINT = [
32
+ '❌ App Store Connect 인증이 설정되지 않았어.',
33
+ '',
34
+ '터미널에서 이것만 실행하면 돼:',
35
+ '',
36
+ ' npx -y @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
37
+ '',
38
+ 'Issuer ID, Key ID, .p8 파일 경로를 입력하면 저장 완료.',
39
+ '그 다음에 다시 물어봐줘.',
40
+ ].join('\n');
41
+ export function requirePlayStoreAuth(packageName) {
42
+ const client = getServiceAccountClient(packageName);
43
+ if (!client)
44
+ throw new Error(PLAY_AUTH_HINT);
45
+ return client;
46
+ }
47
+ export function requireServiceAccountJson(packageName) {
48
+ const json = getServiceAccountJson(packageName);
49
+ if (!json)
50
+ throw new Error(PLAY_AUTH_HINT);
51
+ return json;
52
+ }
53
+ export function requireAppStoreCreds() {
54
+ const creds = getAppStoreCredentials();
55
+ if (!creds)
56
+ throw new Error(APPSTORE_AUTH_HINT);
57
+ return creds;
58
+ }