magic-builder 1.2.0 → 1.3.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/README.md +67 -1
- package/bin/magic-builder.js +1 -0
- package/package.json +5 -2
- package/src/commands/config.js +3 -1
- package/src/commands/doc.js +29 -6
- package/src/commands/extract-cookie.js +33 -2
- package/src/commands/page.js +108 -2
- package/src/commands/performance.js +367 -22
- package/src/commands/skill.js +8 -4
- package/src/commands/widget-publish.js +339 -0
- package/src/index.js +1 -0
- package/src/lib/config.js +28 -1
- package/src/lib/help.js +105 -9
- package/src/lib/multipart.js +3 -0
|
@@ -3,26 +3,82 @@
|
|
|
3
3
|
const { existsSync, readFileSync } = require('fs');
|
|
4
4
|
const { resolve } = require('path');
|
|
5
5
|
const { success, fail } = require('../lib/output');
|
|
6
|
+
const { getPerformanceConfigPath, loadPerformanceConfig } = require('../lib/config');
|
|
6
7
|
|
|
7
8
|
const DEFAULTS = {
|
|
8
9
|
endpoint: 'https://people.bytedance.net/perf/api/foundation/v2/draft',
|
|
10
|
+
submitEndpoint: 'https://people.bytedance.net/perf/api/review/v2/stage',
|
|
9
11
|
settingsEndpoint: 'https://people.bytedance.net/perf/api/user/settings',
|
|
10
12
|
draftEndpoint: 'https://people.bytedance.net/perf/api/foundation/draft',
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
async function run(args, opts) {
|
|
14
|
-
|
|
16
|
+
const action = args[0] || 'draft';
|
|
17
|
+
if (args.length > 1 || !['draft', 'submit', 'self-review', 'key-works', 'review-users', 'invite-review'].includes(action)) {
|
|
18
|
+
fail('Usage: magic-builder performance [submit|self-review|key-works|review-users|invite-review] --review-url <url> --cookie <content|file>', 'E_INVALID_ARGS');
|
|
19
|
+
}
|
|
15
20
|
|
|
16
21
|
const cookie = readCookie(opts.cookie, opts.cookieFile);
|
|
17
22
|
if (!cookie) fail('Missing Cookie. Use --cookie <content|file> or --cookie-file <file>.', 'E_INVALID_ARGS');
|
|
18
23
|
|
|
19
|
-
const markdown = await readMarkdown(opts.markdown, opts.markdownFile);
|
|
20
|
-
if (!markdown.trim()) fail('Missing Markdown. Use --markdown <content|file|url>, --markdown-file <file>, or stdin.', 'E_INVALID_ARGS');
|
|
21
|
-
|
|
22
24
|
const csrf = getCookie(cookie, 'x-f-csrf');
|
|
23
25
|
if (!csrf) fail('Cookie does not contain x-f-csrf.', 'E_INVALID_ARGS');
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
if (action === 'key-works') {
|
|
28
|
+
const data = await getKeyWorks(opts, cookie, csrf);
|
|
29
|
+
success(data, opts);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (action === 'review-users') {
|
|
33
|
+
const data = await getReviewUsers(opts, cookie, csrf);
|
|
34
|
+
success(data, opts);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (action === 'invite-review') {
|
|
38
|
+
const payload = readInviteReviewPayload(opts);
|
|
39
|
+
const endpoint = String(opts.endpoint || DEFAULTS.endpoint);
|
|
40
|
+
if (opts.dryRun) {
|
|
41
|
+
success({ dryRun: true, action: 'invite-review', endpoint, payload }, opts);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const review = parseReviewLocation(opts.reviewUrl || opts.referer);
|
|
45
|
+
const data = await postPerformance(endpoint, payload, cookie, csrf, {
|
|
46
|
+
referer: String(opts.referer || review.referer || 'https://people.bytedance.net/performance/perf/review'),
|
|
47
|
+
tenantId: String(opts.tenantId || ''),
|
|
48
|
+
});
|
|
49
|
+
success(data, opts);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const config = await resolveConfig(opts, cookie, action === 'self-review' ? 'self_review' : 'confirm_invitation');
|
|
54
|
+
if (action === 'self-review') {
|
|
55
|
+
const updates = readSelfReviewInputs(opts);
|
|
56
|
+
const payload = buildSelfReviewPayloadFromDraft(config.draft, updates);
|
|
57
|
+
if (opts.dryRun) {
|
|
58
|
+
success({ dryRun: true, action: 'self-review', endpoint: config.endpoint, payload }, opts);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const data = await postPerformance(config.endpoint, payload, cookie, csrf, config);
|
|
62
|
+
success(data, opts);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (action === 'submit') {
|
|
66
|
+
const payload = buildStagePayloadFromDraft(config.draft, config);
|
|
67
|
+
if (opts.dryRun) {
|
|
68
|
+
success({ dryRun: true, action: 'submit', endpoint: config.submitEndpoint, payload }, opts);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!opts.yes) {
|
|
72
|
+
fail('Formal performance submission requires --yes. Run with --dry-run first to review the payload.', 'E_CONFIRM_REQUIRED');
|
|
73
|
+
}
|
|
74
|
+
const data = await postPerformance(config.submitEndpoint, payload, cookie, csrf, config);
|
|
75
|
+
success(data, opts);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const markdown = await readMarkdown(opts.markdown, opts.markdownFile);
|
|
80
|
+
if (!markdown.trim()) fail('Missing Markdown. Use --markdown <content|file|url>, --markdown-file <file>, or stdin.', 'E_INVALID_ARGS');
|
|
81
|
+
|
|
26
82
|
const payload = config.draft
|
|
27
83
|
? buildPayloadFromDraft(markdown, config.draft, config)
|
|
28
84
|
: buildPayload(markdown, config);
|
|
@@ -31,7 +87,143 @@ async function run(args, opts) {
|
|
|
31
87
|
return;
|
|
32
88
|
}
|
|
33
89
|
|
|
34
|
-
const
|
|
90
|
+
const data = await postPerformance(config.endpoint, payload, cookie, csrf, config);
|
|
91
|
+
success(data, opts);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readInviteReviewPayload(opts) {
|
|
95
|
+
if (opts.payload && opts.payloadFile) {
|
|
96
|
+
fail('Pass either --payload or --payload-file, not both.', 'E_INVALID_ARGS');
|
|
97
|
+
}
|
|
98
|
+
const input = opts.payloadFile
|
|
99
|
+
? readFileSync(resolve(String(opts.payloadFile)), 'utf8')
|
|
100
|
+
: String(opts.payload || '');
|
|
101
|
+
if (!input.trim()) fail('invite-review requires --payload <json> or --payload-file <file>.', 'E_INVALID_ARGS');
|
|
102
|
+
const payload = parseJson(input);
|
|
103
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
104
|
+
fail('Invite-review payload must be a valid JSON object.', 'E_INVALID_ARGS');
|
|
105
|
+
}
|
|
106
|
+
const missing = ['key', 'data', 'version'].filter(key => payload[key] === undefined || payload[key] === null);
|
|
107
|
+
if (missing.length) fail(`Invite-review payload is missing ${missing.join(', ')}.`, 'E_INVALID_ARGS');
|
|
108
|
+
if (!String(payload.key).includes('__invite_review__')) {
|
|
109
|
+
fail('Invite-review payload key must contain __invite_review__.', 'E_INVALID_ARGS');
|
|
110
|
+
}
|
|
111
|
+
return normalizePerformancePayload(payload);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizePerformancePayload(payload) {
|
|
115
|
+
const normalized = structuredClone(payload);
|
|
116
|
+
normalizePerformanceNode(normalized);
|
|
117
|
+
return normalized;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizePerformanceNode(node) {
|
|
121
|
+
if (!node || typeof node !== 'object') return;
|
|
122
|
+
if (Array.isArray(node)) {
|
|
123
|
+
node.forEach(normalizePerformanceNode);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const [key, value] of Object.entries(node)) {
|
|
127
|
+
if (key === 'insert' && typeof value === 'string') {
|
|
128
|
+
node[key] = value.replace(/\\n(?=\r?\n|$)/g, '\n');
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (typeof value === 'string' && ['text', 'value', 'rich_text'].includes(key)) {
|
|
132
|
+
const embedded = parseJson(value);
|
|
133
|
+
if (embedded && typeof embedded === 'object') {
|
|
134
|
+
normalizePerformanceNode(embedded);
|
|
135
|
+
node[key] = JSON.stringify(embedded);
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
normalizePerformanceNode(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function getKeyWorks(opts, cookie, csrf) {
|
|
144
|
+
if (!opts.rootReviewId) {
|
|
145
|
+
fail('key-works requires --root-review-id <id>.', 'E_INVALID_ARGS');
|
|
146
|
+
}
|
|
147
|
+
const personalConfig = opts.performanceConfig || loadPerformanceConfig();
|
|
148
|
+
const templateId = String(opts.templateId || personalConfig.invite_review_template_id || personalConfig.inviteReviewTemplateId || '');
|
|
149
|
+
if (!templateId) {
|
|
150
|
+
fail(`Missing template id. Use --template-id <id> or set invite_review_template_id in ${getPerformanceConfigPath()}.`, 'E_INVALID_ARGS');
|
|
151
|
+
}
|
|
152
|
+
const endpoint = assertPeoplePerfEndpoint(opts.stageEndpoint || DEFAULTS.submitEndpoint);
|
|
153
|
+
endpoint.searchParams.set('template_id', templateId);
|
|
154
|
+
endpoint.searchParams.set('root_review_id', String(opts.rootReviewId));
|
|
155
|
+
const referer = String(opts.referer || opts.reviewUrl || 'https://people.bytedance.net/performance/perf/review');
|
|
156
|
+
const headers = {
|
|
157
|
+
accept: 'application/json, text/plain, */*',
|
|
158
|
+
'accept-language': 'zh-CN,zh;q=0.9',
|
|
159
|
+
cookie,
|
|
160
|
+
referer,
|
|
161
|
+
'x-f-csrf': csrf,
|
|
162
|
+
'x-f-lang': 'zh-CN',
|
|
163
|
+
'x-f-timezone': 'Asia/Shanghai',
|
|
164
|
+
};
|
|
165
|
+
if (opts.tenantId) headers['rpc-persist-lane-c-perfx-tenant-id'] = String(opts.tenantId);
|
|
166
|
+
const response = await fetch(endpoint, { method: 'GET', headers });
|
|
167
|
+
const text = await response.text();
|
|
168
|
+
const data = parseJson(text);
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
const error = new Error(data?.message || data?.msg || `Performance request failed: HTTP ${response.status}`);
|
|
171
|
+
error.code = response.status === 401 || response.status === 403 ? 'E_AUTH_FAILED' : 'E_REQUEST_FAILED';
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
if (data === null) {
|
|
175
|
+
const error = new Error('Performance response is not valid JSON.');
|
|
176
|
+
error.code = 'E_REQUEST_FAILED';
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
return data;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function getReviewUsers(opts, cookie, csrf) {
|
|
183
|
+
const location = opts.reviewUrl ? parseReviewLocation(opts.reviewUrl) : {};
|
|
184
|
+
const reviewId = String(opts.reviewId || location.reviewId || '');
|
|
185
|
+
const stageId = String(opts.stageId || location.formId || '');
|
|
186
|
+
if (!reviewId || !stageId) {
|
|
187
|
+
fail('review-users requires --review-url <url> or both --review-id <id> and --stage-id <id>.', 'E_INVALID_ARGS');
|
|
188
|
+
}
|
|
189
|
+
const base = assertPeoplePerfEndpoint(opts.reviewUsersEndpoint || DEFAULTS.submitEndpoint);
|
|
190
|
+
const endpoint = new URL(`${base.origin}/perf/api/review/v2/stage/${encodeURIComponent(reviewId)}/list_invite_review`);
|
|
191
|
+
const referer = String(opts.referer || location.referer || `https://people.bytedance.net/performance/perf/review/${reviewId}/${stageId}/members`);
|
|
192
|
+
const headers = {
|
|
193
|
+
accept: 'application/json, text/plain, */*',
|
|
194
|
+
'accept-language': 'zh-CN,zh;q=0.9',
|
|
195
|
+
'content-type': 'application/json;charset=UTF-8',
|
|
196
|
+
cookie,
|
|
197
|
+
origin: 'https://people.bytedance.net',
|
|
198
|
+
referer,
|
|
199
|
+
'x-f-csrf': csrf,
|
|
200
|
+
'x-f-lang': 'zh-CN',
|
|
201
|
+
'x-f-timezone': 'Asia/Shanghai',
|
|
202
|
+
};
|
|
203
|
+
if (opts.tenantId) headers['rpc-persist-lane-c-perfx-tenant-id'] = String(opts.tenantId);
|
|
204
|
+
const response = await fetch(endpoint, {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
headers,
|
|
207
|
+
body: JSON.stringify({ stage_id: stageId }),
|
|
208
|
+
});
|
|
209
|
+
const text = await response.text();
|
|
210
|
+
const data = parseJson(text);
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
const error = new Error(data?.message || data?.msg || `Performance request failed: HTTP ${response.status}`);
|
|
213
|
+
error.code = response.status === 401 || response.status === 403 ? 'E_AUTH_FAILED' : 'E_REQUEST_FAILED';
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
if (data === null) {
|
|
217
|
+
const error = new Error('Performance response is not valid JSON.');
|
|
218
|
+
error.code = 'E_REQUEST_FAILED';
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
return data;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function postPerformance(endpoint, payload, cookie, csrf, config) {
|
|
225
|
+
assertPeoplePerfEndpoint(endpoint);
|
|
226
|
+
const response = await fetch(endpoint, {
|
|
35
227
|
method: 'POST',
|
|
36
228
|
headers: {
|
|
37
229
|
accept: 'application/json, text/plain, */*',
|
|
@@ -55,7 +247,7 @@ async function run(args, opts) {
|
|
|
55
247
|
error.code = response.status === 401 || response.status === 403 ? 'E_AUTH_FAILED' : 'E_REQUEST_FAILED';
|
|
56
248
|
throw error;
|
|
57
249
|
}
|
|
58
|
-
|
|
250
|
+
return data === null ? text : data;
|
|
59
251
|
}
|
|
60
252
|
|
|
61
253
|
function readCookie(value, file) {
|
|
@@ -105,20 +297,21 @@ function getCookie(cookie, name) {
|
|
|
105
297
|
return '';
|
|
106
298
|
}
|
|
107
299
|
|
|
108
|
-
async function resolveConfig(opts, cookie) {
|
|
300
|
+
async function resolveConfig(opts, cookie, draftKind = 'confirm_invitation') {
|
|
109
301
|
const review = parseReviewLocation(opts.reviewUrl || opts.referer, opts.reviewId, opts.formId);
|
|
110
302
|
if (review.reviewId && review.formId) {
|
|
111
303
|
const settings = await requestJson(opts.settingsEndpoint || DEFAULTS.settingsEndpoint, cookie, review.referer);
|
|
112
304
|
const operatorId = String(opts.operatorId || settings?.data?.user?.id || '');
|
|
113
305
|
const tenantId = String(opts.tenantId || settings?.data?.tenant?.id || '');
|
|
114
306
|
if (!operatorId) fail('Unable to resolve operator id from /perf/api/user/settings.', 'E_REQUEST_FAILED');
|
|
115
|
-
const key =
|
|
307
|
+
const key = buildDraftKey(review.reviewId, draftKind, operatorId, review.formId);
|
|
116
308
|
const draftUrl = new URL(opts.draftEndpoint || DEFAULTS.draftEndpoint);
|
|
117
309
|
draftUrl.searchParams.set('key', key);
|
|
118
310
|
const draftResponse = await requestJson(draftUrl.toString(), cookie, review.referer);
|
|
119
311
|
if (!draftResponse?.success || !draftResponse?.data) fail('Unable to load the current performance draft.', 'E_REQUEST_FAILED');
|
|
120
312
|
return {
|
|
121
313
|
endpoint: String(opts.endpoint || DEFAULTS.endpoint),
|
|
314
|
+
submitEndpoint: String(opts.submitEndpoint || DEFAULTS.submitEndpoint),
|
|
122
315
|
reviewId: review.reviewId,
|
|
123
316
|
formId: review.formId,
|
|
124
317
|
operatorId,
|
|
@@ -126,6 +319,7 @@ async function resolveConfig(opts, cookie) {
|
|
|
126
319
|
referer: review.referer,
|
|
127
320
|
uid: String(opts.uid || `magic-builder-${Date.now()}`),
|
|
128
321
|
version: opts.draftVersion === undefined ? undefined : Number(opts.draftVersion),
|
|
322
|
+
templateGroupId: String(opts.templateGroupId || ''),
|
|
129
323
|
draft: draftResponse.data,
|
|
130
324
|
};
|
|
131
325
|
}
|
|
@@ -137,6 +331,7 @@ async function resolveConfig(opts, cookie) {
|
|
|
137
331
|
}
|
|
138
332
|
return {
|
|
139
333
|
endpoint: String(opts.endpoint || DEFAULTS.endpoint),
|
|
334
|
+
submitEndpoint: String(opts.submitEndpoint || DEFAULTS.submitEndpoint),
|
|
140
335
|
reviewId: String(opts.reviewId),
|
|
141
336
|
formId: String(opts.formId),
|
|
142
337
|
templateId: String(opts.templateId),
|
|
@@ -147,6 +342,7 @@ async function resolveConfig(opts, cookie) {
|
|
|
147
342
|
sourceId: String(opts.sourceId),
|
|
148
343
|
fieldSourceId: String(opts.fieldSourceId),
|
|
149
344
|
rootReviewId: String(opts.rootReviewId),
|
|
345
|
+
templateGroupId: String(opts.templateGroupId || ''),
|
|
150
346
|
version: Number(opts.draftVersion || opts.version),
|
|
151
347
|
tenantId: String(opts.tenantId || ''),
|
|
152
348
|
referer: String(opts.referer || `https://people.bytedance.net/performance/perf/review/${opts.reviewId}/${opts.formId}?mode=editable`),
|
|
@@ -154,6 +350,36 @@ async function resolveConfig(opts, cookie) {
|
|
|
154
350
|
};
|
|
155
351
|
}
|
|
156
352
|
|
|
353
|
+
function buildDraftKey(reviewId, draftKind, operatorId, formId) {
|
|
354
|
+
return `${reviewId}__${draftKind}__${operatorId}__${formId}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function readSelfReviewInputs(opts) {
|
|
358
|
+
const inputs = [
|
|
359
|
+
['good', 'goodFile'],
|
|
360
|
+
['improve', 'improveFile'],
|
|
361
|
+
['valuesComment', 'valuesCommentFile'],
|
|
362
|
+
];
|
|
363
|
+
const updates = {};
|
|
364
|
+
for (const [valueKey, fileKey] of inputs) {
|
|
365
|
+
const hasValue = Object.prototype.hasOwnProperty.call(opts, valueKey);
|
|
366
|
+
const hasFile = Object.prototype.hasOwnProperty.call(opts, fileKey);
|
|
367
|
+
if (hasValue && hasFile) {
|
|
368
|
+
fail(`Pass either --${toKebab(valueKey)} or --${toKebab(fileKey)}, not both.`, 'E_INVALID_ARGS');
|
|
369
|
+
}
|
|
370
|
+
if (hasFile) updates[valueKey] = readFileSync(resolve(String(opts[fileKey])), 'utf8');
|
|
371
|
+
else if (hasValue) updates[valueKey] = String(opts[valueKey]);
|
|
372
|
+
}
|
|
373
|
+
if (!Object.keys(updates).length) {
|
|
374
|
+
fail('Pass at least one of --good, --improve, or --values-comment (or the matching --*-file option).', 'E_INVALID_ARGS');
|
|
375
|
+
}
|
|
376
|
+
return updates;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function toKebab(value) {
|
|
380
|
+
return String(value).replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
157
383
|
function parseReviewLocation(value, reviewId, formId) {
|
|
158
384
|
if (reviewId && formId) {
|
|
159
385
|
return {
|
|
@@ -172,6 +398,7 @@ function parseReviewLocation(value, reviewId, formId) {
|
|
|
172
398
|
}
|
|
173
399
|
|
|
174
400
|
async function requestJson(url, cookie, referer) {
|
|
401
|
+
assertPeoplePerfEndpoint(url);
|
|
175
402
|
const response = await fetch(url, {
|
|
176
403
|
headers: { accept: 'application/json, text/plain, */*', cookie, referer },
|
|
177
404
|
});
|
|
@@ -181,6 +408,16 @@ async function requestJson(url, cookie, referer) {
|
|
|
181
408
|
return data;
|
|
182
409
|
}
|
|
183
410
|
|
|
411
|
+
function assertPeoplePerfEndpoint(value) {
|
|
412
|
+
const url = new URL(String(value || ''));
|
|
413
|
+
if (url.origin !== 'https://people.bytedance.net' || !url.pathname.startsWith('/perf/api/')) {
|
|
414
|
+
const error = new Error('Performance API endpoint must use https://people.bytedance.net/perf/api/.');
|
|
415
|
+
error.code = 'E_INVALID_ARGS';
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
return url;
|
|
419
|
+
}
|
|
420
|
+
|
|
184
421
|
function buildPayload(markdown, config) {
|
|
185
422
|
const now = Date.now();
|
|
186
423
|
const sections = splitMarkdownSections(markdown);
|
|
@@ -256,6 +493,92 @@ function buildPayloadFromDraft(markdown, draft, config) {
|
|
|
256
493
|
};
|
|
257
494
|
}
|
|
258
495
|
|
|
496
|
+
function buildSelfReviewPayloadFromDraft(draft, updates) {
|
|
497
|
+
const snapshot = structuredClone(draft);
|
|
498
|
+
const units = snapshot?.data?.units;
|
|
499
|
+
if (!Array.isArray(units)) throw selfReviewSchemaError('Current self-review draft does not contain data.units.');
|
|
500
|
+
const fields = units.flatMap(unit => Array.isArray(unit?.fields) ? unit.fields : []);
|
|
501
|
+
const textFields = fields.filter(field => field?.entityType === 'text');
|
|
502
|
+
const tagTextFields = fields.filter(field => field?.entityType === 'tag_text');
|
|
503
|
+
if (textFields.length !== 2 || tagTextFields.length !== 1) {
|
|
504
|
+
throw selfReviewSchemaError('Unable to identify the self-review text fields from the current template.');
|
|
505
|
+
}
|
|
506
|
+
if (Object.prototype.hasOwnProperty.call(updates, 'good')) {
|
|
507
|
+
textFields[0].value = selfReviewTextValue(updates.good);
|
|
508
|
+
}
|
|
509
|
+
if (Object.prototype.hasOwnProperty.call(updates, 'improve')) {
|
|
510
|
+
textFields[1].value = selfReviewTextValue(updates.improve);
|
|
511
|
+
}
|
|
512
|
+
if (Object.prototype.hasOwnProperty.call(updates, 'valuesComment')) {
|
|
513
|
+
tagTextFields[0].json_value = selfReviewTextValue(updates.valuesComment);
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
key: snapshot.key,
|
|
517
|
+
data: snapshot.data,
|
|
518
|
+
version: Number(snapshot.version),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function selfReviewSchemaError(message) {
|
|
523
|
+
const error = new Error(message);
|
|
524
|
+
error.code = 'E_REQUEST_FAILED';
|
|
525
|
+
return error;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function selfReviewTextValue(text) {
|
|
529
|
+
return { '0': { ops: markdownToDelta(String(text)), zoneId: '0', zoneType: 'Z' } };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function buildStagePayloadFromDraft(draft, config = {}) {
|
|
533
|
+
if (!draft?.data?.keyWorks) fail('Current draft does not contain keyWorks.', 'E_REQUEST_FAILED');
|
|
534
|
+
const keyWorks = structuredClone(draft.data.keyWorks);
|
|
535
|
+
const stageId = String(config.stageId || config.formId || '');
|
|
536
|
+
const rootReviewId = String(config.rootReviewId || draft.data.rootReviewId || draft.data.root_review_id || '');
|
|
537
|
+
const templateId = String(config.templateId || keyWorks.template_id || keyWorks.templateId || '');
|
|
538
|
+
const templateGroupId = String(
|
|
539
|
+
config.templateGroupId
|
|
540
|
+
|| draft.data.templateGroupId
|
|
541
|
+
|| draft.data.template_group_id
|
|
542
|
+
|| keyWorks.templateGroupId
|
|
543
|
+
|| keyWorks.template_group_id
|
|
544
|
+
|| ''
|
|
545
|
+
);
|
|
546
|
+
const missing = [
|
|
547
|
+
['root_review_id', rootReviewId],
|
|
548
|
+
['template_id', templateId],
|
|
549
|
+
['template_group_id', templateGroupId],
|
|
550
|
+
['stage_id', stageId],
|
|
551
|
+
].filter(([, value]) => !value).map(([name]) => name);
|
|
552
|
+
if (missing.length) {
|
|
553
|
+
fail(`Unable to build formal submission payload; missing ${missing.join(', ')}. Pass --template-group-id when it is absent from the draft.`, 'E_INVALID_ARGS');
|
|
554
|
+
}
|
|
555
|
+
if (!Array.isArray(keyWorks.units) || !keyWorks.units.length) {
|
|
556
|
+
fail('Current draft does not contain keyWorks.units.', 'E_REQUEST_FAILED');
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
root_review_id: rootReviewId,
|
|
560
|
+
template_id: templateId,
|
|
561
|
+
template_group_id: templateGroupId,
|
|
562
|
+
stage_id: stageId,
|
|
563
|
+
stage_ids: [stageId],
|
|
564
|
+
data: { units: keyWorks.units.filter(unit => unit?.type === 'object').map(serializeStageUnit) },
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function serializeStageUnit(unit) {
|
|
569
|
+
const output = structuredClone(unit);
|
|
570
|
+
for (const field of output.fields || []) {
|
|
571
|
+
delete field.json_value;
|
|
572
|
+
for (const group of field.sub_units || []) {
|
|
573
|
+
for (const subUnit of group || []) {
|
|
574
|
+
if (subUnit?.value && typeof subUnit.value !== 'string') subUnit.value = JSON.stringify(subUnit.value);
|
|
575
|
+
delete subUnit.json_value;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return output;
|
|
580
|
+
}
|
|
581
|
+
|
|
259
582
|
function createSubUnits(markdown, textFieldId, sourceId, now = Date.now()) {
|
|
260
583
|
return splitMarkdownSections(markdown).map((section, index) => {
|
|
261
584
|
const value = { '0': { ops: markdownToDelta(section), zoneId: '0', zoneType: 'Z' } };
|
|
@@ -273,17 +596,28 @@ function splitMarkdownSections(markdown) {
|
|
|
273
596
|
for (const line of lines) {
|
|
274
597
|
if (/^##\s+/.test(line)) {
|
|
275
598
|
if (current.some(item => item.trim()) && !current.every(item => /^#\s+/.test(item) || !item.trim())) {
|
|
276
|
-
sections.push(current.join('\n')
|
|
599
|
+
sections.push(cleanMarkdownSection(current.join('\n')));
|
|
277
600
|
}
|
|
278
601
|
current = [`# ${line.replace(/^##\s+/, '')}`];
|
|
279
602
|
} else if (current.length || !/^#\s+/.test(line)) {
|
|
280
603
|
current.push(line);
|
|
281
604
|
}
|
|
282
605
|
}
|
|
283
|
-
if (current.some(item => item.trim())) sections.push(current.join('\n')
|
|
606
|
+
if (current.some(item => item.trim())) sections.push(cleanMarkdownSection(current.join('\n')));
|
|
284
607
|
return sections.length ? sections : [text];
|
|
285
608
|
}
|
|
286
609
|
|
|
610
|
+
function cleanMarkdownSection(value) {
|
|
611
|
+
const cleaned = String(value || '').trim().replace(/(?:\n\s*(?:---+|\*\*\*+|___+)\s*)+$/u, '').trim();
|
|
612
|
+
return cleaned.split('\n').map((line, index) => {
|
|
613
|
+
if (index === 0) return line.replace(/^#{1,6}\s+/, '# ');
|
|
614
|
+
if (!line.trim() || /^\s*(?:[-*+]\s+|\d+[.)]\s+)/u.test(line)) return line;
|
|
615
|
+
if (/^\s*[-*+]\S/u.test(line)) return line.replace(/^(\s*[-*+])\s*/u, '$1 ');
|
|
616
|
+
if (/^\s*\d+[.)]\S/u.test(line)) return line.replace(/^(\s*\d+[.)])\s*/u, '$1 ');
|
|
617
|
+
return `- ${line.trim()}`;
|
|
618
|
+
}).join('\n');
|
|
619
|
+
}
|
|
620
|
+
|
|
287
621
|
function markdownToDelta(markdown) {
|
|
288
622
|
const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
|
|
289
623
|
const ops = [];
|
|
@@ -298,18 +632,29 @@ function markdownToDelta(markdown) {
|
|
|
298
632
|
ops.push({ insert: '\n', attributes: { 'code-block': true } });
|
|
299
633
|
continue;
|
|
300
634
|
}
|
|
635
|
+
if (!line.trim()) continue;
|
|
301
636
|
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
302
|
-
const unordered = line.match(
|
|
303
|
-
const ordered = line.match(
|
|
637
|
+
const unordered = line.match(/^(\s*)[-*+]\s+(.+)$/);
|
|
638
|
+
const ordered = line.match(/^(\s*)(\d+)[.)]\s+(.+)$/);
|
|
304
639
|
const quote = line.match(/^\s*>\s?(.*)$/);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
640
|
+
if (heading) {
|
|
641
|
+
ops.push({ insert: `${heading[1]} ${heading[2]}`, attributes: { bold: true } });
|
|
642
|
+
ops.push({ insert: '\n' });
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (unordered || ordered) {
|
|
646
|
+
const indent = (unordered?.[1] || ordered?.[1] || '').replace(/\t/g, ' ').length;
|
|
647
|
+
const level = Math.max(1, Math.floor(indent / 2) + 1);
|
|
648
|
+
const list = `${unordered ? 'bullet' : 'ordered'}${level}`;
|
|
649
|
+
const marker = unordered ? '*' : `${ordered[2]}.`;
|
|
650
|
+
const content = unordered?.[2] ?? ordered?.[3] ?? '';
|
|
651
|
+
ops.push({ insert: marker, attributes: { list, lmkr: '1' } });
|
|
652
|
+
appendInline(ops, content);
|
|
653
|
+
ops.push({ insert: '\n' });
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
appendInline(ops, quote?.[1] ?? line);
|
|
657
|
+
ops.push(quote ? { insert: '\n', attributes: { blockquote: true } } : { insert: '\n' });
|
|
313
658
|
}
|
|
314
659
|
if (ops.length > 1 && lines.at(-1) === '') ops.pop();
|
|
315
660
|
return ops.length ? ops : [{ insert: '\n' }];
|
|
@@ -334,4 +679,4 @@ function parseJson(text) {
|
|
|
334
679
|
try { return text ? JSON.parse(text) : {}; } catch (_) { return null; }
|
|
335
680
|
}
|
|
336
681
|
|
|
337
|
-
module.exports = { run, readCookie, normalizeCookie, getCookie, parseReviewLocation, splitMarkdownSections, markdownToDelta, buildPayload, buildPayloadFromDraft };
|
|
682
|
+
module.exports = { run, getKeyWorks, getReviewUsers, readInviteReviewPayload, normalizePerformancePayload, readCookie, normalizeCookie, getCookie, readSelfReviewInputs, buildDraftKey, parseReviewLocation, splitMarkdownSections, markdownToDelta, buildPayload, buildPayloadFromDraft, buildSelfReviewPayloadFromDraft, buildStagePayloadFromDraft, assertPeoplePerfEndpoint };
|
package/src/commands/skill.js
CHANGED
|
@@ -24,7 +24,7 @@ async function run(args, opts) {
|
|
|
24
24
|
if (sub !== 'check-update' && sub !== 'update' && sub !== 'install') {
|
|
25
25
|
fail('Usage: magic-builder skill <check-update|update|install>', 'E_INVALID_ARGS');
|
|
26
26
|
}
|
|
27
|
-
const skillsRoot = path.resolve(opts.skillsRoot ||
|
|
27
|
+
const skillsRoot = path.resolve(opts.skillsRoot || defaultSkillsRoot());
|
|
28
28
|
const environment = resolveEnvironment(String(opts.environment || 'auto'), skillsRoot);
|
|
29
29
|
const remote = await prepareRemotePackage();
|
|
30
30
|
const report = buildReport(remote, skillsRoot, environment);
|
|
@@ -106,7 +106,7 @@ function buildCloudUpdateDescriptor(report, options = {}) {
|
|
|
106
106
|
}] : [],
|
|
107
107
|
instructions: [
|
|
108
108
|
'Use the cloud runtime managed skill update/install mechanism.',
|
|
109
|
-
'Do not copy files into ~/.
|
|
109
|
+
'Do not copy files into ~/.agents/skills or overwrite local filesystem paths in cloud mode.',
|
|
110
110
|
'If no managed cloud updater is exposed, report this descriptor and do not claim the update was applied.',
|
|
111
111
|
],
|
|
112
112
|
};
|
|
@@ -142,7 +142,7 @@ function readSkillMetadata(skillDir) {
|
|
|
142
142
|
if (!fs.existsSync(skillPath)) return { exists: false, name: null, version: null, path: skillPath };
|
|
143
143
|
const text = fs.readFileSync(skillPath, 'utf8');
|
|
144
144
|
const nameMatch = text.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
145
|
-
const versionMatch = text.match(
|
|
145
|
+
const versionMatch = text.match(/^\s*version:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
146
146
|
return {
|
|
147
147
|
exists: true,
|
|
148
148
|
name: nameMatch ? nameMatch[1].trim() : null,
|
|
@@ -189,4 +189,8 @@ function ensureSafeSkillId(id) {
|
|
|
189
189
|
if (!/^[a-z0-9][a-z0-9-]*$/i.test(id)) throw new Error(`Unsafe skill id in update package: ${id}`);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
function defaultSkillsRoot(homeDir = os.homedir()) {
|
|
193
|
+
return path.join(homeDir, '.agents', 'skills');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = { run, defaultSkillsRoot };
|