@yoonion/mimi-seed-mcp 0.5.0 โ†’ 0.6.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.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({
@@ -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;
@@ -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/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',
@@ -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: { Authorization: basicAuth(cfg.username, cfg.token) },
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: { Authorization: basicAuth(cfg.username, cfg.token) },
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
- Authorization: basicAuth(cfg.username, cfg.token),
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
- Authorization: basicAuth(cfg.username, cfg.token),
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
- Authorization: basicAuth(cfg.username, cfg.token),
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
+ }
@@ -0,0 +1,28 @@
1
+ import type { JenkinsConfig } from './config.js';
2
+ export interface JenkinsJobSummary {
3
+ name: string;
4
+ url: string;
5
+ /** ์žก ์ƒํƒœ ์ƒ‰์ƒ. ํด๋”๋Š” ์ƒ‰์ด ์—†๋‹ค. */
6
+ color?: string;
7
+ }
8
+ /**
9
+ * ์žก ๋ชฉ๋ก. folder ๋ฅผ ์ฃผ๋ฉด ๊ทธ ํด๋” ์•ˆ์„, ์—†์œผ๋ฉด ๋ฃจํŠธ๋ฅผ ์กฐํšŒํ•œ๋‹ค.
10
+ * ์žฌ๊ท€ํ•˜์ง€ ์•Š๋Š”๋‹ค (ํ•œ ๋‹จ๊ณ„๋งŒ).
11
+ */
12
+ export declare function listJobs(cfg: JenkinsConfig, folder?: string): Promise<JenkinsJobSummary[]>;
13
+ /**
14
+ * 404 ๋งŒ "์—†์Œ" ์œผ๋กœ ๋ณธ๋‹ค. 401/403/500 ์„ ์—†์Œ์œผ๋กœ ์‚ผํ‚ค๋ฉด upsertJob ์ด ์—‰๋šฑํ•˜๊ฒŒ
15
+ * ์ƒ์„ฑ ๋ถ„๊ธฐ๋ฅผ ํƒ€๋ฏ€๋กœ, ๊ทธ ๊ฒฝ์šฐ๋Š” ์›์ธ์„ ๋“œ๋Ÿฌ๋‚ด๋ฉฐ throw ํ•œ๋‹ค.
16
+ */
17
+ export declare function jobExists(cfg: JenkinsConfig, jobPath: string): Promise<boolean>;
18
+ /** ์žก์˜ config.xml ์›๋ฌธ. ๋ฐฑ์—…ํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ • ์ „ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ํ™•์ธํ•  ๋•Œ ์“ด๋‹ค. */
19
+ export declare function getJobConfig(cfg: JenkinsConfig, jobPath: string): Promise<string>;
20
+ /**
21
+ * ์ƒˆ ์žก ์ƒ์„ฑ. ๊ฐ™์€ ์ด๋ฆ„์ด ์ด๋ฏธ ์žˆ์œผ๋ฉด Jenkins ๊ฐ€ 400 ์„ ์ค€๋‹ค.
22
+ * ํด๋” ์•ˆ์— ๋งŒ๋“ค๋ ค๋ฉด jobPath ์— "folder/name" ์„ ๋„˜๊ธด๋‹ค (ํด๋”๋Š” ๋ฏธ๋ฆฌ ์กด์žฌํ•ด์•ผ ํ•จ).
23
+ */
24
+ export declare function createJob(cfg: JenkinsConfig, jobPath: string, configXml: string): Promise<void>;
25
+ /** ๊ธฐ์กด ์žก์˜ config.xml ๊ต์ฒด. ์žก์ด ์—†์œผ๋ฉด 404. */
26
+ export declare function updateJob(cfg: JenkinsConfig, jobPath: string, configXml: string): Promise<void>;
27
+ /** ์žˆ์œผ๋ฉด ์ˆ˜์ •, ์—†์œผ๋ฉด ์ƒ์„ฑ. Jenkinsfile/job-config.xml ์„ ๋ ˆํฌ์—์„œ ๋ฐ€์–ด๋„ฃ์„ ๋•Œ ์“ด๋‹ค. */
28
+ export declare function upsertJob(cfg: JenkinsConfig, jobPath: string, configXml: string): Promise<'created' | 'updated'>;
@@ -0,0 +1,80 @@
1
+ import { authHeaders, baseUrl, createItemUrl, getCrumb, jenkinsError, jobUrl } from './http.js';
2
+ /**
3
+ * ์žก ๋ชฉ๋ก. folder ๋ฅผ ์ฃผ๋ฉด ๊ทธ ํด๋” ์•ˆ์„, ์—†์œผ๋ฉด ๋ฃจํŠธ๋ฅผ ์กฐํšŒํ•œ๋‹ค.
4
+ * ์žฌ๊ท€ํ•˜์ง€ ์•Š๋Š”๋‹ค (ํ•œ ๋‹จ๊ณ„๋งŒ).
5
+ */
6
+ export async function listJobs(cfg, folder) {
7
+ const root = folder ? jobUrl(cfg, folder) : baseUrl(cfg);
8
+ const res = await fetch(`${root}/api/json?tree=jobs[name,url,color]`, {
9
+ headers: authHeaders(cfg),
10
+ });
11
+ if (!res.ok)
12
+ throw await jenkinsError('Jenkins ์žก ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ', res);
13
+ const data = (await res.json());
14
+ return data.jobs ?? [];
15
+ }
16
+ /**
17
+ * 404 ๋งŒ "์—†์Œ" ์œผ๋กœ ๋ณธ๋‹ค. 401/403/500 ์„ ์—†์Œ์œผ๋กœ ์‚ผํ‚ค๋ฉด upsertJob ์ด ์—‰๋šฑํ•˜๊ฒŒ
18
+ * ์ƒ์„ฑ ๋ถ„๊ธฐ๋ฅผ ํƒ€๋ฏ€๋กœ, ๊ทธ ๊ฒฝ์šฐ๋Š” ์›์ธ์„ ๋“œ๋Ÿฌ๋‚ด๋ฉฐ throw ํ•œ๋‹ค.
19
+ */
20
+ export async function jobExists(cfg, jobPath) {
21
+ const res = await fetch(`${jobUrl(cfg, jobPath)}/api/json?tree=name`, {
22
+ headers: authHeaders(cfg),
23
+ });
24
+ if (res.ok)
25
+ return true;
26
+ if (res.status === 404)
27
+ return false;
28
+ throw await jenkinsError(`Jenkins ์žก ์กด์žฌ ํ™•์ธ ์‹คํŒจ: ${jobPath}`, res);
29
+ }
30
+ /** ์žก์˜ config.xml ์›๋ฌธ. ๋ฐฑ์—…ํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ • ์ „ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ํ™•์ธํ•  ๋•Œ ์“ด๋‹ค. */
31
+ export async function getJobConfig(cfg, jobPath) {
32
+ const res = await fetch(`${jobUrl(cfg, jobPath)}/config.xml`, {
33
+ headers: authHeaders(cfg),
34
+ });
35
+ if (!res.ok)
36
+ throw await jenkinsError(`Jenkins ์žก config ์กฐํšŒ ์‹คํŒจ: ${jobPath}`, res);
37
+ return res.text();
38
+ }
39
+ /**
40
+ * ์ƒˆ ์žก ์ƒ์„ฑ. ๊ฐ™์€ ์ด๋ฆ„์ด ์ด๋ฏธ ์žˆ์œผ๋ฉด Jenkins ๊ฐ€ 400 ์„ ์ค€๋‹ค.
41
+ * ํด๋” ์•ˆ์— ๋งŒ๋“ค๋ ค๋ฉด jobPath ์— "folder/name" ์„ ๋„˜๊ธด๋‹ค (ํด๋”๋Š” ๋ฏธ๋ฆฌ ์กด์žฌํ•ด์•ผ ํ•จ).
42
+ */
43
+ export async function createJob(cfg, jobPath, configXml) {
44
+ const crumb = await getCrumb(cfg);
45
+ const res = await fetch(createItemUrl(cfg, jobPath), {
46
+ method: 'POST',
47
+ headers: {
48
+ ...authHeaders(cfg),
49
+ 'Content-Type': 'application/xml',
50
+ ...crumb,
51
+ },
52
+ body: configXml,
53
+ });
54
+ if (!res.ok)
55
+ throw await jenkinsError(`Jenkins ์žก ์ƒ์„ฑ ์‹คํŒจ: ${jobPath}`, res);
56
+ }
57
+ /** ๊ธฐ์กด ์žก์˜ config.xml ๊ต์ฒด. ์žก์ด ์—†์œผ๋ฉด 404. */
58
+ export async function updateJob(cfg, jobPath, configXml) {
59
+ const crumb = await getCrumb(cfg);
60
+ const res = await fetch(`${jobUrl(cfg, jobPath)}/config.xml`, {
61
+ method: 'POST',
62
+ headers: {
63
+ ...authHeaders(cfg),
64
+ 'Content-Type': 'application/xml',
65
+ ...crumb,
66
+ },
67
+ body: configXml,
68
+ });
69
+ if (!res.ok)
70
+ throw await jenkinsError(`Jenkins ์žก ์ˆ˜์ • ์‹คํŒจ: ${jobPath}`, res);
71
+ }
72
+ /** ์žˆ์œผ๋ฉด ์ˆ˜์ •, ์—†์œผ๋ฉด ์ƒ์„ฑ. Jenkinsfile/job-config.xml ์„ ๋ ˆํฌ์—์„œ ๋ฐ€์–ด๋„ฃ์„ ๋•Œ ์“ด๋‹ค. */
73
+ export async function upsertJob(cfg, jobPath, configXml) {
74
+ if (await jobExists(cfg, jobPath)) {
75
+ await updateJob(cfg, jobPath, configXml);
76
+ return 'updated';
77
+ }
78
+ await createJob(cfg, jobPath, configXml);
79
+ return 'created';
80
+ }