@sakupa/mcp 0.1.0 → 0.2.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.
Files changed (3) hide show
  1. package/dist/bin.js +76 -13
  2. package/dist/index.js +76 -13
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -440,6 +440,15 @@ function safeDecode(bytes) {
440
440
  }
441
441
  }
442
442
 
443
+ // ../core/dist/domain/hashing.js
444
+ async function sha256Hex(bytes) {
445
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
446
+ let hex = "";
447
+ for (const byte of new Uint8Array(digest))
448
+ hex += byte.toString(16).padStart(2, "0");
449
+ return hex;
450
+ }
451
+
443
452
  // ../core/dist/domain/subscription.js
444
453
  var SUBSCRIPTION_WARNING_TEXT = "You are subscribing {billingDomain} to Sakupa Domain Hosting: the {plan} monthly plan (\xA5{priceJpy}/month).\n\nPaying does not prove that you own this domain, does not grant management rights, and does not by itself publish a website: the domain must still pass DNS ownership verification.\n\nIf this site outgrows its plan, Sakupa automatically upgrades the subscription to the next plan (water -> personal -> share -> business) and renewals bill the new plan. There is no metered overage: above the Business plan, growth is limited instead of billed further.\n\nYou can cancel anytime; hosting then runs to the end of the already-paid month and stops.";
445
454
 
@@ -1187,6 +1196,36 @@ Please ask the user to choose, then re-run deploy_site with:
1187
1196
  Output directory: "${analysis.recommendedOutputDir ?? "."}", ${analysis.fileCount} files.`
1188
1197
  );
1189
1198
  }
1199
+ var MB2 = 1024 * 1024;
1200
+ function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
1201
+ const oversized = manifest.find((f) => f.size > MAX_SINGLE_FILE_BYTES);
1202
+ if (oversized) {
1203
+ throw new SakupaError(
1204
+ "validation_failed",
1205
+ `"${oversized.path}" is ${(oversized.size / MB2).toFixed(1)}MB, over the ${Math.floor(MAX_SINGLE_FILE_BYTES / MB2)}MB per-file limit. Remove or shrink it before deploying.`
1206
+ );
1207
+ }
1208
+ if (isFirstFreeDeploy) {
1209
+ const total = manifest.reduce((sum, f) => sum + f.size, 0);
1210
+ if (total > FREE_SITE_MAX_TOTAL_BYTES) {
1211
+ throw new SakupaError(
1212
+ "validation_failed",
1213
+ `The static output is ${(total / MB2).toFixed(1)}MB, over the ${Math.floor(FREE_SITE_MAX_TOTAL_BYTES / MB2)}MB free-site limit. Reduce the output (bundle/compress assets, drop large media) or use a paid domain plan for larger sites.`
1214
+ );
1215
+ }
1216
+ }
1217
+ }
1218
+ async function buildHashedManifest(files, outputAbs) {
1219
+ const manifest = [];
1220
+ for (const file of files) {
1221
+ const bytes = new Uint8Array(await fs2.readFile(join3(outputAbs, file.path)));
1222
+ manifest.push({ path: file.path, size: file.size, contentHash: await sha256Hex(bytes) });
1223
+ }
1224
+ return manifest;
1225
+ }
1226
+ function isIncrementalReuseFailure(e) {
1227
+ return isSakupaError(e) && e.code === "state_conflict" && typeof e.details === "object" && e.details !== null && e.details.incrementalReuseFailed === true;
1228
+ }
1190
1229
  async function uploadAll(ctx, targets, files, outputAbs) {
1191
1230
  for (let i = 0; i < targets.length; i++) {
1192
1231
  const target = targets[i];
@@ -1199,6 +1238,12 @@ async function uploadAll(ctx, targets, files, outputAbs) {
1199
1238
  );
1200
1239
  }
1201
1240
  const bytes = new Uint8Array(await fs2.readFile(join3(outputAbs, match.path)));
1241
+ if (bytes.byteLength !== match.size) {
1242
+ throw new SakupaError(
1243
+ "validation_failed",
1244
+ `"${match.path}" changed since analysis (${match.size} -> ${bytes.byteLength} bytes). Re-run deploy_site so Sakupa re-checks the current files before uploading.`
1245
+ );
1246
+ }
1202
1247
  await ctx.client.uploadFile(target, bytes);
1203
1248
  }
1204
1249
  return targets.length;
@@ -1256,9 +1301,10 @@ Next action: ${analysis.suggestedNextAction}`,
1256
1301
  return notDeployableResult(analysis);
1257
1302
  }
1258
1303
  const files = analysis.files;
