@yoonion/mimi-seed-mcp 0.3.39 → 0.4.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/dist/admob/cli.d.ts +2 -0
- package/dist/admob/cli.js +53 -0
- package/dist/admob/tools.d.ts +12 -2
- package/dist/admob/tools.js +32 -7
- package/dist/auth/errors.d.ts +1 -1
- package/dist/auth/google-auth.d.ts +2 -0
- package/dist/auth/google-auth.js +5 -0
- package/dist/firebase/cli.d.ts +2 -0
- package/dist/firebase/cli.js +82 -0
- package/dist/firebase/tools.d.ts +14 -0
- package/dist/firebase/tools.js +34 -0
- package/dist/ga4/cli.d.ts +2 -0
- package/dist/ga4/cli.js +63 -0
- package/dist/ga4/tools.d.ts +107 -0
- package/dist/ga4/tools.js +119 -0
- package/dist/helpers.d.ts +1 -1
- package/dist/helpers.js +18 -2
- package/dist/index.js +5 -0
- package/dist/jenkins/credentials.d.ts +4 -0
- package/dist/jenkins/credentials.js +67 -28
- package/dist/lib/cli-args.d.ts +31 -0
- package/dist/lib/cli-args.js +96 -0
- package/dist/lib/google-errors.d.ts +8 -0
- package/dist/lib/google-errors.js +26 -0
- package/dist/playstore/errors.js +18 -12
- package/dist/registers/admob.js +4 -3
- package/dist/registers/android.js +4 -0
- package/dist/registers/firebase.js +14 -0
- package/dist/registers/ga4.d.ts +2 -0
- package/dist/registers/ga4.js +90 -0
- package/dist/registers/playstore.js +4 -4
- package/package.json +5 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// mimi-seed-admob — AdMob sub-CLI (계정/앱/광고단위 조회 + v1beta 생성).
|
|
3
|
+
// 사용자 진입점은 `mimi-seed admob <subcommand>`.
|
|
4
|
+
// ⚠️ create-* 는 AdMob v1beta(Limited Access)라 계정 allowlist 미승인 시 403 → 콘솔 수동 폴백.
|
|
5
|
+
import { requireAuth } from '../helpers.js';
|
|
6
|
+
import * as admob from './tools.js';
|
|
7
|
+
import { runDomainCli, requireFlag, flag, CliUsageError } from '../lib/cli-args.js';
|
|
8
|
+
const HELP = `
|
|
9
|
+
💰 mimi-seed-admob — AdMob attach
|
|
10
|
+
|
|
11
|
+
사용법:
|
|
12
|
+
mimi-seed admob accounts
|
|
13
|
+
AdMob 계정 목록 (accountId 확인 — accounts/pub-XXXX)
|
|
14
|
+
|
|
15
|
+
mimi-seed admob apps --account <id>
|
|
16
|
+
mimi-seed admob ad-units --account <id>
|
|
17
|
+
등록된 앱 / 광고 단위 목록
|
|
18
|
+
|
|
19
|
+
mimi-seed admob create-app --account <id> --platform <ANDROID|IOS> --name <displayName>
|
|
20
|
+
[--store-id com.x.y | 123456789]
|
|
21
|
+
앱 등록 (store-id 주면 스토어 링크). ⚠️ v1beta Limited Access — 403 가능
|
|
22
|
+
|
|
23
|
+
mimi-seed admob create-ad-unit --account <id> --app <appId> --name <name>
|
|
24
|
+
--format <BANNER|INTERSTITIAL|REWARDED|REWARDED_INTERSTITIAL|APP_OPEN|NATIVE>
|
|
25
|
+
광고 단위 생성. ⚠️ v1beta Limited Access — 403 가능
|
|
26
|
+
|
|
27
|
+
인증: npx -y @yoonion/mimi-seed-mcp mimi-seed-auth
|
|
28
|
+
`;
|
|
29
|
+
const AD_FORMATS = ['BANNER', 'INTERSTITIAL', 'REWARDED', 'REWARDED_INTERSTITIAL', 'APP_OPEN', 'NATIVE'];
|
|
30
|
+
runDomainCli({
|
|
31
|
+
binName: 'mimi-seed-admob',
|
|
32
|
+
argv: process.argv.slice(2),
|
|
33
|
+
help: HELP,
|
|
34
|
+
handlers: {
|
|
35
|
+
accounts: async () => admob.listAccounts(await requireAuth()),
|
|
36
|
+
apps: async (p) => admob.listApps(await requireAuth(), requireFlag(p, 'account')),
|
|
37
|
+
'ad-units': async (p) => admob.listAdUnits(await requireAuth(), requireFlag(p, 'account')),
|
|
38
|
+
'create-app': async (p) => {
|
|
39
|
+
const platform = requireFlag(p, 'platform').toUpperCase();
|
|
40
|
+
if (platform !== 'ANDROID' && platform !== 'IOS') {
|
|
41
|
+
throw new CliUsageError('--platform 은 ANDROID | IOS 여야 합니다.');
|
|
42
|
+
}
|
|
43
|
+
return admob.createApp(await requireAuth(), requireFlag(p, 'account'), platform, requireFlag(p, 'name'), flag(p, 'store-id'));
|
|
44
|
+
},
|
|
45
|
+
'create-ad-unit': async (p) => {
|
|
46
|
+
const format = requireFlag(p, 'format').toUpperCase();
|
|
47
|
+
if (!AD_FORMATS.includes(format)) {
|
|
48
|
+
throw new CliUsageError(`--format 은 ${AD_FORMATS.join(' | ')} 중 하나여야 합니다.`);
|
|
49
|
+
}
|
|
50
|
+
return admob.createAdUnit(await requireAuth(), requireFlag(p, 'account'), requireFlag(p, 'app'), requireFlag(p, 'name'), format);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
package/dist/admob/tools.d.ts
CHANGED
|
@@ -35,5 +35,15 @@ export declare function getNetworkReport(auth: OAuth2Client, accountId: string,
|
|
|
35
35
|
day: number;
|
|
36
36
|
}): Promise<import("googleapis").admob_v1.Schema$GenerateNetworkReportResponse>;
|
|
37
37
|
export declare function getTodayEarnings(auth: OAuth2Client, accountId: string): Promise<import("googleapis").admob_v1.Schema$GenerateNetworkReportResponse>;
|
|
38
|
-
export
|
|
39
|
-
|
|
38
|
+
export type AdFormat = 'BANNER' | 'INTERSTITIAL' | 'REWARDED' | 'REWARDED_INTERSTITIAL' | 'APP_OPEN' | 'NATIVE';
|
|
39
|
+
/**
|
|
40
|
+
* adFormat → adTypes 파생 (순수 함수 — 테스트 대상).
|
|
41
|
+
* AdMob v1beta adTypes 의 유효값은 'RICH_MEDIA'(텍스트·이미지 등 비동영상)와 'VIDEO' 둘뿐
|
|
42
|
+
* (Schema$AdUnit.adTypes 문서). 동영상 지원 포맷은 VIDEO 를 포함하고,
|
|
43
|
+
* REWARDED_INTERSTITIAL 은 문서상 video-only 다.
|
|
44
|
+
*/
|
|
45
|
+
export declare function adTypesForFormat(adFormat: AdFormat): string[];
|
|
46
|
+
export declare function createApp(auth: OAuth2Client, accountId: string, platform: 'ANDROID' | 'IOS', displayName: string,
|
|
47
|
+
/** 스토어에 게시된 앱이면 store ID 로 링크 (Android=패키지명, iOS=App Store 숫자 ID). 없으면 manual(unlinked) 앱. */
|
|
48
|
+
appStoreId?: string): Promise<any>;
|
|
49
|
+
export declare function createAdUnit(auth: OAuth2Client, accountId: string, appId: string, displayName: string, adFormat: AdFormat): Promise<any>;
|
package/dist/admob/tools.js
CHANGED
|
@@ -91,17 +91,42 @@ export async function getTodayEarnings(auth, accountId) {
|
|
|
91
91
|
const today = { year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate() };
|
|
92
92
|
return getNetworkReport(auth, accountId, today, today);
|
|
93
93
|
}
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
/**
|
|
95
|
+
* adFormat → adTypes 파생 (순수 함수 — 테스트 대상).
|
|
96
|
+
* AdMob v1beta adTypes 의 유효값은 'RICH_MEDIA'(텍스트·이미지 등 비동영상)와 'VIDEO' 둘뿐
|
|
97
|
+
* (Schema$AdUnit.adTypes 문서). 동영상 지원 포맷은 VIDEO 를 포함하고,
|
|
98
|
+
* REWARDED_INTERSTITIAL 은 문서상 video-only 다.
|
|
99
|
+
*/
|
|
100
|
+
export function adTypesForFormat(adFormat) {
|
|
101
|
+
switch (adFormat) {
|
|
102
|
+
case 'INTERSTITIAL':
|
|
103
|
+
case 'REWARDED':
|
|
104
|
+
case 'APP_OPEN':
|
|
105
|
+
return ['RICH_MEDIA', 'VIDEO'];
|
|
106
|
+
case 'REWARDED_INTERSTITIAL':
|
|
107
|
+
return ['VIDEO'];
|
|
108
|
+
case 'BANNER':
|
|
109
|
+
case 'NATIVE':
|
|
110
|
+
default:
|
|
111
|
+
return ['RICH_MEDIA'];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export async function createApp(auth, accountId, platform, displayName,
|
|
115
|
+
/** 스토어에 게시된 앱이면 store ID 로 링크 (Android=패키지명, iOS=App Store 숫자 ID). 없으면 manual(unlinked) 앱. */
|
|
116
|
+
appStoreId) {
|
|
96
117
|
// v1beta — 대부분 계정에서 403. Google Account Manager 승인 필요.
|
|
97
118
|
const admobBeta = google.admob('v1beta');
|
|
119
|
+
const requestBody = { platform };
|
|
120
|
+
// manualAppInfo.displayName 은 writable(사용자 제공, AdMob UI 표시명) — linking 후에도 유지된다.
|
|
121
|
+
// linkedAppInfo.displayName 은 output-only(스토어 표시명)라 보내도 무시되므로, store 링크엔 appStoreId 만 넣는다.
|
|
122
|
+
requestBody.manualAppInfo = { displayName };
|
|
123
|
+
if (appStoreId) {
|
|
124
|
+
requestBody.linkedAppInfo = { appStoreId };
|
|
125
|
+
}
|
|
98
126
|
const res = await admobBeta.accounts.apps.create({
|
|
99
127
|
auth,
|
|
100
128
|
parent: accountId,
|
|
101
|
-
requestBody
|
|
102
|
-
platform,
|
|
103
|
-
manualAppInfo: { displayName },
|
|
104
|
-
},
|
|
129
|
+
requestBody,
|
|
105
130
|
});
|
|
106
131
|
return res.data;
|
|
107
132
|
}
|
|
@@ -115,7 +140,7 @@ export async function createAdUnit(auth, accountId, appId, displayName, adFormat
|
|
|
115
140
|
appId,
|
|
116
141
|
displayName,
|
|
117
142
|
adFormat,
|
|
118
|
-
adTypes:
|
|
143
|
+
adTypes: adTypesForFormat(adFormat),
|
|
119
144
|
},
|
|
120
145
|
});
|
|
121
146
|
return res.data;
|
package/dist/auth/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AuthErrorCode = 'UNAUTHENTICATED' | 'NO_REFRESH_TOKEN' | 'INVALID_GRANT' | 'RAPT_REQUIRED' | 'INVALID_CLIENT' | 'UNAUTHORIZED_CLIENT' | 'REFRESH_NETWORK_ERROR' | 'REFRESH_UNKNOWN' | 'CALLBACK_PORT_IN_USE' | 'CALLBACK_TIMEOUT' | 'BROWSER_OPEN_FAILED' | 'USER_DENIED' | 'CODE_EXCHANGE_FAILED' | 'TOKEN_RESPONSE_INVALID';
|
|
1
|
+
export type AuthErrorCode = 'UNAUTHENTICATED' | 'NO_REFRESH_TOKEN' | 'INVALID_GRANT' | 'RAPT_REQUIRED' | 'INVALID_CLIENT' | 'UNAUTHORIZED_CLIENT' | 'REFRESH_NETWORK_ERROR' | 'REFRESH_UNKNOWN' | 'INSUFFICIENT_SCOPE' | 'CALLBACK_PORT_IN_USE' | 'CALLBACK_TIMEOUT' | 'BROWSER_OPEN_FAILED' | 'USER_DENIED' | 'CODE_EXCHANGE_FAILED' | 'TOKEN_RESPONSE_INVALID';
|
|
2
2
|
export interface AuthErrorPayload {
|
|
3
3
|
code: AuthErrorCode;
|
|
4
4
|
message: string;
|
|
@@ -4,6 +4,8 @@ export interface StoredTokens {
|
|
|
4
4
|
refresh_token: string;
|
|
5
5
|
token_type: string;
|
|
6
6
|
expiry_date: number;
|
|
7
|
+
/** 공백 구분 부여 스코프. 신규 도구(GA4 등) pre-flight 스코프 검사에 사용. 구 토큰은 undefined. */
|
|
8
|
+
scope?: string;
|
|
7
9
|
}
|
|
8
10
|
export declare function getStoredCredentials(): {
|
|
9
11
|
clientId: string;
|
package/dist/auth/google-auth.js
CHANGED
|
@@ -14,6 +14,7 @@ const SCOPES = [
|
|
|
14
14
|
'https://www.googleapis.com/auth/androidpublisher',
|
|
15
15
|
'https://www.googleapis.com/auth/adwords', // Google Ads API (googleads_* tools)
|
|
16
16
|
'https://www.googleapis.com/auth/webmasters', // Search Console API (gsc_* tools, 사이트맵 제출 포함)
|
|
17
|
+
'https://www.googleapis.com/auth/analytics.edit', // GA4 Analytics Admin API (ga4_* tools — property/data stream 생성·조회)
|
|
17
18
|
];
|
|
18
19
|
// Primary config dir. Legacy `~/.preseed` is read as a fallback during the
|
|
19
20
|
// rebrand so existing auth sessions don't force a re-login; new writes go to
|
|
@@ -112,6 +113,7 @@ export function getAuthenticatedClient() {
|
|
|
112
113
|
...(newTokens.access_token && { access_token: newTokens.access_token }),
|
|
113
114
|
...(newTokens.refresh_token && { refresh_token: newTokens.refresh_token }),
|
|
114
115
|
...(newTokens.expiry_date && { expiry_date: newTokens.expiry_date }),
|
|
116
|
+
...(newTokens.scope && { scope: newTokens.scope }),
|
|
115
117
|
});
|
|
116
118
|
}
|
|
117
119
|
});
|
|
@@ -209,6 +211,7 @@ export function startAuth(clientId, clientSecret, options = {}) {
|
|
|
209
211
|
refresh_token: tokens.refresh_token,
|
|
210
212
|
token_type: tokens.token_type ?? 'Bearer',
|
|
211
213
|
expiry_date: tokens.expiry_date ?? Date.now() + 3600_000,
|
|
214
|
+
...(tokens.scope && { scope: tokens.scope }),
|
|
212
215
|
};
|
|
213
216
|
saveTokens(stored);
|
|
214
217
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
@@ -327,6 +330,8 @@ export async function ensureFreshAccessToken(marginMs = 300_000) {
|
|
|
327
330
|
refresh_token: credentials.refresh_token ?? tokens.refresh_token,
|
|
328
331
|
token_type: credentials.token_type ?? tokens.token_type ?? 'Bearer',
|
|
329
332
|
expiry_date: credentials.expiry_date ?? Date.now() + 3600_000,
|
|
333
|
+
// refresh 응답은 scope 를 생략할 수 있으므로 기존 값을 보존(blank 방지).
|
|
334
|
+
scope: credentials.scope ?? tokens.scope,
|
|
330
335
|
};
|
|
331
336
|
saveTokens(refreshed);
|
|
332
337
|
return {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// mimi-seed-firebase — Firebase 프로비저닝 sub-CLI (프로젝트/앱 조회·생성, config 다운로드, GA4 링크).
|
|
3
|
+
// 사용자 진입점은 `mimi-seed firebase <subcommand>`.
|
|
4
|
+
import { requireAuth } from '../helpers.js';
|
|
5
|
+
import * as firebase from './tools.js';
|
|
6
|
+
import { runDomainCli, requireFlag, flag, CliUsageError } from '../lib/cli-args.js';
|
|
7
|
+
const FB_PLATFORMS = ['android', 'ios', 'web'];
|
|
8
|
+
const HELP = `
|
|
9
|
+
🔥 mimi-seed-firebase — Firebase attach (프로비저닝 + config 산출)
|
|
10
|
+
|
|
11
|
+
사용법:
|
|
12
|
+
mimi-seed firebase projects
|
|
13
|
+
내 Firebase 프로젝트 목록
|
|
14
|
+
|
|
15
|
+
mimi-seed firebase apps --project <id> [--platform android|ios|web]
|
|
16
|
+
프로젝트의 앱 목록 (기본: android+ios+web 전부)
|
|
17
|
+
|
|
18
|
+
mimi-seed firebase create-android --project <id> --package com.x.y --name <displayName>
|
|
19
|
+
mimi-seed firebase create-ios --project <id> --bundle com.x.y --name <displayName>
|
|
20
|
+
새 앱 등록
|
|
21
|
+
|
|
22
|
+
mimi-seed firebase config --project <id> --app <appId> --platform <android|ios|web>
|
|
23
|
+
⭐ google-services.json / GoogleService-Info.plist / web config 출력 (attach 페이로드)
|
|
24
|
+
|
|
25
|
+
mimi-seed firebase enable-services --project <id>
|
|
26
|
+
Firebase 기본 서비스 일괄 활성화 (Analytics 포함)
|
|
27
|
+
|
|
28
|
+
mimi-seed firebase link-analytics --project <id> (--account <gaAccountId> | --property <gaPropertyId>)
|
|
29
|
+
GA4 링크 (앱 measurement 자동 활성화) — account(신규 property 생성) 또는 property(기존 연결) 중 하나 필수
|
|
30
|
+
|
|
31
|
+
mimi-seed firebase analytics-details --project <id>
|
|
32
|
+
GA4 링크 상세 (property + stream 매핑)
|
|
33
|
+
|
|
34
|
+
인증: npx -y @yoonion/mimi-seed-mcp mimi-seed-auth
|
|
35
|
+
`;
|
|
36
|
+
async function listApps(projectId, platform) {
|
|
37
|
+
const auth = await requireAuth();
|
|
38
|
+
// platform 이 지정됐는데 알 수 없는 값이면 조용히 'all' 로 떨어지지 않고 명시적으로 거부.
|
|
39
|
+
if (platform !== undefined && !FB_PLATFORMS.includes(platform)) {
|
|
40
|
+
throw new CliUsageError('--platform 은 android | ios | web 중 하나여야 합니다.');
|
|
41
|
+
}
|
|
42
|
+
if (platform === 'android')
|
|
43
|
+
return firebase.listAndroidApps(auth, projectId);
|
|
44
|
+
if (platform === 'ios')
|
|
45
|
+
return firebase.listIosApps(auth, projectId);
|
|
46
|
+
if (platform === 'web')
|
|
47
|
+
return firebase.listWebApps(auth, projectId);
|
|
48
|
+
const [android, ios, web] = await Promise.all([
|
|
49
|
+
firebase.listAndroidApps(auth, projectId),
|
|
50
|
+
firebase.listIosApps(auth, projectId),
|
|
51
|
+
firebase.listWebApps(auth, projectId),
|
|
52
|
+
]);
|
|
53
|
+
return { android, ios, web };
|
|
54
|
+
}
|
|
55
|
+
async function getConfig(projectId, appId, platform) {
|
|
56
|
+
const auth = await requireAuth();
|
|
57
|
+
if (platform === 'android')
|
|
58
|
+
return firebase.getAndroidConfig(auth, projectId, appId);
|
|
59
|
+
if (platform === 'ios')
|
|
60
|
+
return firebase.getIosConfig(auth, projectId, appId);
|
|
61
|
+
if (platform === 'web')
|
|
62
|
+
return firebase.getWebConfig(auth, projectId, appId);
|
|
63
|
+
throw new CliUsageError('--platform 은 android | ios | web 중 하나여야 합니다.');
|
|
64
|
+
}
|
|
65
|
+
runDomainCli({
|
|
66
|
+
binName: 'mimi-seed-firebase',
|
|
67
|
+
argv: process.argv.slice(2),
|
|
68
|
+
help: HELP,
|
|
69
|
+
handlers: {
|
|
70
|
+
projects: async () => firebase.listProjects(await requireAuth()),
|
|
71
|
+
apps: async (p) => listApps(requireFlag(p, 'project'), flag(p, 'platform')?.toLowerCase()),
|
|
72
|
+
'create-android': async (p) => firebase.createAndroidApp(await requireAuth(), requireFlag(p, 'project'), requireFlag(p, 'package'), requireFlag(p, 'name')),
|
|
73
|
+
'create-ios': async (p) => firebase.createIosApp(await requireAuth(), requireFlag(p, 'project'), requireFlag(p, 'bundle'), requireFlag(p, 'name')),
|
|
74
|
+
config: async (p) => getConfig(requireFlag(p, 'project'), requireFlag(p, 'app'), requireFlag(p, 'platform').toLowerCase()),
|
|
75
|
+
'enable-services': async (p) => firebase.enableCommonServices(await requireAuth(), requireFlag(p, 'project')),
|
|
76
|
+
'link-analytics': async (p) => firebase.linkAnalytics(await requireAuth(), requireFlag(p, 'project'), {
|
|
77
|
+
analyticsAccountId: flag(p, 'account'),
|
|
78
|
+
analyticsPropertyId: flag(p, 'property'),
|
|
79
|
+
}),
|
|
80
|
+
'analytics-details': async (p) => firebase.getAnalyticsDetails(await requireAuth(), requireFlag(p, 'project')),
|
|
81
|
+
},
|
|
82
|
+
});
|
package/dist/firebase/tools.d.ts
CHANGED
|
@@ -49,6 +49,20 @@ export declare function listEnabledServices(auth: OAuth2Client, projectId: strin
|
|
|
49
49
|
name: string | null | undefined;
|
|
50
50
|
title: string | null | undefined;
|
|
51
51
|
}[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Firebase 프로젝트에 Google Analytics(GA4)를 링크한다.
|
|
54
|
+
* - analyticsAccountId: 그 GA 계정 하위에 GA4 property 를 새로 만들어 링크.
|
|
55
|
+
* - analyticsPropertyId: 기존 GA4 property 에 링크.
|
|
56
|
+
* 둘 중 정확히 하나는 필수다(addGoogleAnalytics API 계약 — 무인자 모드 없음).
|
|
57
|
+
* 앱별 data stream(android/ios measurement)은 링크 후 Firebase 가 자동 생성한다.
|
|
58
|
+
* (반환값은 long-running Operation — 완료까지 수 초 소요될 수 있음.)
|
|
59
|
+
*/
|
|
60
|
+
export declare function linkAnalytics(auth: OAuth2Client, projectId: string, opts?: {
|
|
61
|
+
analyticsAccountId?: string;
|
|
62
|
+
analyticsPropertyId?: string;
|
|
63
|
+
}): Promise<import("googleapis").firebase_v1beta1.Schema$Operation>;
|
|
64
|
+
/** 프로젝트의 GA4 링크 상세 — 연결된 analyticsProperty + 앱↔stream 매핑 조회. */
|
|
65
|
+
export declare function getAnalyticsDetails(auth: OAuth2Client, projectId: string): Promise<import("googleapis").firebase_v1beta1.Schema$AnalyticsDetails>;
|
|
52
66
|
export declare function enableCommonServices(auth: OAuth2Client, projectId: string): Promise<({
|
|
53
67
|
service: string;
|
|
54
68
|
status: string;
|
package/dist/firebase/tools.js
CHANGED
|
@@ -158,6 +158,39 @@ export async function listEnabledServices(auth, projectId) {
|
|
|
158
158
|
title: s.config?.title,
|
|
159
159
|
}));
|
|
160
160
|
}
|
|
161
|
+
// ─── Google Analytics(GA4) 링크 ───
|
|
162
|
+
/**
|
|
163
|
+
* Firebase 프로젝트에 Google Analytics(GA4)를 링크한다.
|
|
164
|
+
* - analyticsAccountId: 그 GA 계정 하위에 GA4 property 를 새로 만들어 링크.
|
|
165
|
+
* - analyticsPropertyId: 기존 GA4 property 에 링크.
|
|
166
|
+
* 둘 중 정확히 하나는 필수다(addGoogleAnalytics API 계약 — 무인자 모드 없음).
|
|
167
|
+
* 앱별 data stream(android/ios measurement)은 링크 후 Firebase 가 자동 생성한다.
|
|
168
|
+
* (반환값은 long-running Operation — 완료까지 수 초 소요될 수 있음.)
|
|
169
|
+
*/
|
|
170
|
+
export async function linkAnalytics(auth, projectId, opts = {}) {
|
|
171
|
+
const requestBody = {};
|
|
172
|
+
if (opts.analyticsAccountId)
|
|
173
|
+
requestBody.analyticsAccountId = opts.analyticsAccountId;
|
|
174
|
+
if (opts.analyticsPropertyId)
|
|
175
|
+
requestBody.analyticsPropertyId = opts.analyticsPropertyId;
|
|
176
|
+
if (!requestBody.analyticsAccountId && !requestBody.analyticsPropertyId) {
|
|
177
|
+
throw new Error('GA4 링크엔 analyticsAccountId(신규 property 생성) 또는 analyticsPropertyId(기존 property 연결) 중 하나가 필요합니다.');
|
|
178
|
+
}
|
|
179
|
+
const res = await google.firebase('v1beta1').projects.addGoogleAnalytics({
|
|
180
|
+
auth,
|
|
181
|
+
parent: `projects/${projectId}`,
|
|
182
|
+
requestBody,
|
|
183
|
+
});
|
|
184
|
+
return res.data;
|
|
185
|
+
}
|
|
186
|
+
/** 프로젝트의 GA4 링크 상세 — 연결된 analyticsProperty + 앱↔stream 매핑 조회. */
|
|
187
|
+
export async function getAnalyticsDetails(auth, projectId) {
|
|
188
|
+
const res = await google.firebase('v1beta1').projects.getAnalyticsDetails({
|
|
189
|
+
auth,
|
|
190
|
+
name: `projects/${projectId}/analyticsDetails`,
|
|
191
|
+
});
|
|
192
|
+
return res.data;
|
|
193
|
+
}
|
|
161
194
|
// ─── 편의 함수 ───
|
|
162
195
|
const COMMON_SERVICES = [
|
|
163
196
|
'firebase.googleapis.com',
|
|
@@ -167,6 +200,7 @@ const COMMON_SERVICES = [
|
|
|
167
200
|
'fcm.googleapis.com',
|
|
168
201
|
'identitytoolkit.googleapis.com',
|
|
169
202
|
'cloudresourcemanager.googleapis.com',
|
|
203
|
+
'firebaseanalytics.googleapis.com', // GA4(Firebase Analytics) — linkAnalytics 전에 활성화
|
|
170
204
|
];
|
|
171
205
|
export async function enableCommonServices(auth, projectId) {
|
|
172
206
|
const results = [];
|
package/dist/ga4/cli.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// mimi-seed-ga4 — GA4 Analytics Admin/Data API sub-CLI.
|
|
3
|
+
// 사용자 진입점은 `mimi-seed ga4 <subcommand>` (cli 패키지가 npx 로 이 bin 을 호출).
|
|
4
|
+
import { requireAuth } from '../helpers.js';
|
|
5
|
+
import * as ga4 from './tools.js';
|
|
6
|
+
import { runDomainCli, requireFlag, flag, flagList } from '../lib/cli-args.js';
|
|
7
|
+
const HELP = `
|
|
8
|
+
📊 mimi-seed-ga4 — GA4(Google Analytics 4) attach
|
|
9
|
+
|
|
10
|
+
사용법:
|
|
11
|
+
mimi-seed ga4 accounts
|
|
12
|
+
접근 가능한 GA 계정 + property 요약 (accountId/propertyId 확인)
|
|
13
|
+
|
|
14
|
+
mimi-seed ga4 properties --account <id>
|
|
15
|
+
계정 하위 GA4 property 목록
|
|
16
|
+
|
|
17
|
+
mimi-seed ga4 create-property --account <id> --name <displayName>
|
|
18
|
+
[--timezone Asia/Seoul] [--currency KRW]
|
|
19
|
+
새 GA4 property 생성
|
|
20
|
+
|
|
21
|
+
mimi-seed ga4 create-stream --property <id> --platform <web|android|ios> --name <name>
|
|
22
|
+
[--uri https://...] [--package com.x.y] [--bundle com.x.y]
|
|
23
|
+
data stream 생성 (web→measurementId, app→firebaseAppId)
|
|
24
|
+
|
|
25
|
+
mimi-seed ga4 streams --property <id>
|
|
26
|
+
data stream 목록
|
|
27
|
+
|
|
28
|
+
mimi-seed ga4 report --property <id> --start 28daysAgo --end today
|
|
29
|
+
[--dimensions date,country] [--metrics activeUsers,eventCount]
|
|
30
|
+
GA4 Data API 리포트
|
|
31
|
+
|
|
32
|
+
⚠️ analytics.edit 스코프 필요 — 처음이면 재로그인:
|
|
33
|
+
npx -y @yoonion/mimi-seed-mcp mimi-seed-auth
|
|
34
|
+
`;
|
|
35
|
+
runDomainCli({
|
|
36
|
+
binName: 'mimi-seed-ga4',
|
|
37
|
+
argv: process.argv.slice(2),
|
|
38
|
+
help: HELP,
|
|
39
|
+
handlers: {
|
|
40
|
+
accounts: async () => ga4.listAccountSummaries(await requireAuth(ga4.GA4_SCOPE)),
|
|
41
|
+
properties: async (p) => ga4.listProperties(await requireAuth(ga4.GA4_SCOPE), requireFlag(p, 'account')),
|
|
42
|
+
'create-property': async (p) => ga4.createProperty(await requireAuth(ga4.GA4_SCOPE), {
|
|
43
|
+
accountId: requireFlag(p, 'account'),
|
|
44
|
+
displayName: requireFlag(p, 'name'),
|
|
45
|
+
timeZone: flag(p, 'timezone'),
|
|
46
|
+
currencyCode: flag(p, 'currency'),
|
|
47
|
+
}),
|
|
48
|
+
'create-stream': async (p) => ga4.createDataStream(await requireAuth(ga4.GA4_SCOPE), requireFlag(p, 'property'), {
|
|
49
|
+
platform: requireFlag(p, 'platform'),
|
|
50
|
+
displayName: requireFlag(p, 'name'),
|
|
51
|
+
defaultUri: flag(p, 'uri'),
|
|
52
|
+
packageName: flag(p, 'package'),
|
|
53
|
+
bundleId: flag(p, 'bundle'),
|
|
54
|
+
}),
|
|
55
|
+
streams: async (p) => ga4.listDataStreams(await requireAuth(ga4.GA4_SCOPE), requireFlag(p, 'property')),
|
|
56
|
+
report: async (p) => ga4.runReport(await requireAuth(ga4.GA4_SCOPE), requireFlag(p, 'property'), {
|
|
57
|
+
startDate: requireFlag(p, 'start'),
|
|
58
|
+
endDate: requireFlag(p, 'end'),
|
|
59
|
+
dimensions: flagList(p, 'dimensions'),
|
|
60
|
+
metrics: flagList(p, 'metrics'),
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { OAuth2Client } from 'google-auth-library';
|
|
2
|
+
/** GA4 도구가 요구하는 OAuth 스코프 — requireAuth() pre-flight 검사에 사용. */
|
|
3
|
+
export declare const GA4_SCOPE = "https://www.googleapis.com/auth/analytics.edit";
|
|
4
|
+
export type Ga4Auth = OAuth2Client;
|
|
5
|
+
export type DataStreamPlatform = 'web' | 'android' | 'ios';
|
|
6
|
+
/** '123' | 'accounts/123' → 'accounts/123' */
|
|
7
|
+
export declare function normalizeAccountName(accountId: string): string;
|
|
8
|
+
/** '123' | 'properties/123' → 'properties/123' */
|
|
9
|
+
export declare function normalizePropertyName(propertyId: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* dataStreams.create 응답에서 클라이언트가 실제로 필요로 하는 식별자를 평탄화한다.
|
|
12
|
+
* - web stream → measurementId (G-XXXX, gtag/web 연동용)
|
|
13
|
+
* - app stream → firebaseAppId (Firebase 앱과 자동 링크된 경우)
|
|
14
|
+
*/
|
|
15
|
+
export declare function flattenDataStream(d: {
|
|
16
|
+
name?: string | null;
|
|
17
|
+
type?: string | null;
|
|
18
|
+
displayName?: string | null;
|
|
19
|
+
webStreamData?: {
|
|
20
|
+
measurementId?: string | null;
|
|
21
|
+
defaultUri?: string | null;
|
|
22
|
+
} | null;
|
|
23
|
+
androidAppStreamData?: {
|
|
24
|
+
firebaseAppId?: string | null;
|
|
25
|
+
packageName?: string | null;
|
|
26
|
+
} | null;
|
|
27
|
+
iosAppStreamData?: {
|
|
28
|
+
firebaseAppId?: string | null;
|
|
29
|
+
bundleId?: string | null;
|
|
30
|
+
} | null;
|
|
31
|
+
}): {
|
|
32
|
+
name: string | null;
|
|
33
|
+
type: string | null;
|
|
34
|
+
displayName: string | null;
|
|
35
|
+
measurementId: string | null;
|
|
36
|
+
firebaseAppId: string | null;
|
|
37
|
+
};
|
|
38
|
+
/** data stream 생성 요청 본문 조립 (순수 함수 — 테스트 대상). */
|
|
39
|
+
export declare function buildDataStreamBody(opts: {
|
|
40
|
+
platform: DataStreamPlatform;
|
|
41
|
+
displayName: string;
|
|
42
|
+
defaultUri?: string;
|
|
43
|
+
packageName?: string;
|
|
44
|
+
bundleId?: string;
|
|
45
|
+
}): {
|
|
46
|
+
type: string;
|
|
47
|
+
displayName: string;
|
|
48
|
+
webStreamData: {
|
|
49
|
+
defaultUri: string | undefined;
|
|
50
|
+
};
|
|
51
|
+
androidAppStreamData?: undefined;
|
|
52
|
+
iosAppStreamData?: undefined;
|
|
53
|
+
} | {
|
|
54
|
+
type: string;
|
|
55
|
+
displayName: string;
|
|
56
|
+
androidAppStreamData: {
|
|
57
|
+
packageName: string | undefined;
|
|
58
|
+
};
|
|
59
|
+
webStreamData?: undefined;
|
|
60
|
+
iosAppStreamData?: undefined;
|
|
61
|
+
} | {
|
|
62
|
+
type: string;
|
|
63
|
+
displayName: string;
|
|
64
|
+
iosAppStreamData: {
|
|
65
|
+
bundleId: string | undefined;
|
|
66
|
+
};
|
|
67
|
+
webStreamData?: undefined;
|
|
68
|
+
androidAppStreamData?: undefined;
|
|
69
|
+
};
|
|
70
|
+
/** 접근 가능한 GA 계정 + 각 계정의 property 요약. accountId/propertyId 를 찾는 시작점. */
|
|
71
|
+
export declare function listAccountSummaries(auth: Ga4Auth): Promise<import("googleapis").analyticsadmin_v1beta.Schema$GoogleAnalyticsAdminV1betaAccountSummary[]>;
|
|
72
|
+
/** 계정 하위 GA4 property 목록. accountId: '123' 또는 'accounts/123'. */
|
|
73
|
+
export declare function listProperties(auth: Ga4Auth, accountId: string): Promise<import("googleapis").analyticsadmin_v1beta.Schema$GoogleAnalyticsAdminV1betaProperty[]>;
|
|
74
|
+
export declare function createProperty(auth: Ga4Auth, opts: {
|
|
75
|
+
accountId: string;
|
|
76
|
+
displayName: string;
|
|
77
|
+
timeZone?: string;
|
|
78
|
+
currencyCode?: string;
|
|
79
|
+
}): Promise<import("googleapis").analyticsadmin_v1beta.Schema$GoogleAnalyticsAdminV1betaProperty>;
|
|
80
|
+
export declare function createDataStream(auth: Ga4Auth, propertyId: string, opts: {
|
|
81
|
+
platform: DataStreamPlatform;
|
|
82
|
+
displayName: string;
|
|
83
|
+
defaultUri?: string;
|
|
84
|
+
packageName?: string;
|
|
85
|
+
bundleId?: string;
|
|
86
|
+
}): Promise<{
|
|
87
|
+
raw: import("googleapis").analyticsadmin_v1beta.Schema$GoogleAnalyticsAdminV1betaDataStream;
|
|
88
|
+
name: string | null;
|
|
89
|
+
type: string | null;
|
|
90
|
+
displayName: string | null;
|
|
91
|
+
measurementId: string | null;
|
|
92
|
+
firebaseAppId: string | null;
|
|
93
|
+
}>;
|
|
94
|
+
export declare function listDataStreams(auth: Ga4Auth, propertyId: string): Promise<{
|
|
95
|
+
name: string | null;
|
|
96
|
+
type: string | null;
|
|
97
|
+
displayName: string | null;
|
|
98
|
+
measurementId: string | null;
|
|
99
|
+
firebaseAppId: string | null;
|
|
100
|
+
}[]>;
|
|
101
|
+
export interface RunReportParams {
|
|
102
|
+
startDate: string;
|
|
103
|
+
endDate: string;
|
|
104
|
+
dimensions?: string[];
|
|
105
|
+
metrics?: string[];
|
|
106
|
+
}
|
|
107
|
+
export declare function runReport(auth: Ga4Auth, propertyId: string, params: RunReportParams): Promise<import("googleapis").analyticsdata_v1beta.Schema$RunReportResponse>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
/**
|
|
3
|
+
* Google Analytics 4 — Admin API(v1beta) + Data API(v1beta) 래퍼.
|
|
4
|
+
*
|
|
5
|
+
* Admin API 로 GA4 property/data stream 을 직접 생성·조회한다. Firebase Analytics 를
|
|
6
|
+
* enable 하면 GA4 property 가 자동 생성되지만, 그 경로는 property 이름/타임존/통화나
|
|
7
|
+
* web data stream(measurement ID, G-XXXX) 을 제어할 수 없다. 풀 컨트롤이 필요하면 이 모듈을 쓴다.
|
|
8
|
+
*
|
|
9
|
+
* 인증은 mimi-seed OAuth 클라이언트(`requireAuth()`)를 그대로 사용 — `analytics.edit` 스코프 필요.
|
|
10
|
+
* (스코프 추가 후 기존 사용자는 1회 재로그인: `npx -y @yoonion/mimi-seed-mcp mimi-seed-auth`)
|
|
11
|
+
*/
|
|
12
|
+
const admin = () => google.analyticsadmin('v1beta');
|
|
13
|
+
const data = () => google.analyticsdata('v1beta');
|
|
14
|
+
/** GA4 도구가 요구하는 OAuth 스코프 — requireAuth() pre-flight 검사에 사용. */
|
|
15
|
+
export const GA4_SCOPE = 'https://www.googleapis.com/auth/analytics.edit';
|
|
16
|
+
// ─── 경로 정규화 (순수 함수 — 테스트 대상) ───
|
|
17
|
+
/** '123' | 'accounts/123' → 'accounts/123' */
|
|
18
|
+
export function normalizeAccountName(accountId) {
|
|
19
|
+
const id = accountId.trim();
|
|
20
|
+
return id.startsWith('accounts/') ? id : `accounts/${id}`;
|
|
21
|
+
}
|
|
22
|
+
/** '123' | 'properties/123' → 'properties/123' */
|
|
23
|
+
export function normalizePropertyName(propertyId) {
|
|
24
|
+
const id = propertyId.trim();
|
|
25
|
+
return id.startsWith('properties/') ? id : `properties/${id}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* dataStreams.create 응답에서 클라이언트가 실제로 필요로 하는 식별자를 평탄화한다.
|
|
29
|
+
* - web stream → measurementId (G-XXXX, gtag/web 연동용)
|
|
30
|
+
* - app stream → firebaseAppId (Firebase 앱과 자동 링크된 경우)
|
|
31
|
+
*/
|
|
32
|
+
export function flattenDataStream(d) {
|
|
33
|
+
return {
|
|
34
|
+
name: d.name ?? null,
|
|
35
|
+
type: d.type ?? null,
|
|
36
|
+
displayName: d.displayName ?? null,
|
|
37
|
+
measurementId: d.webStreamData?.measurementId ?? null,
|
|
38
|
+
firebaseAppId: d.androidAppStreamData?.firebaseAppId ?? d.iosAppStreamData?.firebaseAppId ?? null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** data stream 생성 요청 본문 조립 (순수 함수 — 테스트 대상). */
|
|
42
|
+
export function buildDataStreamBody(opts) {
|
|
43
|
+
switch (opts.platform) {
|
|
44
|
+
case 'web':
|
|
45
|
+
return {
|
|
46
|
+
type: 'WEB_DATA_STREAM',
|
|
47
|
+
displayName: opts.displayName,
|
|
48
|
+
webStreamData: { defaultUri: opts.defaultUri },
|
|
49
|
+
};
|
|
50
|
+
case 'android':
|
|
51
|
+
return {
|
|
52
|
+
type: 'ANDROID_APP_DATA_STREAM',
|
|
53
|
+
displayName: opts.displayName,
|
|
54
|
+
androidAppStreamData: { packageName: opts.packageName },
|
|
55
|
+
};
|
|
56
|
+
case 'ios':
|
|
57
|
+
return {
|
|
58
|
+
type: 'IOS_APP_DATA_STREAM',
|
|
59
|
+
displayName: opts.displayName,
|
|
60
|
+
iosAppStreamData: { bundleId: opts.bundleId },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// ─── 계정/속성 디스커버리 ───
|
|
65
|
+
/** 접근 가능한 GA 계정 + 각 계정의 property 요약. accountId/propertyId 를 찾는 시작점. */
|
|
66
|
+
export async function listAccountSummaries(auth) {
|
|
67
|
+
const res = await admin().accountSummaries.list({ auth, pageSize: 200 });
|
|
68
|
+
return res.data.accountSummaries ?? [];
|
|
69
|
+
}
|
|
70
|
+
/** 계정 하위 GA4 property 목록. accountId: '123' 또는 'accounts/123'. */
|
|
71
|
+
export async function listProperties(auth, accountId) {
|
|
72
|
+
const res = await admin().properties.list({
|
|
73
|
+
auth,
|
|
74
|
+
filter: `parent:${normalizeAccountName(accountId)}`,
|
|
75
|
+
pageSize: 200,
|
|
76
|
+
});
|
|
77
|
+
return res.data.properties ?? [];
|
|
78
|
+
}
|
|
79
|
+
// ─── property / data stream 생성 (attach 핵심) ───
|
|
80
|
+
export async function createProperty(auth, opts) {
|
|
81
|
+
const res = await admin().properties.create({
|
|
82
|
+
auth,
|
|
83
|
+
requestBody: {
|
|
84
|
+
parent: normalizeAccountName(opts.accountId),
|
|
85
|
+
displayName: opts.displayName,
|
|
86
|
+
timeZone: opts.timeZone ?? 'America/Los_Angeles',
|
|
87
|
+
currencyCode: opts.currencyCode ?? 'USD',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
return res.data; // { name: 'properties/XXXX', displayName, ... }
|
|
91
|
+
}
|
|
92
|
+
export async function createDataStream(auth, propertyId, opts) {
|
|
93
|
+
const res = await admin().properties.dataStreams.create({
|
|
94
|
+
auth,
|
|
95
|
+
parent: normalizePropertyName(propertyId),
|
|
96
|
+
requestBody: buildDataStreamBody(opts),
|
|
97
|
+
});
|
|
98
|
+
return { ...flattenDataStream(res.data), raw: res.data };
|
|
99
|
+
}
|
|
100
|
+
export async function listDataStreams(auth, propertyId) {
|
|
101
|
+
const res = await admin().properties.dataStreams.list({
|
|
102
|
+
auth,
|
|
103
|
+
parent: normalizePropertyName(propertyId),
|
|
104
|
+
pageSize: 200,
|
|
105
|
+
});
|
|
106
|
+
return (res.data.dataStreams ?? []).map((d) => flattenDataStream(d));
|
|
107
|
+
}
|
|
108
|
+
export async function runReport(auth, propertyId, params) {
|
|
109
|
+
const res = await data().properties.runReport({
|
|
110
|
+
auth,
|
|
111
|
+
property: normalizePropertyName(propertyId),
|
|
112
|
+
requestBody: {
|
|
113
|
+
dateRanges: [{ startDate: params.startDate, endDate: params.endDate }],
|
|
114
|
+
dimensions: (params.dimensions ?? []).map((name) => ({ name })),
|
|
115
|
+
metrics: (params.metrics ?? ['activeUsers', 'eventCount']).map((name) => ({ name })),
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
return res.data;
|
|
119
|
+
}
|
package/dist/helpers.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { ensureFreshAccessToken };
|
|
|
8
8
|
* 기존엔 만료 시 각 OAuth 도구(firebase/admob/iam/googleads/checks)가
|
|
9
9
|
* googleapis GaxiosError 를 그대로 노출 → 사용자는 "재로그인하라"는 안내를 못 받았다.
|
|
10
10
|
*/
|
|
11
|
-
export declare function requireAuth(): Promise<import("google-auth-library").OAuth2Client>;
|
|
11
|
+
export declare function requireAuth(requiredScope?: string): Promise<import("google-auth-library").OAuth2Client>;
|
|
12
12
|
export declare const PLAY_AUTH_HINT: string;
|
|
13
13
|
export declare const APPSTORE_AUTH_HINT: string;
|
|
14
14
|
/**
|