@yoonion/mimi-seed-mcp 0.5.0 โ 0.6.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/LICENSE +75 -75
- package/README.md +241 -241
- package/dist/admob/cli.js +20 -20
- package/dist/auth/google-auth.js +7 -7
- package/dist/firebase/cli.js +27 -27
- package/dist/firebase/tools.d.ts +37 -0
- package/dist/firebase/tools.js +99 -0
- package/dist/ga4/cli.js +27 -27
- package/dist/googleads/tools.js +54 -54
- package/dist/jenkins/credentials.js +6 -29
- package/dist/jenkins/http.d.ts +22 -0
- package/dist/jenkins/http.js +67 -0
- package/dist/jenkins/jobs.d.ts +28 -0
- package/dist/jenkins/jobs.js +80 -0
- package/dist/lib/google-errors.js +21 -6
- package/dist/registers/firebase.js +37 -0
- package/dist/registers/jenkins.js +75 -0
- package/package.json +1 -1
package/dist/admob/cli.js
CHANGED
|
@@ -5,26 +5,26 @@
|
|
|
5
5
|
import { requireAuth } from '../helpers.js';
|
|
6
6
|
import * as admob from './tools.js';
|
|
7
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
|
|
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
28
|
`;
|
|
29
29
|
const AD_FORMATS = ['BANNER', 'INTERSTITIAL', 'REWARDED', 'REWARDED_INTERSTITIAL', 'APP_OPEN', 'NATIVE'];
|
|
30
30
|
runDomainCli({
|
package/dist/auth/google-auth.js
CHANGED
|
@@ -215,13 +215,13 @@ export function startAuth(clientId, clientSecret, options = {}) {
|
|
|
215
215
|
};
|
|
216
216
|
saveTokens(stored);
|
|
217
217
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
218
|
-
res.end(`
|
|
219
|
-
<html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
|
|
220
|
-
<div style="text-align:center">
|
|
221
|
-
<h1>โ
Mimi Seed ์ธ์ฆ ์๋ฃ!</h1>
|
|
222
|
-
<p>์ด ์ฐฝ์ ๋ซ๊ณ Claude Code ๋๋ Codex๋ก ๋์๊ฐ์ธ์.</p>
|
|
223
|
-
</div>
|
|
224
|
-
</body></html>
|
|
218
|
+
res.end(`
|
|
219
|
+
<html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
|
|
220
|
+
<div style="text-align:center">
|
|
221
|
+
<h1>โ
Mimi Seed ์ธ์ฆ ์๋ฃ!</h1>
|
|
222
|
+
<p>์ด ์ฐฝ์ ๋ซ๊ณ Claude Code ๋๋ Codex๋ก ๋์๊ฐ์ธ์.</p>
|
|
223
|
+
</div>
|
|
224
|
+
</body></html>
|
|
225
225
|
`);
|
|
226
226
|
server.close();
|
|
227
227
|
activeAuthServer = null;
|
package/dist/firebase/cli.js
CHANGED
|
@@ -5,33 +5,33 @@ import { requireAuth } from '../helpers.js';
|
|
|
5
5
|
import * as firebase from './tools.js';
|
|
6
6
|
import { runDomainCli, requireFlag, flag, CliUsageError } from '../lib/cli-args.js';
|
|
7
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
|
|
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
35
|
`;
|
|
36
36
|
async function listApps(projectId, platform) {
|
|
37
37
|
const auth = await requireAuth();
|
package/dist/firebase/tools.d.ts
CHANGED
|
@@ -9,6 +9,43 @@ export declare function listProjects(auth: OAuth2Client): Promise<{
|
|
|
9
9
|
projectNumber: string | null | undefined;
|
|
10
10
|
}[]>;
|
|
11
11
|
export declare function getProject(auth: OAuth2Client, projectId: string): Promise<import("googleapis").firebase_v1beta1.Schema$FirebaseProject>;
|
|
12
|
+
/** long-running Operation ํ๋์ ์๋ฃ ์ฌ๋ถ๋ฅผ ํ์ ํ๋ค. ํด๋ง ๋ฃจํ์์ ์ฌ์ฌ์ฉ + ๋จ๋
ํ
์คํธ ๊ฐ๋ฅ. */
|
|
13
|
+
export declare function operationOutcome(op: {
|
|
14
|
+
done?: boolean | null;
|
|
15
|
+
error?: {
|
|
16
|
+
message?: string | null;
|
|
17
|
+
} | null;
|
|
18
|
+
}): {
|
|
19
|
+
status: 'pending';
|
|
20
|
+
} | {
|
|
21
|
+
status: 'done';
|
|
22
|
+
} | {
|
|
23
|
+
status: 'error';
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* getOperation()์ ์๋ฃ(done)๊น์ง ํด๋งํ๋ค. getOperation() ์์ฒด๊ฐ ํ๊ธฐ๋ ๊ฒ(๋คํธ์ํฌ blip)๊ณผ
|
|
28
|
+
* operation์ด ์์ง ์ ๋๋ ๊ฒ(pending)์ ๊ตฌ๋ถํด์, ์ ์๋ ๋ช ๋ฒ ๋ ์ฐธ์์ฃผ๊ณ ํ์๋ง ์งํ ์ ํธ๋ก ๋ณธ๋ค.
|
|
29
|
+
* intervalMs/maxAttempts๋ ํ
์คํธ์์ ์ค์๊ฐ ๋๊ธฐ ์์ด ๊ฒ์ฆํ๊ธฐ ์ํ override.
|
|
30
|
+
*/
|
|
31
|
+
export declare function waitForOperation(getOperation: () => Promise<{
|
|
32
|
+
done?: boolean | null;
|
|
33
|
+
error?: {
|
|
34
|
+
message?: string | null;
|
|
35
|
+
} | null;
|
|
36
|
+
}>, label: string, opts?: {
|
|
37
|
+
intervalMs?: number;
|
|
38
|
+
maxAttempts?: number;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* ์ GCP ํ๋ก์ ํธ๋ฅผ ๋ง๋ค๊ณ Firebase๋ฅผ ์ถ๊ฐํ๋ค (์ฑ ํ๋๋น ์ ์ฉ Firebase ํ๋ก์ ํธ ์ปจ๋ฒค์
).
|
|
42
|
+
* ๋ ๋ค long-running operation์ด๋ผ ์๋ฃ๊น์ง ํด๋งํ๋ค(์ต์
์ผ์ด์ค ~2๋ถ: ๋จ๊ณ๋น ~60s ร 2๋จ๊ณ).
|
|
43
|
+
* parent ์๋ต ์ ๊ณ์ ์ ๊ธฐ๋ณธ ์ ์ฑ
๋๋ก ์์ฑ๋๋ค โ ์กฐ์ง ์์์ด ๊ฐ์ ๋ ๊ณ์ ์ parent
|
|
44
|
+
* ('organizations/<id>' ๋๋ 'folders/<id>') ์์ด ํธ์ถํ๋ฉด ์๋ฌ๊ฐ ๋๋ค.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createProject(auth: OAuth2Client, projectId: string, displayName: string, opts?: {
|
|
47
|
+
parent?: string;
|
|
48
|
+
}): Promise<import("googleapis").firebase_v1beta1.Schema$FirebaseProject>;
|
|
12
49
|
export declare function listAndroidApps(auth: OAuth2Client, projectId: string): Promise<{
|
|
13
50
|
appId: string | null | undefined;
|
|
14
51
|
packageName: string | null | undefined;
|
package/dist/firebase/tools.js
CHANGED
|
@@ -19,6 +19,105 @@ export async function getProject(auth, projectId) {
|
|
|
19
19
|
});
|
|
20
20
|
return res.data;
|
|
21
21
|
}
|
|
22
|
+
const OPERATION_POLL_INTERVAL_MS = 2000;
|
|
23
|
+
const OPERATION_MAX_ATTEMPTS = 30; // ~60s ์ํ (operation ํ๋๋น) โ ํ๋ก์ ํธ ์์ฑ + Firebase ์ถ๊ฐ ๋ ๋จ๊ณ๋ผ ์ต์
~2๋ถ
|
|
24
|
+
const OPERATION_FETCH_RETRY_LIMIT = 3; // getOperation() ์์ฒด๊ฐ ํ๊ธฐ๋ ๊ฒฝ์ฐ(๋คํธ์ํฌ blip) ํ์ฉํ ์ฐ์ ์คํจ ํ์
|
|
25
|
+
function sleep(ms) {
|
|
26
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
+
}
|
|
28
|
+
/** long-running Operation ํ๋์ ์๋ฃ ์ฌ๋ถ๋ฅผ ํ์ ํ๋ค. ํด๋ง ๋ฃจํ์์ ์ฌ์ฌ์ฉ + ๋จ๋
ํ
์คํธ ๊ฐ๋ฅ. */
|
|
29
|
+
export function operationOutcome(op) {
|
|
30
|
+
if (!op.done)
|
|
31
|
+
return { status: 'pending' };
|
|
32
|
+
if (op.error)
|
|
33
|
+
return { status: 'error', message: op.error.message ?? JSON.stringify(op.error) };
|
|
34
|
+
return { status: 'done' };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* getOperation()์ ์๋ฃ(done)๊น์ง ํด๋งํ๋ค. getOperation() ์์ฒด๊ฐ ํ๊ธฐ๋ ๊ฒ(๋คํธ์ํฌ blip)๊ณผ
|
|
38
|
+
* operation์ด ์์ง ์ ๋๋ ๊ฒ(pending)์ ๊ตฌ๋ถํด์, ์ ์๋ ๋ช ๋ฒ ๋ ์ฐธ์์ฃผ๊ณ ํ์๋ง ์งํ ์ ํธ๋ก ๋ณธ๋ค.
|
|
39
|
+
* intervalMs/maxAttempts๋ ํ
์คํธ์์ ์ค์๊ฐ ๋๊ธฐ ์์ด ๊ฒ์ฆํ๊ธฐ ์ํ override.
|
|
40
|
+
*/
|
|
41
|
+
export async function waitForOperation(getOperation, label, opts = {}) {
|
|
42
|
+
const intervalMs = opts.intervalMs ?? OPERATION_POLL_INTERVAL_MS;
|
|
43
|
+
const maxAttempts = opts.maxAttempts ?? OPERATION_MAX_ATTEMPTS;
|
|
44
|
+
let consecutiveFetchErrors = 0;
|
|
45
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
46
|
+
let op;
|
|
47
|
+
try {
|
|
48
|
+
op = await getOperation();
|
|
49
|
+
consecutiveFetchErrors = 0;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
consecutiveFetchErrors += 1;
|
|
53
|
+
if (consecutiveFetchErrors > OPERATION_FETCH_RETRY_LIMIT)
|
|
54
|
+
throw err;
|
|
55
|
+
if (attempt < maxAttempts - 1)
|
|
56
|
+
await sleep(intervalMs);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const outcome = operationOutcome(op);
|
|
60
|
+
if (outcome.status === 'error')
|
|
61
|
+
throw new Error(`${label} ์คํจ: ${outcome.message}`);
|
|
62
|
+
if (outcome.status === 'done')
|
|
63
|
+
return;
|
|
64
|
+
if (attempt < maxAttempts - 1)
|
|
65
|
+
await sleep(intervalMs);
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`${label}์ด(๊ฐ) ์๊ฐ ๋ด์ ๋๋์ง ์์์ต๋๋ค โ ๋์ค์ firebase_get_project ๋ก ์ํ๋ฅผ ํ์ธํ์ธ์.`);
|
|
68
|
+
}
|
|
69
|
+
/** ๋ฐฉ๊ธ ๋ง๋ ํ๋ก์ ํธ๋ฅผ ๊ณง๋ฐ๋ก ์กฐํํ ๋์ ์งง์ ์ ํ ์ง์ฐ(propagation lag)์ ํก์ํ๋ ์ฌ์๋. */
|
|
70
|
+
async function getProjectWithRetry(auth, projectId, attempts = 3, delayMs = 1500) {
|
|
71
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
72
|
+
try {
|
|
73
|
+
return await getProject(auth, projectId);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (attempt === attempts - 1)
|
|
77
|
+
throw err;
|
|
78
|
+
await sleep(delayMs);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new Error('unreachable');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* ์ GCP ํ๋ก์ ํธ๋ฅผ ๋ง๋ค๊ณ Firebase๋ฅผ ์ถ๊ฐํ๋ค (์ฑ ํ๋๋น ์ ์ฉ Firebase ํ๋ก์ ํธ ์ปจ๋ฒค์
).
|
|
85
|
+
* ๋ ๋ค long-running operation์ด๋ผ ์๋ฃ๊น์ง ํด๋งํ๋ค(์ต์
์ผ์ด์ค ~2๋ถ: ๋จ๊ณ๋น ~60s ร 2๋จ๊ณ).
|
|
86
|
+
* parent ์๋ต ์ ๊ณ์ ์ ๊ธฐ๋ณธ ์ ์ฑ
๋๋ก ์์ฑ๋๋ค โ ์กฐ์ง ์์์ด ๊ฐ์ ๋ ๊ณ์ ์ parent
|
|
87
|
+
* ('organizations/<id>' ๋๋ 'folders/<id>') ์์ด ํธ์ถํ๋ฉด ์๋ฌ๊ฐ ๋๋ค.
|
|
88
|
+
*/
|
|
89
|
+
export async function createProject(auth, projectId, displayName, opts = {}) {
|
|
90
|
+
const crm = google.cloudresourcemanager('v3');
|
|
91
|
+
const createRes = await crm.projects.create({
|
|
92
|
+
auth,
|
|
93
|
+
requestBody: { projectId, displayName, parent: opts.parent },
|
|
94
|
+
});
|
|
95
|
+
const createOpName = createRes.data.name;
|
|
96
|
+
if (!createOpName)
|
|
97
|
+
throw new Error('GCP ํ๋ก์ ํธ ์์ฑ ์๋ต์ operation name์ด ์์ต๋๋ค.');
|
|
98
|
+
await waitForOperation(async () => (await crm.operations.get({ auth, name: createOpName })).data, 'GCP ํ๋ก์ ํธ ์์ฑ');
|
|
99
|
+
// ์ด ์ง์ ๋ถํฐ๋ GCP ํ๋ก์ ํธ๊ฐ ์ด๋ฏธ ์กด์ฌํ๋ค โ ์๋์ ์คํจํ๋ฉด "๋ค๋ฅธ projectId๋ก ์ฌ์๋"๊ฐ
|
|
100
|
+
// ์๋๋ผ "๊ฐ์ projectId๋ก ๋ณต๊ตฌ"๋ฅผ ์๋ดํด์ผ ํ๋ค(์ ๊ทธ๋ฌ๋ฉด ๊ณ ์ ํ๋ก์ ํธ๊ฐ ๊ณ์ ์์ผ ์ ์์).
|
|
101
|
+
try {
|
|
102
|
+
const fb = google.firebase('v1beta1');
|
|
103
|
+
const addRes = await fb.projects.addFirebase({ auth, project: `projects/${projectId}` });
|
|
104
|
+
const addOpName = addRes.data.name;
|
|
105
|
+
if (!addOpName)
|
|
106
|
+
throw new Error('Firebase ์ถ๊ฐ ์๋ต์ operation name์ด ์์ต๋๋ค.');
|
|
107
|
+
await waitForOperation(async () => (await fb.operations.get({ auth, name: addOpName })).data, 'Firebase ์ถ๊ฐ');
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
if (err && typeof err === 'object') {
|
|
111
|
+
err.partialFailureNote = [
|
|
112
|
+
`โ ๏ธ GCP ํ๋ก์ ํธ \`${projectId}\`๋ ์ด๋ฏธ ์์ฑ๋์ต๋๋ค.`,
|
|
113
|
+
'โ ๋ค๋ฅธ projectId๋ก ์ฌ์๋ํ์ง ๋ง๊ณ , ์์ธ์ ํด๊ฒฐํ ๋ค ๊ฐ์ projectId๋ก ๋ค์ ์๋ํ๊ฑฐ๋',
|
|
114
|
+
`firebase_get_project("${projectId}")๋ก ์ํ๋ฅผ ํ์ธํ์ธ์.`,
|
|
115
|
+
].join(' ');
|
|
116
|
+
}
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
return getProjectWithRetry(auth, projectId);
|
|
120
|
+
}
|
|
22
121
|
// โโโ Android ์ฑ โโโ
|
|
23
122
|
export async function listAndroidApps(auth, projectId) {
|
|
24
123
|
const res = await google.firebase('v1beta1').projects.androidApps.list({
|
package/dist/ga4/cli.js
CHANGED
|
@@ -4,33 +4,33 @@
|
|
|
4
4
|
import { requireAuth } from '../helpers.js';
|
|
5
5
|
import * as ga4 from './tools.js';
|
|
6
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
|
|
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
34
|
`;
|
|
35
35
|
runDomainCli({
|
|
36
36
|
binName: 'mimi-seed-ga4',
|
package/dist/googleads/tools.js
CHANGED
|
@@ -84,20 +84,20 @@ export async function listAccessibleCustomers(auth, cfg) {
|
|
|
84
84
|
}
|
|
85
85
|
// โโโ ์บ ํ์ธ ๋ชฉ๋ก โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
86
86
|
export async function listCampaigns(auth, cfg) {
|
|
87
|
-
const query = `
|
|
88
|
-
SELECT
|
|
89
|
-
campaign.id,
|
|
90
|
-
campaign.name,
|
|
91
|
-
campaign.status,
|
|
92
|
-
campaign.advertising_channel_type,
|
|
93
|
-
campaign.advertising_channel_sub_type,
|
|
94
|
-
campaign.start_date,
|
|
95
|
-
campaign.end_date,
|
|
96
|
-
campaign_budget.amount_micros
|
|
97
|
-
FROM campaign
|
|
98
|
-
WHERE campaign.status != 'REMOVED'
|
|
99
|
-
ORDER BY campaign.name ASC
|
|
100
|
-
LIMIT 200
|
|
87
|
+
const query = `
|
|
88
|
+
SELECT
|
|
89
|
+
campaign.id,
|
|
90
|
+
campaign.name,
|
|
91
|
+
campaign.status,
|
|
92
|
+
campaign.advertising_channel_type,
|
|
93
|
+
campaign.advertising_channel_sub_type,
|
|
94
|
+
campaign.start_date,
|
|
95
|
+
campaign.end_date,
|
|
96
|
+
campaign_budget.amount_micros
|
|
97
|
+
FROM campaign
|
|
98
|
+
WHERE campaign.status != 'REMOVED'
|
|
99
|
+
ORDER BY campaign.name ASC
|
|
100
|
+
LIMIT 200
|
|
101
101
|
`;
|
|
102
102
|
const rows = await search(auth, cfg, query);
|
|
103
103
|
return rows.map((r) => ({
|
|
@@ -113,26 +113,26 @@ export async function listCampaigns(auth, cfg) {
|
|
|
113
113
|
}
|
|
114
114
|
// โโโ ์บ ํ์ธ ์ฑ๊ณผ ๋ฆฌํฌํธ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
115
115
|
export async function getCampaignReport(auth, cfg, range) {
|
|
116
|
-
const query = `
|
|
117
|
-
SELECT
|
|
118
|
-
campaign.id,
|
|
119
|
-
campaign.name,
|
|
120
|
-
campaign.status,
|
|
121
|
-
campaign.advertising_channel_type,
|
|
122
|
-
campaign.advertising_channel_sub_type,
|
|
123
|
-
metrics.clicks,
|
|
124
|
-
metrics.impressions,
|
|
125
|
-
metrics.cost_micros,
|
|
126
|
-
metrics.conversions,
|
|
127
|
-
metrics.conversions_value,
|
|
128
|
-
metrics.cost_per_conversion,
|
|
129
|
-
metrics.ctr,
|
|
130
|
-
metrics.average_cpc
|
|
131
|
-
FROM campaign
|
|
132
|
-
WHERE campaign.status != 'REMOVED'
|
|
133
|
-
AND segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'
|
|
134
|
-
ORDER BY metrics.cost_micros DESC
|
|
135
|
-
LIMIT 500
|
|
116
|
+
const query = `
|
|
117
|
+
SELECT
|
|
118
|
+
campaign.id,
|
|
119
|
+
campaign.name,
|
|
120
|
+
campaign.status,
|
|
121
|
+
campaign.advertising_channel_type,
|
|
122
|
+
campaign.advertising_channel_sub_type,
|
|
123
|
+
metrics.clicks,
|
|
124
|
+
metrics.impressions,
|
|
125
|
+
metrics.cost_micros,
|
|
126
|
+
metrics.conversions,
|
|
127
|
+
metrics.conversions_value,
|
|
128
|
+
metrics.cost_per_conversion,
|
|
129
|
+
metrics.ctr,
|
|
130
|
+
metrics.average_cpc
|
|
131
|
+
FROM campaign
|
|
132
|
+
WHERE campaign.status != 'REMOVED'
|
|
133
|
+
AND segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'
|
|
134
|
+
ORDER BY metrics.cost_micros DESC
|
|
135
|
+
LIMIT 500
|
|
136
136
|
`;
|
|
137
137
|
const rows = await search(auth, cfg, query);
|
|
138
138
|
return rows.map((r) => ({
|
|
@@ -153,26 +153,26 @@ export async function getCampaignReport(auth, cfg, range) {
|
|
|
153
153
|
}
|
|
154
154
|
// โโโ UAC(์ฑ ์บ ํ์ธ) ๋ฆฌํฌํธ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
155
155
|
export async function getUacReport(auth, cfg, range) {
|
|
156
|
-
const query = `
|
|
157
|
-
SELECT
|
|
158
|
-
campaign.id,
|
|
159
|
-
campaign.name,
|
|
160
|
-
campaign.status,
|
|
161
|
-
campaign.advertising_channel_sub_type,
|
|
162
|
-
metrics.clicks,
|
|
163
|
-
metrics.impressions,
|
|
164
|
-
metrics.cost_micros,
|
|
165
|
-
metrics.conversions,
|
|
166
|
-
metrics.conversions_value,
|
|
167
|
-
metrics.cost_per_conversion,
|
|
168
|
-
metrics.ctr,
|
|
169
|
-
segments.date
|
|
170
|
-
FROM campaign
|
|
171
|
-
WHERE campaign.advertising_channel_type = 'MULTI_CHANNEL'
|
|
172
|
-
AND campaign.advertising_channel_sub_type IN ('APP_CAMPAIGN', 'APP_CAMPAIGN_FOR_ENGAGEMENT', 'APP_CAMPAIGN_FOR_PRE_REGISTRATION')
|
|
173
|
-
AND segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'
|
|
174
|
-
ORDER BY segments.date DESC, metrics.cost_micros DESC
|
|
175
|
-
LIMIT 500
|
|
156
|
+
const query = `
|
|
157
|
+
SELECT
|
|
158
|
+
campaign.id,
|
|
159
|
+
campaign.name,
|
|
160
|
+
campaign.status,
|
|
161
|
+
campaign.advertising_channel_sub_type,
|
|
162
|
+
metrics.clicks,
|
|
163
|
+
metrics.impressions,
|
|
164
|
+
metrics.cost_micros,
|
|
165
|
+
metrics.conversions,
|
|
166
|
+
metrics.conversions_value,
|
|
167
|
+
metrics.cost_per_conversion,
|
|
168
|
+
metrics.ctr,
|
|
169
|
+
segments.date
|
|
170
|
+
FROM campaign
|
|
171
|
+
WHERE campaign.advertising_channel_type = 'MULTI_CHANNEL'
|
|
172
|
+
AND campaign.advertising_channel_sub_type IN ('APP_CAMPAIGN', 'APP_CAMPAIGN_FOR_ENGAGEMENT', 'APP_CAMPAIGN_FOR_PRE_REGISTRATION')
|
|
173
|
+
AND segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'
|
|
174
|
+
ORDER BY segments.date DESC, metrics.cost_micros DESC
|
|
175
|
+
LIMIT 500
|
|
176
176
|
`;
|
|
177
177
|
const rows = await search(auth, cfg, query);
|
|
178
178
|
const byId = new Map();
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
+
import { authHeaders, getCrumb } from './http.js';
|
|
1
2
|
const FILE_CLASS = 'org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl';
|
|
2
3
|
const TEXT_CLASS = 'org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl';
|
|
3
|
-
function basicAuth(username, token) {
|
|
4
|
-
return 'Basic ' + Buffer.from(`${username}:${token}`).toString('base64');
|
|
5
|
-
}
|
|
6
4
|
// ๋๋ฉ์ธ(_ = ์ ์ญ) ๋ ๋ฒจ โ ๋ชฉ๋ก ์กฐํ / createCredentials ์ ๋ฒ ์ด์ค
|
|
7
5
|
function storeBase(url) {
|
|
8
6
|
return `${url.replace(/\/$/, '')}/credentials/store/system/domain/_`;
|
|
@@ -11,36 +9,15 @@ function storeBase(url) {
|
|
|
11
9
|
function credentialBase(url, id) {
|
|
12
10
|
return `${storeBase(url)}/credential/${encodeURIComponent(id)}`;
|
|
13
11
|
}
|
|
14
|
-
/**
|
|
15
|
-
* CSRF crumb (best-effort). crumb issuer ๋นํ์ฑ์ด๊ฑฐ๋ API ํ ํฐ์ผ๋ก ๋ฉด์ ๋๋ฉด ๋น ๊ฐ์ฒด.
|
|
16
|
-
* ๊ตฌ๋ฒ์ Jenkins / ๋น๋ฐ๋ฒํธ ์ธ์ฆ ํ๊ฒฝ์์ POST 403 ๋ฐฉ์ง.
|
|
17
|
-
*/
|
|
18
|
-
async function getCrumb(cfg) {
|
|
19
|
-
try {
|
|
20
|
-
const res = await fetch(`${cfg.url.replace(/\/$/, '')}/crumbIssuer/api/json`, {
|
|
21
|
-
headers: { Authorization: basicAuth(cfg.username, cfg.token) },
|
|
22
|
-
});
|
|
23
|
-
if (!res.ok)
|
|
24
|
-
return {};
|
|
25
|
-
const data = (await res.json());
|
|
26
|
-
if (data.crumbRequestField && data.crumb) {
|
|
27
|
-
return { [data.crumbRequestField]: data.crumb };
|
|
28
|
-
}
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return {};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
12
|
async function credentialExists(cfg, id) {
|
|
36
13
|
const res = await fetch(`${credentialBase(cfg.url, id)}/api/json`, {
|
|
37
|
-
headers:
|
|
14
|
+
headers: authHeaders(cfg),
|
|
38
15
|
});
|
|
39
16
|
return res.ok;
|
|
40
17
|
}
|
|
41
18
|
export async function listCredentials(cfg) {
|
|
42
19
|
const res = await fetch(`${storeBase(cfg.url)}/api/json?depth=1`, {
|
|
43
|
-
headers:
|
|
20
|
+
headers: authHeaders(cfg),
|
|
44
21
|
});
|
|
45
22
|
if (!res.ok)
|
|
46
23
|
throw new Error(`Jenkins credentials ์กฐํ ์คํจ (${res.status})`);
|
|
@@ -70,7 +47,7 @@ export async function upsertSecretText(cfg, id, secret, description = '') {
|
|
|
70
47
|
const res = await fetch(endpoint, {
|
|
71
48
|
method: 'POST',
|
|
72
49
|
headers: {
|
|
73
|
-
|
|
50
|
+
...authHeaders(cfg),
|
|
74
51
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
75
52
|
...crumb,
|
|
76
53
|
},
|
|
@@ -109,7 +86,7 @@ export async function upsertSecretFile(cfg, id, fileBase64, fileName, descriptio
|
|
|
109
86
|
const res = await fetch(endpoint, {
|
|
110
87
|
method: 'POST',
|
|
111
88
|
headers: {
|
|
112
|
-
|
|
89
|
+
...authHeaders(cfg),
|
|
113
90
|
...crumb,
|
|
114
91
|
},
|
|
115
92
|
body: form,
|
|
@@ -124,7 +101,7 @@ export async function deleteCredential(cfg, id) {
|
|
|
124
101
|
const res = await fetch(`${credentialBase(cfg.url, id)}/doDelete`, {
|
|
125
102
|
method: 'POST',
|
|
126
103
|
headers: {
|
|
127
|
-
|
|
104
|
+
...authHeaders(cfg),
|
|
128
105
|
...crumb,
|
|
129
106
|
},
|
|
130
107
|
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { JenkinsConfig } from './config.js';
|
|
2
|
+
export declare function basicAuth(username: string, token: string): string;
|
|
3
|
+
export declare function baseUrl(cfg: JenkinsConfig): string;
|
|
4
|
+
export declare function authHeaders(cfg: JenkinsConfig): Record<string, string>;
|
|
5
|
+
/**
|
|
6
|
+
* CSRF crumb (best-effort). crumb issuer ๋นํ์ฑ์ด๊ฑฐ๋ API ํ ํฐ์ผ๋ก ๋ฉด์ ๋๋ฉด ๋น ๊ฐ์ฒด.
|
|
7
|
+
* ๊ตฌ๋ฒ์ Jenkins / ๋น๋ฐ๋ฒํธ ์ธ์ฆ ํ๊ฒฝ์์ POST 403 ๋ฐฉ์ง.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getCrumb(cfg: JenkinsConfig): Promise<Record<string, string>>;
|
|
10
|
+
/**
|
|
11
|
+
* ์ก ๊ฒฝ๋ก๋ฅผ Jenkins URL ๋ก ๋ฐ๊พผ๋ค. ํด๋๋ `/` ๋ก ๊ตฌ๋ถํ๋ค.
|
|
12
|
+
* "penguinrun" -> <base>/job/penguinrun
|
|
13
|
+
* "vir-game/client" -> <base>/job/vir-game/job/client
|
|
14
|
+
*/
|
|
15
|
+
export declare function jobUrl(cfg: JenkinsConfig, jobPath: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* createItem ์ "๋ถ๋ชจ ์ปจํ
์ด๋" ์ POST ํ๊ณ leaf ์ด๋ฆ์ ์ฟผ๋ฆฌ๋ก ๋๊ธด๋ค.
|
|
18
|
+
* ๋ฃจํธ ์ก์ด๋ฉด ๋ถ๋ชจ๊ฐ Jenkins ๋ฃจํธ, ํด๋ ์์ด๋ฉด ํด๋ URL.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createItemUrl(cfg: JenkinsConfig, jobPath: string): string;
|
|
21
|
+
/** ์คํจ ์๋ต์ ๋ณธ๋ฌธ ์ผ๋ถ๋ฅผ ๋ถ์ฌ ์๋ฌ๋ฅผ ๋ง๋ ๋ค. Jenkins ๋ ์์ธ์ ๋ณธ๋ฌธ์๋ง ๋ด๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค. */
|
|
22
|
+
export declare function jenkinsError(prefix: string, res: Response): Promise<Error>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function basicAuth(username, token) {
|
|
2
|
+
return 'Basic ' + Buffer.from(`${username}:${token}`).toString('base64');
|
|
3
|
+
}
|
|
4
|
+
export function baseUrl(cfg) {
|
|
5
|
+
return cfg.url.replace(/\/$/, '');
|
|
6
|
+
}
|
|
7
|
+
export function authHeaders(cfg) {
|
|
8
|
+
return { Authorization: basicAuth(cfg.username, cfg.token) };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* CSRF crumb (best-effort). crumb issuer ๋นํ์ฑ์ด๊ฑฐ๋ API ํ ํฐ์ผ๋ก ๋ฉด์ ๋๋ฉด ๋น ๊ฐ์ฒด.
|
|
12
|
+
* ๊ตฌ๋ฒ์ Jenkins / ๋น๋ฐ๋ฒํธ ์ธ์ฆ ํ๊ฒฝ์์ POST 403 ๋ฐฉ์ง.
|
|
13
|
+
*/
|
|
14
|
+
export async function getCrumb(cfg) {
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(`${baseUrl(cfg)}/crumbIssuer/api/json`, {
|
|
17
|
+
headers: authHeaders(cfg),
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok)
|
|
20
|
+
return {};
|
|
21
|
+
const data = (await res.json());
|
|
22
|
+
if (data.crumbRequestField && data.crumb) {
|
|
23
|
+
return { [data.crumbRequestField]: data.crumb };
|
|
24
|
+
}
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* ์ก ๊ฒฝ๋ก๋ฅผ Jenkins URL ๋ก ๋ฐ๊พผ๋ค. ํด๋๋ `/` ๋ก ๊ตฌ๋ถํ๋ค.
|
|
33
|
+
* "penguinrun" -> <base>/job/penguinrun
|
|
34
|
+
* "vir-game/client" -> <base>/job/vir-game/job/client
|
|
35
|
+
*/
|
|
36
|
+
export function jobUrl(cfg, jobPath) {
|
|
37
|
+
const segments = jobPath.split('/').filter(Boolean);
|
|
38
|
+
if (segments.length === 0)
|
|
39
|
+
throw new Error('์ก ์ด๋ฆ์ด ๋น์ด ์์ต๋๋ค.');
|
|
40
|
+
const suffix = segments.map((s) => `job/${encodeURIComponent(s)}`).join('/');
|
|
41
|
+
return `${baseUrl(cfg)}/${suffix}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* createItem ์ "๋ถ๋ชจ ์ปจํ
์ด๋" ์ POST ํ๊ณ leaf ์ด๋ฆ์ ์ฟผ๋ฆฌ๋ก ๋๊ธด๋ค.
|
|
45
|
+
* ๋ฃจํธ ์ก์ด๋ฉด ๋ถ๋ชจ๊ฐ Jenkins ๋ฃจํธ, ํด๋ ์์ด๋ฉด ํด๋ URL.
|
|
46
|
+
*/
|
|
47
|
+
export function createItemUrl(cfg, jobPath) {
|
|
48
|
+
const segments = jobPath.split('/').filter(Boolean);
|
|
49
|
+
if (segments.length === 0)
|
|
50
|
+
throw new Error('์ก ์ด๋ฆ์ด ๋น์ด ์์ต๋๋ค.');
|
|
51
|
+
const leaf = segments.pop();
|
|
52
|
+
const parent = segments.length
|
|
53
|
+
? `${baseUrl(cfg)}/${segments.map((s) => `job/${encodeURIComponent(s)}`).join('/')}`
|
|
54
|
+
: baseUrl(cfg);
|
|
55
|
+
return `${parent}/createItem?name=${encodeURIComponent(leaf)}`;
|
|
56
|
+
}
|
|
57
|
+
/** ์คํจ ์๋ต์ ๋ณธ๋ฌธ ์ผ๋ถ๋ฅผ ๋ถ์ฌ ์๋ฌ๋ฅผ ๋ง๋ ๋ค. Jenkins ๋ ์์ธ์ ๋ณธ๋ฌธ์๋ง ๋ด๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค. */
|
|
58
|
+
export async function jenkinsError(prefix, res) {
|
|
59
|
+
let detail = '';
|
|
60
|
+
try {
|
|
61
|
+
detail = (await res.text()).trim().slice(0, 300);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// ๋ณธ๋ฌธ ์์
|
|
65
|
+
}
|
|
66
|
+
return new Error(`${prefix} (${res.status})${detail ? `: ${detail}` : ''}`);
|
|
67
|
+
}
|