1259
- const manifest = files.map((f) => ({ path: f.path, size: f.size }));
1260
1304
  const outputAbs = resolve2(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
1305
+ const manifest = await buildHashedManifest(files, outputAbs);
1261
1306
  const existing = readSiteFile(ctx.projectDir);
1307
+ ensureUploadSizeWithinLimits(manifest, !existing);
1262
1308
  if (!existing) {
1263
1309
  const created = await ctx.client.createSite({
1264
1310
  manifest,
@@ -1291,18 +1337,35 @@ Warnings:
1291
1337
  ${JSON.stringify(finalized2.warnings, null, 2)}` : "")
1292
1338
  );
1293
1339
  }
1294
- const deployment = await ctx.client.createDeployment(existing.siteId, existing.credential, {
1295
- manifest,
1296
- ...args.lang !== void 0 ? { lang: args.lang } : {},
1297
- spaFallback: args.spaFallback === true,
1298
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
1299
- });
1300
- const uploaded = await uploadAll(ctx, deployment.uploadTargets, files, outputAbs);
1301
- const finalized = await ctx.client.finalizeDeployment(
1302
- deployment.deploymentId,
1303
- existing.siteId,
1304
- existing.credential
1305
- );
1340
+ const updateOnce = async (forceFullUpload) => {
1341
+ const req = {
1342
+ manifest,
1343
+ ...args.lang !== void 0 ? { lang: args.lang } : {},
1344
+ spaFallback: args.spaFallback === true,
1345
+ ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {},
1346
+ ...forceFullUpload ? { forceFullUpload: true } : {}
1347
+ };
1348
+ const deployment = await ctx.client.createDeployment(
1349
+ existing.siteId,
1350
+ existing.credential,
1351
+ req
1352
+ );
1353
+ const uploaded2 = await uploadAll(ctx, deployment.uploadTargets, files, outputAbs);
1354
+ const finalized2 = await ctx.client.finalizeDeployment(
1355
+ deployment.deploymentId,
1356
+ existing.siteId,
1357
+ existing.credential
1358
+ );
1359
+ return { uploaded: uploaded2, finalized: finalized2 };
1360
+ };
1361
+ let update;
1362
+ try {
1363
+ update = await updateOnce(false);
1364
+ } catch (e) {
1365
+ if (!isIncrementalReuseFailure(e)) throw e;
1366
+ update = await updateOnce(true);
1367
+ }
1368
+ const { uploaded, finalized } = update;
1306
1369
  writeSiteFile(ctx.projectDir, { ...existing, url: finalized.url });
1307
1370
  return text(
1308
1371
  `Site updated: ${finalized.url}
package/dist/index.js CHANGED
@@ -490,6 +490,15 @@ function safeDecode(bytes) {
490
490
  }
491
491
  }
492
492
 
493
+ // ../core/dist/domain/hashing.js
494
+ async function sha256Hex(bytes) {
495
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
496
+ let hex = "";
497
+ for (const byte of new Uint8Array(digest))
498
+ hex += byte.toString(16).padStart(2, "0");
499
+ return hex;
500
+ }
501
+
493
502
  // ../core/dist/domain/subscription.js
494
503
  var SUBSCRIPTION_WARNING_TEXT = "You are subscribing {billingDomain} to Sakupa Domain Hosting: the {plan} monthly plan (\xA5{priceJpy}/month).\n\nPaying does not prove that you own this domain, does not grant management rights, and does not by itself publish a website: the domain must still pass DNS ownership verification.\n\nIf this site outgrows its plan, Sakupa automatically upgrades the subscription to the next plan (water -> personal -> share -> business) and renewals bill the new plan. There is no metered overage: above the Business plan, growth is limited instead of billed further.\n\nYou can cancel anytime; hosting then runs to the end of the already-paid month and stops.";
495
504
 
@@ -1234,6 +1243,36 @@ Please ask the user to choose, then re-run deploy_site with:
1234
1243
  Output directory: "${analysis.recommendedOutputDir ?? "."}", ${analysis.fileCount} files.`
1235
1244
  );
1236
1245
  }
1246
+ var MB2 = 1024 * 1024;
1247
+ function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
1248
+ const oversized = manifest.find((f) => f.size > MAX_SINGLE_FILE_BYTES);
1249
+ if (oversized) {
1250
+ throw new SakupaError(
1251
+ "validation_failed",
1252
+ `"${oversized.path}" is ${(oversized.size / MB2).toFixed(1)}MB, over the ${Math.floor(MAX_SINGLE_FILE_BYTES / MB2)}MB per-file limit. Remove or shrink it before deploying.`
1253
+ );
1254
+ }
1255
+ if (isFirstFreeDeploy) {
1256
+ const total = manifest.reduce((sum, f) => sum + f.size, 0);
1257
+ if (total > FREE_SITE_MAX_TOTAL_BYTES) {
1258
+ throw new SakupaError(
1259
+ "validation_failed",
1260
+ `The static output is ${(total / MB2).toFixed(1)}MB, over the ${Math.floor(FREE_SITE_MAX_TOTAL_BYTES / MB2)}MB free-site limit. Reduce the output (bundle/compress assets, drop large media) or use a paid domain plan for larger sites.`
1261
+ );
1262
+ }
1263
+ }
1264
+ }
1265
+ async function buildHashedManifest(files, outputAbs) {
1266
+ const manifest = [];
1267
+ for (const file of files) {
1268
+ const bytes = new Uint8Array(await fs2.readFile(join3(outputAbs, file.path)));
1269
+ manifest.push({ path: file.path, size: file.size, contentHash: await sha256Hex(bytes) });
1270
+ }
1271
+ return manifest;
1272
+ }
1273
+ function isIncrementalReuseFailure(e) {
1274
+ return isSakupaError(e) && e.code === "state_conflict" && typeof e.details === "object" && e.details !== null && e.details.incrementalReuseFailed === true;
1275
+ }
1237
1276
  async function uploadAll(ctx, targets, files, outputAbs) {
1238
1277
  for (let i = 0; i < targets.length; i++) {
1239
1278
  const target = targets[i];
@@ -1246,6 +1285,12 @@ async function uploadAll(ctx, targets, files, outputAbs) {
1246
1285
  );
1247
1286
  }
1248
1287
  const bytes = new Uint8Array(await fs2.readFile(join3(outputAbs, match.path)));
1288
+ if (bytes.byteLength !== match.size) {
1289
+ throw new SakupaError(
1290
+ "validation_failed",
1291
+ `"${match.path}" changed since analysis (${match.size} -> ${bytes.byteLength} bytes). Re-run deploy_site so Sakupa re-checks the current files before uploading.`
1292
+ );
1293
+ }
1249
1294
  await ctx.client.uploadFile(target, bytes);
1250
1295
  }
1251
1296
  return targets.length;
@@ -1303,9 +1348,10 @@ Next action: ${analysis.suggestedNextAction}`,
1303
1348
  return notDeployableResult(analysis);
1304
1349
  }
1305
1350
  const files = analysis.files;
1306
- const manifest = files.map((f) => ({ path: f.path, size: f.size }));
1307
1351
  const outputAbs = resolve2(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
1352
+ const manifest = await buildHashedManifest(files, outputAbs);
1308
1353
  const existing = readSiteFile(ctx.projectDir);
1354
+ ensureUploadSizeWithinLimits(manifest, !existing);
1309
1355
  if (!existing) {
1310
1356
  const created = await ctx.client.createSite({
1311
1357
  manifest,
@@ -1338,18 +1384,35 @@ Warnings:
1338
1384
  ${JSON.stringify(finalized2.warnings, null, 2)}` : "")
1339
1385
  );
1340
1386
  }
1341
- const deployment = await ctx.client.createDeployment(existing.siteId, existing.credential, {
1342
- manifest,
1343
- ...args.lang !== void 0 ? { lang: args.lang } : {},
1344
- spaFallback: args.spaFallback === true,
1345
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
1346
- });
1347
- const uploaded = await uploadAll(ctx, deployment.uploadTargets, files, outputAbs);
1348
- const finalized = await ctx.client.finalizeDeployment(
1349
- deployment.deploymentId,
1350
- existing.siteId,
1351
- existing.credential
1352
- );
1387
+ const updateOnce = async (forceFullUpload) => {
1388
+ const req = {
1389
+ manifest,
1390
+ ...args.lang !== void 0 ? { lang: args.lang } : {},
1391
+ spaFallback: args.spaFallback === true,
1392
+ ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {},
1393
+ ...forceFullUpload ? { forceFullUpload: true } : {}
1394
+ };
1395
+ const deployment = await ctx.client.createDeployment(
1396
+ existing.siteId,
1397
+ existing.credential,
1398
+ req
1399
+ );
1400
+ const uploaded2 = await uploadAll(ctx, deployment.uploadTargets, files, outputAbs);
1401
+ const finalized2 = await ctx.client.finalizeDeployment(
1402
+ deployment.deploymentId,
1403
+ existing.siteId,
1404
+ existing.credential
1405
+ );
1406
+ return { uploaded: uploaded2, finalized: finalized2 };
1407
+ };
1408
+ let update;
1409
+ try {
1410
+ update = await updateOnce(false);
1411
+ } catch (e) {
1412
+ if (!isIncrementalReuseFailure(e)) throw e;
1413
+ update = await updateOnce(true);
1414
+ }
1415
+ const { uploaded, finalized } = update;
1353
1416
  writeSiteFile(ctx.projectDir, { ...existing, url: finalized.url });
1354
1417
  return text(
1355
1418
  `Site updated: ${finalized.url}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Sakupa MCP server: publish AI-made static sites from your AI tool. AI-made pages, live in seconds.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",