@sakupa/mcp 0.7.31 → 0.7.32
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 +66 -26
- package/dist/index.js +66 -26
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -129,7 +129,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
129
129
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
130
130
|
|
|
131
131
|
// ../core/dist/domain/version.js
|
|
132
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
132
|
+
var SAKUPA_MCP_VERSION = "0.7.32";
|
|
133
133
|
|
|
134
134
|
// ../core/dist/domain/errors.js
|
|
135
135
|
var HTTP_STATUS = {
|
|
@@ -690,7 +690,7 @@ var HttpApiClient = class {
|
|
|
690
690
|
// src/tools/definitions.ts
|
|
691
691
|
import { randomUUID } from "node:crypto";
|
|
692
692
|
import { existsSync as existsSync3, promises as fs2 } from "node:fs";
|
|
693
|
-
import { join as join5, resolve as resolve4 } from "node:path";
|
|
693
|
+
import { join as join5, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
|
|
694
694
|
import { z as z3 } from "zod";
|
|
695
695
|
|
|
696
696
|
// src/analyze/analyzer.ts
|
|
@@ -1113,7 +1113,15 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1113
1113
|
}
|
|
1114
1114
|
|
|
1115
1115
|
// src/project-file.ts
|
|
1116
|
-
import {
|
|
1116
|
+
import {
|
|
1117
|
+
chmodSync,
|
|
1118
|
+
existsSync,
|
|
1119
|
+
mkdirSync,
|
|
1120
|
+
readFileSync,
|
|
1121
|
+
rmdirSync,
|
|
1122
|
+
rmSync,
|
|
1123
|
+
writeFileSync
|
|
1124
|
+
} from "node:fs";
|
|
1117
1125
|
import { dirname, join as join2 } from "node:path";
|
|
1118
1126
|
var SITE_DIR = ".sakupa";
|
|
1119
1127
|
var SITE_FILE = "site.json";
|
|
@@ -1236,6 +1244,10 @@ function deleteSiteFile(projectDir) {
|
|
|
1236
1244
|
if (existsSync(path)) {
|
|
1237
1245
|
rmSync(path, { force: true });
|
|
1238
1246
|
}
|
|
1247
|
+
try {
|
|
1248
|
+
rmdirSync(join2(projectDir, SITE_DIR));
|
|
1249
|
+
} catch {
|
|
1250
|
+
}
|
|
1239
1251
|
}
|
|
1240
1252
|
function findAncestor(startDir, predicate, maxLevels = Number.POSITIVE_INFINITY) {
|
|
1241
1253
|
let cursor = startDir;
|
|
@@ -2344,18 +2356,19 @@ ${block}`, checklist: toDnsChecklist(diag.checks) };
|
|
|
2344
2356
|
}
|
|
2345
2357
|
function projectRootAbove(projectDir) {
|
|
2346
2358
|
if (existsSync3(join5(projectDir, "package.json"))) return null;
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
if (outputRel && outputRel !== ".") {
|
|
2354
|
-
const outputAbs = resolve4(projectDir, outputRel);
|
|
2355
|
-
if (bound(outputAbs)) return outputAbs;
|
|
2359
|
+
const packageRoot = findAncestor(projectDir, (dir) => existsSync3(join5(dir, "package.json")), 4);
|
|
2360
|
+
if (packageRoot) {
|
|
2361
|
+
return {
|
|
2362
|
+
projectRoot: packageRoot,
|
|
2363
|
+
outputDir: relative2(packageRoot, projectDir).split(sep3).join("/")
|
|
2364
|
+
};
|
|
2356
2365
|
}
|
|
2357
2366
|
return null;
|
|
2358
2367
|
}
|
|
2368
|
+
function findNeighborBinding(projectDir) {
|
|
2369
|
+
const bound = (dir) => loadSiteFile(dir).kind !== "absent";
|
|
2370
|
+
return findAncestor(projectDir, bound, 3);
|
|
2371
|
+
}
|
|
2359
2372
|
function freeSiteCreationBarrier(apiBaseUrl) {
|
|
2360
2373
|
const recent = listRecentCreations(Date.now(), apiBaseUrl);
|
|
2361
2374
|
if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
|
|
@@ -2406,12 +2419,14 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2406
2419
|
server.registerTool(
|
|
2407
2420
|
"deploy",
|
|
2408
2421
|
{
|
|
2409
|
-
description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscribed sites are permanent). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. Never uploads anything when
|
|
2422
|
+
description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscribed sites are permanent). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. projectDir is ALWAYS the project root where .sakupa belongs; outputDir is a separate REQUIRED relative path supplied from the current project inspection ("." when the root itself is published). NEVER pass the build/output folder as projectDir. Never uploads anything when analysis says the project is not deployable.`,
|
|
2410
2423
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2411
2424
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
2412
2425
|
inputSchema: {
|
|
2413
2426
|
projectDir: projectDirInput,
|
|
2414
|
-
outputDir: z3.string().
|
|
2427
|
+
outputDir: z3.string().min(1).describe(
|
|
2428
|
+
'REQUIRED: exact publish directory relative to projectDir, supplied by the AI after inspecting this project (for example ".", "dist", "html", or any custom build directory). NEVER put this path in projectDir; .sakupa belongs at projectDir.'
|
|
2429
|
+
),
|
|
2415
2430
|
spaFallback: z3.boolean().optional().describe(
|
|
2416
2431
|
"Override automatic SPA-fallback detection (single index.html + JS auto-enables rewriting unknown paths to index.html; multiple HTML pages auto-disable it). Pass only to force the behavior against the detected structure."
|
|
2417
2432
|
),
|
|
@@ -2427,9 +2442,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2427
2442
|
async (args) => {
|
|
2428
2443
|
try {
|
|
2429
2444
|
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2430
|
-
const analysis = await analyzeProject(ctx.projectDir, {
|
|
2431
|
-
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
2432
|
-
});
|
|
2445
|
+
const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
|
|
2433
2446
|
if (!analysis.deployable || !analysis.files) {
|
|
2434
2447
|
return notDeployableResult(analysis);
|
|
2435
2448
|
}
|
|
@@ -2447,18 +2460,40 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2447
2460
|
"blocked"
|
|
2448
2461
|
);
|
|
2449
2462
|
}
|
|
2450
|
-
|
|
2463
|
+
let existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
2464
|
+
let credentialRelocatedFrom = null;
|
|
2465
|
+
if (!existing && analysis.recommendedOutputDir !== ".") {
|
|
2466
|
+
const outputSiteState = loadSiteFile(outputAbs);
|
|
2467
|
+
if (outputSiteState.kind === "corrupted") {
|
|
2468
|
+
return text(
|
|
2469
|
+
"output_site_file_corrupted",
|
|
2470
|
+
`A misplaced .sakupa/site.json exists in output directory ${outputAbs}, but it is damaged: ${outputSiteState.problem}. Repair that file before retrying with projectDir: ${ctx.projectDir}. Nothing was deployed and no site was created.`,
|
|
2471
|
+
{ projectRoot: ctx.projectDir, outputDir: analysis.recommendedOutputDir },
|
|
2472
|
+
"blocked"
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
if (outputSiteState.kind === "ok") {
|
|
2476
|
+
writeSiteFile(ctx.projectDir, outputSiteState.file);
|
|
2477
|
+
deleteSiteFile(outputAbs);
|
|
2478
|
+
existing = outputSiteState.file;
|
|
2479
|
+
credentialRelocatedFrom = outputAbs;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2451
2482
|
if (!existing) {
|
|
2452
|
-
const
|
|
2453
|
-
if (
|
|
2483
|
+
const rootHint = args.subprojectConfirmed === true ? null : projectRootAbove(ctx.projectDir);
|
|
2484
|
+
if (rootHint) {
|
|
2454
2485
|
return text(
|
|
2455
2486
|
"not_project_root",
|
|
2456
|
-
`${ctx.projectDir}
|
|
2457
|
-
{
|
|
2487
|
+
`${ctx.projectDir} is a SUBFOLDER of a package.json project, and .sakupa must live at the project ROOT. Re-run deploy with projectDir: ${rootHint.projectRoot} and outputDir: ${rootHint.outputDir}. Only if the user explicitly says this subfolder is an INDEPENDENT site (e.g. a docs/ site inside a repo), re-run with subprojectConfirmed: true. Nothing was deployed and no site was created.`,
|
|
2488
|
+
{
|
|
2489
|
+
projectRoot: rootHint.projectRoot,
|
|
2490
|
+
outputDir: rootHint.outputDir,
|
|
2491
|
+
confirmationField: "subprojectConfirmed"
|
|
2492
|
+
},
|
|
2458
2493
|
"blocked"
|
|
2459
2494
|
);
|
|
2460
2495
|
}
|
|
2461
|
-
const neighbor = findNeighborBinding(ctx.projectDir
|
|
2496
|
+
const neighbor = findNeighborBinding(ctx.projectDir);
|
|
2462
2497
|
if (neighbor) {
|
|
2463
2498
|
return text(
|
|
2464
2499
|
"neighbor_binding_found",
|
|
@@ -2582,6 +2617,7 @@ Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
|
|
|
2582
2617
|
Project directory: ${ctx.projectDir}
|
|
2583
2618
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
2584
2619
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
2620
|
+
` : "") + (credentialRelocatedFrom ? `Credential binding relocated from ${credentialRelocatedFrom}/.sakupa to ${ctx.projectDir}/.sakupa; the existing site was preserved.
|
|
2585
2621
|
` : "") + (finalized.mode === "free" ? `
|
|
2586
2622
|
Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
|
|
2587
2623
|
` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
|
|
@@ -2596,7 +2632,8 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
2596
2632
|
expiresAt: finalized.expiresAt,
|
|
2597
2633
|
filesUploaded: uploaded,
|
|
2598
2634
|
totalBytes: finalized.totalBytes,
|
|
2599
|
-
warnings: finalized.warnings
|
|
2635
|
+
warnings: finalized.warnings,
|
|
2636
|
+
...credentialRelocatedFrom ? { credentialRelocatedFrom } : {}
|
|
2600
2637
|
}
|
|
2601
2638
|
);
|
|
2602
2639
|
} catch (e) {
|
|
@@ -3635,8 +3672,11 @@ Project directory contract: ONE directory = ONE site (its .sakupa/site.json hold
|
|
|
3635
3672
|
binding). projectDir is REQUIRED on EVERY tool call \u2014 always pass the absolute path of
|
|
3636
3673
|
the user's PROJECT ROOT, the SAME directory every time for the same project: the folder
|
|
3637
3674
|
the user opened (for framework projects, where package.json lives \u2014 never the dist/out
|
|
3638
|
-
build folder
|
|
3639
|
-
|
|
3675
|
+
build folder). For deploy, ALWAYS pass outputDir separately as the exact path RELATIVE to
|
|
3676
|
+
projectDir (use "." when publishing the root); outputDir is required and may have ANY name,
|
|
3677
|
+
so inspect the current project and never infer it from a conventional folder name. The server
|
|
3678
|
+
NEVER guesses either directory: only you can see the user's actual workspace and build output.
|
|
3679
|
+
After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
3640
3680
|
Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
|
|
3641
3681
|
refresh and delete echo
|
|
3642
3682
|
the directory they acted on \u2014 verify it matches the user's active project.
|
package/dist/index.js
CHANGED
|
@@ -124,7 +124,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
124
124
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
125
125
|
|
|
126
126
|
// ../core/dist/domain/version.js
|
|
127
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
127
|
+
var SAKUPA_MCP_VERSION = "0.7.32";
|
|
128
128
|
|
|
129
129
|
// ../core/dist/domain/errors.js
|
|
130
130
|
var HTTP_STATUS = {
|
|
@@ -781,7 +781,15 @@ var HttpApiClient = class {
|
|
|
781
781
|
};
|
|
782
782
|
|
|
783
783
|
// src/project-file.ts
|
|
784
|
-
import {
|
|
784
|
+
import {
|
|
785
|
+
chmodSync,
|
|
786
|
+
existsSync,
|
|
787
|
+
mkdirSync,
|
|
788
|
+
readFileSync,
|
|
789
|
+
rmdirSync,
|
|
790
|
+
rmSync,
|
|
791
|
+
writeFileSync
|
|
792
|
+
} from "node:fs";
|
|
785
793
|
import { dirname, join } from "node:path";
|
|
786
794
|
var SITE_DIR = ".sakupa";
|
|
787
795
|
var SITE_FILE = "site.json";
|
|
@@ -904,6 +912,10 @@ function deleteSiteFile(projectDir) {
|
|
|
904
912
|
if (existsSync(path)) {
|
|
905
913
|
rmSync(path, { force: true });
|
|
906
914
|
}
|
|
915
|
+
try {
|
|
916
|
+
rmdirSync(join(projectDir, SITE_DIR));
|
|
917
|
+
} catch {
|
|
918
|
+
}
|
|
907
919
|
}
|
|
908
920
|
function findAncestor(startDir, predicate, maxLevels = Number.POSITIVE_INFINITY) {
|
|
909
921
|
let cursor = startDir;
|
|
@@ -1484,7 +1496,7 @@ function toolError(e) {
|
|
|
1484
1496
|
// src/tools/definitions.ts
|
|
1485
1497
|
import { randomUUID } from "node:crypto";
|
|
1486
1498
|
import { existsSync as existsSync3, promises as fs2 } from "node:fs";
|
|
1487
|
-
import { join as join5, resolve as resolve4 } from "node:path";
|
|
1499
|
+
import { join as join5, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
|
|
1488
1500
|
import { z as z3 } from "zod";
|
|
1489
1501
|
|
|
1490
1502
|
// src/recovery-archive.ts
|
|
@@ -2433,18 +2445,19 @@ ${block}`, checklist: toDnsChecklist(diag.checks) };
|
|
|
2433
2445
|
}
|
|
2434
2446
|
function projectRootAbove(projectDir) {
|
|
2435
2447
|
if (existsSync3(join5(projectDir, "package.json"))) return null;
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
if (outputRel && outputRel !== ".") {
|
|
2443
|
-
const outputAbs = resolve4(projectDir, outputRel);
|
|
2444
|
-
if (bound(outputAbs)) return outputAbs;
|
|
2448
|
+
const packageRoot = findAncestor(projectDir, (dir) => existsSync3(join5(dir, "package.json")), 4);
|
|
2449
|
+
if (packageRoot) {
|
|
2450
|
+
return {
|
|
2451
|
+
projectRoot: packageRoot,
|
|
2452
|
+
outputDir: relative2(packageRoot, projectDir).split(sep3).join("/")
|
|
2453
|
+
};
|
|
2445
2454
|
}
|
|
2446
2455
|
return null;
|
|
2447
2456
|
}
|
|
2457
|
+
function findNeighborBinding(projectDir) {
|
|
2458
|
+
const bound = (dir) => loadSiteFile(dir).kind !== "absent";
|
|
2459
|
+
return findAncestor(projectDir, bound, 3);
|
|
2460
|
+
}
|
|
2448
2461
|
function freeSiteCreationBarrier(apiBaseUrl) {
|
|
2449
2462
|
const recent = listRecentCreations(Date.now(), apiBaseUrl);
|
|
2450
2463
|
if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
|
|
@@ -2495,12 +2508,14 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2495
2508
|
server.registerTool(
|
|
2496
2509
|
"deploy",
|
|
2497
2510
|
{
|
|
2498
|
-
description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscribed sites are permanent). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. Never uploads anything when
|
|
2511
|
+
description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscribed sites are permanent). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. projectDir is ALWAYS the project root where .sakupa belongs; outputDir is a separate REQUIRED relative path supplied from the current project inspection ("." when the root itself is published). NEVER pass the build/output folder as projectDir. Never uploads anything when analysis says the project is not deployable.`,
|
|
2499
2512
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2500
2513
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
2501
2514
|
inputSchema: {
|
|
2502
2515
|
projectDir: projectDirInput,
|
|
2503
|
-
outputDir: z3.string().
|
|
2516
|
+
outputDir: z3.string().min(1).describe(
|
|
2517
|
+
'REQUIRED: exact publish directory relative to projectDir, supplied by the AI after inspecting this project (for example ".", "dist", "html", or any custom build directory). NEVER put this path in projectDir; .sakupa belongs at projectDir.'
|
|
2518
|
+
),
|
|
2504
2519
|
spaFallback: z3.boolean().optional().describe(
|
|
2505
2520
|
"Override automatic SPA-fallback detection (single index.html + JS auto-enables rewriting unknown paths to index.html; multiple HTML pages auto-disable it). Pass only to force the behavior against the detected structure."
|
|
2506
2521
|
),
|
|
@@ -2516,9 +2531,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2516
2531
|
async (args) => {
|
|
2517
2532
|
try {
|
|
2518
2533
|
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2519
|
-
const analysis = await analyzeProject(ctx.projectDir, {
|
|
2520
|
-
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
2521
|
-
});
|
|
2534
|
+
const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
|
|
2522
2535
|
if (!analysis.deployable || !analysis.files) {
|
|
2523
2536
|
return notDeployableResult(analysis);
|
|
2524
2537
|
}
|
|
@@ -2536,18 +2549,40 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2536
2549
|
"blocked"
|
|
2537
2550
|
);
|
|
2538
2551
|
}
|
|
2539
|
-
|
|
2552
|
+
let existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
2553
|
+
let credentialRelocatedFrom = null;
|
|
2554
|
+
if (!existing && analysis.recommendedOutputDir !== ".") {
|
|
2555
|
+
const outputSiteState = loadSiteFile(outputAbs);
|
|
2556
|
+
if (outputSiteState.kind === "corrupted") {
|
|
2557
|
+
return text(
|
|
2558
|
+
"output_site_file_corrupted",
|
|
2559
|
+
`A misplaced .sakupa/site.json exists in output directory ${outputAbs}, but it is damaged: ${outputSiteState.problem}. Repair that file before retrying with projectDir: ${ctx.projectDir}. Nothing was deployed and no site was created.`,
|
|
2560
|
+
{ projectRoot: ctx.projectDir, outputDir: analysis.recommendedOutputDir },
|
|
2561
|
+
"blocked"
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
if (outputSiteState.kind === "ok") {
|
|
2565
|
+
writeSiteFile(ctx.projectDir, outputSiteState.file);
|
|
2566
|
+
deleteSiteFile(outputAbs);
|
|
2567
|
+
existing = outputSiteState.file;
|
|
2568
|
+
credentialRelocatedFrom = outputAbs;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2540
2571
|
if (!existing) {
|
|
2541
|
-
const
|
|
2542
|
-
if (
|
|
2572
|
+
const rootHint = args.subprojectConfirmed === true ? null : projectRootAbove(ctx.projectDir);
|
|
2573
|
+
if (rootHint) {
|
|
2543
2574
|
return text(
|
|
2544
2575
|
"not_project_root",
|
|
2545
|
-
`${ctx.projectDir}
|
|
2546
|
-
{
|
|
2576
|
+
`${ctx.projectDir} is a SUBFOLDER of a package.json project, and .sakupa must live at the project ROOT. Re-run deploy with projectDir: ${rootHint.projectRoot} and outputDir: ${rootHint.outputDir}. Only if the user explicitly says this subfolder is an INDEPENDENT site (e.g. a docs/ site inside a repo), re-run with subprojectConfirmed: true. Nothing was deployed and no site was created.`,
|
|
2577
|
+
{
|
|
2578
|
+
projectRoot: rootHint.projectRoot,
|
|
2579
|
+
outputDir: rootHint.outputDir,
|
|
2580
|
+
confirmationField: "subprojectConfirmed"
|
|
2581
|
+
},
|
|
2547
2582
|
"blocked"
|
|
2548
2583
|
);
|
|
2549
2584
|
}
|
|
2550
|
-
const neighbor = findNeighborBinding(ctx.projectDir
|
|
2585
|
+
const neighbor = findNeighborBinding(ctx.projectDir);
|
|
2551
2586
|
if (neighbor) {
|
|
2552
2587
|
return text(
|
|
2553
2588
|
"neighbor_binding_found",
|
|
@@ -2671,6 +2706,7 @@ Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
|
|
|
2671
2706
|
Project directory: ${ctx.projectDir}
|
|
2672
2707
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
2673
2708
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
2709
|
+
` : "") + (credentialRelocatedFrom ? `Credential binding relocated from ${credentialRelocatedFrom}/.sakupa to ${ctx.projectDir}/.sakupa; the existing site was preserved.
|
|
2674
2710
|
` : "") + (finalized.mode === "free" ? `
|
|
2675
2711
|
Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
|
|
2676
2712
|
` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
|
|
@@ -2685,7 +2721,8 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
2685
2721
|
expiresAt: finalized.expiresAt,
|
|
2686
2722
|
filesUploaded: uploaded,
|
|
2687
2723
|
totalBytes: finalized.totalBytes,
|
|
2688
|
-
warnings: finalized.warnings
|
|
2724
|
+
warnings: finalized.warnings,
|
|
2725
|
+
...credentialRelocatedFrom ? { credentialRelocatedFrom } : {}
|
|
2689
2726
|
}
|
|
2690
2727
|
);
|
|
2691
2728
|
} catch (e) {
|
|
@@ -3630,8 +3667,11 @@ Project directory contract: ONE directory = ONE site (its .sakupa/site.json hold
|
|
|
3630
3667
|
binding). projectDir is REQUIRED on EVERY tool call \u2014 always pass the absolute path of
|
|
3631
3668
|
the user's PROJECT ROOT, the SAME directory every time for the same project: the folder
|
|
3632
3669
|
the user opened (for framework projects, where package.json lives \u2014 never the dist/out
|
|
3633
|
-
build folder
|
|
3634
|
-
|
|
3670
|
+
build folder). For deploy, ALWAYS pass outputDir separately as the exact path RELATIVE to
|
|
3671
|
+
projectDir (use "." when publishing the root); outputDir is required and may have ANY name,
|
|
3672
|
+
so inspect the current project and never infer it from a conventional folder name. The server
|
|
3673
|
+
NEVER guesses either directory: only you can see the user's actual workspace and build output.
|
|
3674
|
+
After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
3635
3675
|
Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
|
|
3636
3676
|
refresh and delete echo
|
|
3637
3677
|
the directory they acted on \u2014 verify it matches the user's active project.
|