@clack-platform/cli 0.3.1 → 0.3.2

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.
Files changed (2) hide show
  1. package/dist/cli.js +120 -15
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -28,6 +28,31 @@ var CliError = class extends Error {
28
28
  return 1;
29
29
  }
30
30
  };
31
+ function describeDetails(details) {
32
+ if (!details || typeof details !== "object" || Array.isArray(details)) return [];
33
+ const d = details;
34
+ const text = (v) => typeof v === "string" && v ? v.replace(/[\p{C}]/gu, "").slice(0, 160) : typeof v === "number" || typeof v === "boolean" ? String(v) : void 0;
35
+ const lines = [];
36
+ const reason = text(d.reason);
37
+ if (reason) lines.push(`\uC0AC\uC720: ${reason}`);
38
+ const file = text(d.file);
39
+ if (file) lines.push(`\uD30C\uC77C: ${file}`);
40
+ if (Array.isArray(d.issues)) {
41
+ for (const issue of d.issues.slice(0, 5)) {
42
+ if (!issue || typeof issue !== "object") continue;
43
+ const i = issue;
44
+ const parts = [["\uD30C\uC77C", i.file], ["\uC704\uCE58", i.pointer], ["\uADDC\uCE59", i.rule], ["\uAC12", i.param]].flatMap(([label, v]) => {
45
+ const t = text(v);
46
+ return t ? [`${label}=${t}`] : [];
47
+ });
48
+ if (parts.length) lines.push(`\uBB38\uC81C: ${parts.join(" ")}`);
49
+ }
50
+ }
51
+ const expected = text(d.expected);
52
+ const actual = text(d.actual);
53
+ if (expected !== void 0 || actual !== void 0) lines.push(`\uAE30\uB300: ${expected ?? "-"} / \uC2E4\uC81C: ${actual ?? "-"}`);
54
+ return lines;
55
+ }
31
56
  function apiError(status, body, retryAfter) {
32
57
  const value2 = body && typeof body === "object" ? body : {};
33
58
  const code = typeof value2.code === "string" ? value2.code : `HTTP_${status}`;
@@ -47,6 +72,9 @@ function apiError(status, body, retryAfter) {
47
72
  if (code === "CONTENT_METADATA_EXTRAS_DISABLED") message += " \uC774 \uD658\uACBD\uC5D0\uC11C\uB294 \uC5B8\uC5B4\uBCC4 \uBA54\uD0C0\uB370\uC774\uD130\xB7\uD0DC\uADF8\uB97C \uC544\uC9C1 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
48
73
  if (status === 423 || code === "USER_BANNED") message += " \uACC4\uC815 \uC81C\uD55C \uC0C1\uD0DC\uB97C \uC571\uC5D0\uC11C \uD655\uC778\uD558\uC138\uC694.";
49
74
  if (status === 503 && code.endsWith("DISABLED")) message += " \uD604\uC7AC \uC678\uBD80 \uB3C4\uAD6C \uC811\uADFC\uC774 \uC77C\uC2DC \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4.";
75
+ const located = describeDetails(value2.details);
76
+ if (located.length) message += `
77
+ ${located.map((line) => ` ${line}`).join("\n")}`;
50
78
  const raw = Number(value2.retry_after ?? retryAfter);
51
79
  const retry = Number.isFinite(raw) && raw > 0 ? Math.ceil(raw) : void 0;
52
80
  return new CliError(message, code, status, retry);
@@ -156,7 +184,7 @@ function redact(value2) {
156
184
  // package.json
157
185
  var package_default = {
158
186
  name: "@clack-platform/cli",
159
- version: "0.3.1",
187
+ version: "0.3.2",
160
188
  description: "\uD074\uB799 \uACC4\uC815\uC758 \uC0C1\uD488\xB7\uCF58\uD150\uCE20\xB7\uD504\uB85C\uD544\uC744 \uAD00\uB9AC\uD558\uB294 CLI",
161
189
  type: "module",
162
190
  bin: {
@@ -2477,6 +2505,16 @@ function inspectIntroSemantics(skill, form, slots, fail2) {
2477
2505
  if (!field2 || field2["x-clack-visibility"] !== "public") fail2(`/output/intro/${key}`, "\uACF5\uAC1C \uBB38\uC790\uC5F4(text\xB7textarea) \uD3FC \uD544\uB4DC\uB97C \uAC00\uB9AC\uCF1C\uC57C \uD569\uB2C8\uB2E4.");
2478
2506
  else if (field2.maxLength > max) fail2(`/output/intro/${key}`, `\uAC00\uB9AC\uD0A4\uB294 \uD3FC \uD544\uB4DC\uC758 \uCD5C\uB300 \uAE38\uC774\uAC00 ${max}\uC790 \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
2479
2507
  }
2508
+ let sectionLength = 0;
2509
+ for (const [key, name2] of Object.entries(intro.sections ?? {})) {
2510
+ const field2 = textField(name2);
2511
+ if (!field2 || field2["x-clack-visibility"] !== "public") fail2(`/output/intro/sections/${key}`, "\uC18C\uAC1C \uC139\uC158\uC740 \uACF5\uAC1C \uBB38\uC790\uC5F4(text\xB7textarea) \uD3FC \uD544\uB4DC\uB9CC \uAC00\uB9AC\uD0AC \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
2512
+ else {
2513
+ if (field2.maxLength > 2e3) fail2(`/output/intro/sections/${key}`, "\uC18C\uAC1C \uC139\uC158\uC758 \uCD5C\uB300 \uAE38\uC774\uB294 2,000\uC790\uC785\uB2C8\uB2E4.");
2514
+ sectionLength += field2.maxLength;
2515
+ }
2516
+ }
2517
+ if (sectionLength > 6e3) fail2("/output/intro/sections", "\uC18C\uAC1C \uC139\uC158\uC758 \uCD5C\uB300 \uAE38\uC774 \uD569\uACC4\uB294 6,000\uC790\uC785\uB2C8\uB2E4.");
2480
2518
  const fallback = intro.description_fallback ?? [];
2481
2519
  let composed = 0;
2482
2520
  for (const name2 of fallback) {
@@ -3831,6 +3869,37 @@ var clack_skill_v1_schema_default = {
3831
3869
  minItems: 1,
3832
3870
  maxItems: 8,
3833
3871
  uniqueItems: true
3872
+ },
3873
+ sections: {
3874
+ type: "object",
3875
+ additionalProperties: false,
3876
+ description: "\uACF5\uAC1C \uBB38\uC790\uC5F4 \uD3FC \uD544\uB4DC\uB97C \uC18C\uAC1C \uC139\uC158\uC5D0 \uB9E4\uD551\uD569\uB2C8\uB2E4. \uD56D\uBAA9\uBCC4 \uCD5C\uB300 2,000\uC790, \uC120\uC5B8\uB41C \uD544\uB4DC \uCD5C\uB300 \uAE38\uC774 \uD569\uACC4 6,000\uC790\uC785\uB2C8\uB2E4.",
3877
+ properties: {
3878
+ how_to: {
3879
+ type: "string",
3880
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3881
+ },
3882
+ notice: {
3883
+ type: "string",
3884
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3885
+ },
3886
+ character: {
3887
+ type: "string",
3888
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3889
+ },
3890
+ setting: {
3891
+ type: "string",
3892
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3893
+ },
3894
+ relationship: {
3895
+ type: "string",
3896
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3897
+ },
3898
+ first_scene: {
3899
+ type: "string",
3900
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3901
+ }
3902
+ }
3834
3903
  }
3835
3904
  }
3836
3905
  }
@@ -5135,6 +5204,7 @@ var clack_content_v1_schema_default = {
5135
5204
  var MAX_ZIP = 10 * 1024 * 1024;
5136
5205
  var MAX_EXPANDED = 30 * 1024 * 1024;
5137
5206
  var MAX_FILES2 = 500;
5207
+ var MAX_ZIP_ENTRIES = 2e3;
5138
5208
  var textDecoder2 = new TextDecoder("utf-8", { fatal: true });
5139
5209
  var ajv = new Ajv20202({ allErrors: true, strict: false });
5140
5210
  ajv.addSchema(clack_content_v1_schema_default);
@@ -5179,6 +5249,14 @@ function skillPathAllowed(path) {
5179
5249
  if (parts.includes("scripts") || /\.(?:sh|bash|zsh|fish|bat|cmd|ps1|exe|dll|so|dylib|wasm|py|rb|php|jar)$/i.test(path)) return false;
5180
5250
  return true;
5181
5251
  }
5252
+ function isArchiveMetadata(path) {
5253
+ const parts = path.split("/");
5254
+ const name2 = parts.at(-1) ?? "";
5255
+ return parts[0] === "__MACOSX" || name2 === ".DS_Store" || name2.startsWith("._");
5256
+ }
5257
+ function unsafeArchivePath(path) {
5258
+ return path.startsWith("/") || /[\p{Cc}]/u.test(path) || path.split("/").includes("..");
5259
+ }
5182
5260
  async function collectFiles(directory) {
5183
5261
  const files = {};
5184
5262
  let totalBytes = 0;
@@ -5186,6 +5264,7 @@ async function collectFiles(directory) {
5186
5264
  const entries = await readdir(join2(directory, relative), { withFileTypes: true });
5187
5265
  for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
5188
5266
  const path = relative ? `${relative}/${entry.name}` : entry.name;
5267
+ if (isArchiveMetadata(path)) continue;
5189
5268
  if (!skillPathAllowed(path)) invalid2(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD328\uD0A4\uC9C0 \uACBD\uB85C: ${path}`);
5190
5269
  if (entry.isSymbolicLink() || !(entry.isDirectory() || entry.isFile())) invalid2(`\uC77C\uBC18 \uD30C\uC77C\xB7\uD3F4\uB354\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4: ${path}`);
5191
5270
  if (entry.isDirectory()) {
@@ -5204,22 +5283,47 @@ async function collectFiles(directory) {
5204
5283
  }
5205
5284
  function inspectZip(bytes2, expectedDirectory, hashMismatch) {
5206
5285
  if (!bytes2.length || bytes2.length > MAX_ZIP) invalid2("\uC2A4\uD0AC ZIP\uC740 \uBE44\uC5B4 \uC788\uC9C0 \uC54A\uC740 10 MiB \uC774\uD558 \uD30C\uC77C\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.");
5286
+ const raw = [];
5287
+ try {
5288
+ unzipSync2(bytes2, { filter(entry) {
5289
+ if (raw.length >= MAX_ZIP_ENTRIES) invalid2("ZIP \uD56D\uBAA9 \uC218\uAC00 2,000\uAC1C\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
5290
+ const dir = entry.name.endsWith("/");
5291
+ raw.push({ name: dir ? entry.name.slice(0, -1) : entry.name, dir });
5292
+ return false;
5293
+ } });
5294
+ } catch (error2) {
5295
+ if (error2 instanceof CliError) throw error2;
5296
+ return invalid2("ZIP \uAD6C\uC870\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
5297
+ }
5298
+ for (const { name: name2 } of raw) if (unsafeArchivePath(name2)) invalid2(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 ZIP \uACBD\uB85C: ${name2.replace(new RegExp("\\p{C}", "gu"), "").slice(0, 160)}`);
5299
+ const kept = raw.filter(({ name: name2 }) => !isArchiveMetadata(name2));
5300
+ const keptFiles = kept.filter(({ dir }) => !dir);
5301
+ if (keptFiles.length > MAX_FILES2) invalid2("\uC2A4\uD0AC \uD30C\uC77C \uC218\uAC00 500\uAC1C\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
5302
+ let prefix = "";
5303
+ const top = keptFiles[0]?.name.split("/")[0];
5304
+ if (top && keptFiles.every(({ name: name2 }) => name2.startsWith(`${top}/`))) {
5305
+ const names = new Set(keptFiles.map(({ name: name2 }) => name2));
5306
+ if (names.has(`${top}/SKILL.md`) && names.has(`${top}/clack.skill.json`)) prefix = `${top}/`;
5307
+ }
5207
5308
  const seen = /* @__PURE__ */ new Set();
5208
- let entries = 0;
5309
+ const wanted = /* @__PURE__ */ new Map();
5310
+ for (const { name: name2, dir } of kept) {
5311
+ if (dir && prefix && name2 === prefix.slice(0, -1)) continue;
5312
+ const path = prefix && name2.startsWith(prefix) ? name2.slice(prefix.length) : name2;
5313
+ if (!skillPathAllowed(path) || seen.has(path.toLowerCase())) invalid2(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 ZIP \uACBD\uB85C: ${path}`);
5314
+ seen.add(path.toLowerCase());
5315
+ if (!dir) wanted.set(name2, path);
5316
+ }
5209
5317
  let totalBytes = 0;
5210
- let extracted;
5318
+ const extracted = {};
5211
5319
  try {
5212
- extracted = unzipSync2(bytes2, { filter(entry) {
5213
- entries++;
5214
- if (entries > MAX_FILES2) invalid2("\uC2A4\uD0AC \uD30C\uC77C \uC218\uAC00 500\uAC1C\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
5215
- const path = entry.name.endsWith("/") ? entry.name.slice(0, -1) : entry.name;
5216
- if (!skillPathAllowed(path) || seen.has(path.toLowerCase())) invalid2(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 ZIP \uACBD\uB85C: ${path}`);
5217
- seen.add(path.toLowerCase());
5218
- if (entry.name.endsWith("/")) return false;
5320
+ const content = unzipSync2(bytes2, { filter(entry) {
5321
+ if (!wanted.has(entry.name)) return false;
5219
5322
  totalBytes += entry.originalSize;
5220
5323
  if (totalBytes > MAX_EXPANDED) invalid2("\uC2A4\uD0AC \uC555\uCD95 \uD574\uC81C \uC6A9\uB7C9\uC774 30 MiB\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
5221
5324
  return true;
5222
5325
  } });
5326
+ for (const [name2, data] of Object.entries(content)) extracted[wanted.get(name2)] = data;
5223
5327
  } catch (error2) {
5224
5328
  if (error2 instanceof CliError) throw error2;
5225
5329
  return invalid2("ZIP \uAD6C\uC870\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
@@ -5462,11 +5566,12 @@ async function pushSkillVersion(client, prepared, skillId) {
5462
5566
  if (error2 instanceof CliError && error2.code === "SKILL_VERSION_MAJOR_REQUIRED") {
5463
5567
  throw new CliError(`${error2.message} (\uCDE8\uC18C: clack skill cancel ${resolvedSkillId} ${session2.version_id})`, error2.code, error2.status);
5464
5568
  }
5465
- if (error2 instanceof CliError) throw new CliError(
5466
- `${error2.message} \uC644\uB8CC \uC7AC\uC2DC\uB3C4: clack skill complete ${resolvedSkillId} ${session2.version_id}`,
5467
- error2.code,
5468
- error2.status
5469
- );
5569
+ if (error2 instanceof CliError) {
5570
+ const judged = error2.status >= 400 && error2.status < 500 && error2.status !== 429;
5571
+ const hint = judged ? "\uD328\uD0A4\uC9C0\uB97C \uACE0\uCE5C \uB4A4 \uAC19\uC740 \uBC84\uC804\uC73C\uB85C \uB2E4\uC2DC clack skill push\uD558\uC138\uC694." : `\uC644\uB8CC \uC7AC\uC2DC\uB3C4: clack skill complete ${resolvedSkillId} ${session2.version_id}`;
5572
+ throw new CliError(`${error2.message}
5573
+ ${hint}`, error2.code, error2.status, error2.retryAfter);
5574
+ }
5470
5575
  throw error2;
5471
5576
  }
5472
5577
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clack-platform/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "클랙 계정의 상품·콘텐츠·프로필을 관리하는 CLI",
5
5
  "type": "module",
6
6
  "bin": {