@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.
- package/dist/bin.js +76 -13
- package/dist/index.js +76 -13
- 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
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
deployment.
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
deployment.
|
|
1350
|
-
|
|
1351
|
-
|
|
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}
|