pressship 0.1.14 → 0.1.15

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.
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { createReadStream } from "node:fs";
3
- import { cp, mkdir, mkdtemp, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
3
+ import { cp, mkdir, mkdtemp, readdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
4
4
  import { createServer } from "node:http";
5
5
  import { createRequire } from "node:module";
6
6
  import { tmpdir } from "node:os";
@@ -12,7 +12,8 @@ import fg from "fast-glob";
12
12
  import { z } from "zod";
13
13
  import { hasSavedSession } from "../auth/session.js";
14
14
  import { getWordPressOrgAccount } from "../auth/whoami.js";
15
- import { stagePluginDirectory } from "../package/archive.js";
15
+ import { analyzePluginPackage, stagePluginDirectory } from "../package/archive.js";
16
+ import { addPressshipIgnorePattern, createPressshipIgnoreMatcher, hardIgnoreDirectories, isHardIgnoredPath, pressshipIgnoreFile, readPressshipIgnorePatterns, removePressshipIgnorePattern } from "../package/ignore.js";
16
17
  import { createPluginPack, summarizePackResult, validatePluginPack } from "../package/pack.js";
17
18
  import { discoverPluginProject, resolvePluginProjectPath } from "../plugin/discover.js";
18
19
  import { assertDemoLaunchPlanSupported, createDemoLaunchPlan, prepareDemoRuntime, publicDemoLaunchPlan, resetPlaygroundSite } from "../plugin/demo.js";
@@ -26,7 +27,7 @@ import { publish } from "../wordpress-org/publish.js";
26
27
  import { fetchPluginStates, matchesPluginState } from "../wordpress-org/state.js";
27
28
  import { runPluginCheck } from "../checks/plugin-check.js";
28
29
  import { hasBlockingFindings } from "../checks/summary.js";
29
- import { ensureCacheDir, getConfigDir } from "../utils/paths.js";
30
+ import { ensureCacheDir, getConfigDir, pathExists } from "../utils/paths.js";
30
31
  import { addLocalPluginPath, getLocalPlugin, listLocalPlugins, removeLocalPlugin } from "./registry.js";
31
32
  import { WebJobManager } from "./jobs.js";
32
33
  import { getVersionState } from "./version-state.js";
@@ -44,6 +45,7 @@ const bumpVersionSchema = z.object({ bump: z.enum(["patch", "minor", "major"]) }
44
45
  const setVersionSchema = z.object({ version: z.string().min(1) });
45
46
  const createSvnTagSchema = z.object({ name: z.string().min(1) });
46
47
  const switchSvnTagSchema = z.object({ conflictResolution: z.enum(["override", "revert"]).optional() });
48
+ const ignoreRuleSchema = z.object({ pattern: z.string().min(1) });
47
49
  const writeStudioFileSchema = z.object({
48
50
  path: z.string().min(1),
49
51
  content: z.string()
@@ -104,6 +106,7 @@ export async function startWebServer(options = {}) {
104
106
  const approvals = new Map();
105
107
  const playgrounds = new Map();
106
108
  const playgroundPortReservations = new Set();
109
+ const packageSizeCache = new Map();
107
110
  const staticDir = resolveStaticDir();
108
111
  const dependencies = options.dependencies ?? {};
109
112
  const server = createServer((request, response) => {
@@ -113,6 +116,7 @@ export async function startWebServer(options = {}) {
113
116
  approvals,
114
117
  playgrounds,
115
118
  playgroundPortReservations,
119
+ packageSizeCache,
116
120
  staticDir,
117
121
  dependencies
118
122
  });
@@ -275,6 +279,62 @@ async function handleApi(request, response, url, context) {
275
279
  sendJson(response, 200, await listStudioFiles(plugin.path));
276
280
  return;
277
281
  }
282
+ const studioFilesDirectoryMatch = url.pathname.match(/^\/api\/plugins\/local\/([^/]+)\/files\/directory$/);
283
+ if (method === "GET" && studioFilesDirectoryMatch) {
284
+ const plugin = await requireLocalPlugin(decodeURIComponent(studioFilesDirectoryMatch[1]));
285
+ const relativePath = url.searchParams.get("path") ?? "";
286
+ sendJson(response, 200, await listStudioDirectory(plugin.path, relativePath));
287
+ return;
288
+ }
289
+ const studioIgnoreStateMatch = url.pathname.match(/^\/api\/plugins\/local\/([^/]+)\/ignore-state$/);
290
+ if (method === "GET" && studioIgnoreStateMatch) {
291
+ const plugin = await requireLocalPlugin(decodeURIComponent(studioIgnoreStateMatch[1]));
292
+ sendJson(response, 200, await readStudioIgnoreState(plugin.path));
293
+ return;
294
+ }
295
+ const studioPackageSizeMatch = url.pathname.match(/^\/api\/plugins\/local\/([^/]+)\/package-size$/);
296
+ if (method === "GET" && studioPackageSizeMatch) {
297
+ const plugin = await requireLocalPlugin(decodeURIComponent(studioPackageSizeMatch[1]));
298
+ sendJson(response, 200, readStudioPackageSize(plugin.path, context.packageSizeCache));
299
+ return;
300
+ }
301
+ const studioIgnoreRulesMatch = url.pathname.match(/^\/api\/plugins\/local\/([^/]+)\/ignore-rules$/);
302
+ if (method === "POST" && studioIgnoreRulesMatch) {
303
+ const plugin = await requireLocalPlugin(decodeURIComponent(studioIgnoreRulesMatch[1]));
304
+ const body = ignoreRuleSchema.parse(await readJson(request));
305
+ try {
306
+ await addPressshipIgnorePattern(path.resolve(plugin.path), body.pattern);
307
+ invalidateStudioPackageSize(plugin.path, context.packageSizeCache);
308
+ sendJson(response, 200, await readStudioIgnoreState(plugin.path));
309
+ }
310
+ catch (error) {
311
+ sendJson(response, 400, {
312
+ error: {
313
+ message: error instanceof Error ? error.message : String(error),
314
+ code: "invalid_ignore_pattern"
315
+ }
316
+ });
317
+ }
318
+ return;
319
+ }
320
+ if (method === "DELETE" && studioIgnoreRulesMatch) {
321
+ const plugin = await requireLocalPlugin(decodeURIComponent(studioIgnoreRulesMatch[1]));
322
+ const body = ignoreRuleSchema.parse(await readJson(request));
323
+ try {
324
+ await removePressshipIgnorePattern(path.resolve(plugin.path), body.pattern);
325
+ invalidateStudioPackageSize(plugin.path, context.packageSizeCache);
326
+ sendJson(response, 200, await readStudioIgnoreState(plugin.path));
327
+ }
328
+ catch (error) {
329
+ sendJson(response, 400, {
330
+ error: {
331
+ message: error instanceof Error ? error.message : String(error),
332
+ code: "invalid_ignore_pattern"
333
+ }
334
+ });
335
+ }
336
+ return;
337
+ }
278
338
  const studioCheckStateMatch = url.pathname.match(/^\/api\/plugins\/local\/([^/]+)\/check-state$/);
279
339
  if (method === "GET" && studioCheckStateMatch) {
280
340
  const localId = decodeURIComponent(studioCheckStateMatch[1]);
@@ -294,6 +354,7 @@ async function handleApi(request, response, url, context) {
294
354
  const plugin = await requireLocalPlugin(localId);
295
355
  const body = writeStudioFileSchema.parse(await readJson(request));
296
356
  const saved = await writeStudioFile(plugin.path, body.path, body.content);
357
+ invalidateStudioPackageSize(plugin.path, context.packageSizeCache);
297
358
  sendJson(response, 200, {
298
359
  ...saved,
299
360
  checkState: await removeStudioPluginCheckFindingsForFiles(localId, [saved.path])
@@ -307,6 +368,7 @@ async function handleApi(request, response, url, context) {
307
368
  const body = studioAiChangeSchema.parse(await readJson(request));
308
369
  try {
309
370
  const applied = await applyStudioAiChange(plugin.path, body);
371
+ invalidateStudioPackageSize(plugin.path, context.packageSizeCache);
310
372
  const checkState = await removeStudioPluginCheckFindingsForFiles(localId, [applied.path]);
311
373
  sendJson(response, 200, {
312
374
  ...applied,
@@ -329,6 +391,7 @@ async function handleApi(request, response, url, context) {
329
391
  const localId = decodeURIComponent(bumpMatch[1]);
330
392
  const plugin = await requireLocalPlugin(localId);
331
393
  await bumpLocalPluginVersion(plugin.path, body.bump);
394
+ invalidateStudioPackageSize(plugin.path, context.packageSizeCache);
332
395
  await addLocalPluginPath(plugin.path, plugin.source);
333
396
  await removeStudioPluginCheckState(localId);
334
397
  sendJson(response, 200, { ...(await getVersionState(plugin.path)), checkState: null });
@@ -351,6 +414,7 @@ async function handleApi(request, response, url, context) {
351
414
  }
352
415
  try {
353
416
  await setLocalPluginVersion(plugin.path, trimmed);
417
+ invalidateStudioPackageSize(plugin.path, context.packageSizeCache);
354
418
  await addLocalPluginPath(plugin.path, plugin.source);
355
419
  await removeStudioPluginCheckState(localId);
356
420
  sendJson(response, 200, { ...(await getVersionState(plugin.path)), checkState: null });
@@ -693,12 +757,13 @@ async function dryRunPublishJob(localId, requestedAction, approvals, context) {
693
757
  if (route.action === "release" && versionState.releaseBlocked) {
694
758
  context.log("Release is blocked by version state.", versionState);
695
759
  }
760
+ const ignorePatterns = await readPressshipIgnorePatterns(source.rootDir);
696
761
  context.status("Validating package.");
697
- const validation = await validatePluginPack(project, {});
762
+ const validation = await validatePluginPack(project, { ignore: ignorePatterns });
698
763
  const validationBlocked = hasBlockingFindings(validation.readmeFindings) || hasBlockingFindings(validation.pluginCheckFindings);
699
764
  const cacheDir = path.join(await ensureCacheDir(), "studio-packages");
700
765
  await mkdir(cacheDir, { recursive: true, mode: 0o700 });
701
- const pack = summarizePackResult(await createPluginPack(source.rootDir, { outputDir: cacheDir }), validation);
766
+ const pack = summarizePackResult(await createPluginPack(source.rootDir, { outputDir: cacheDir, ignore: ignorePatterns }), validation);
702
767
  const releasePlan = route.action === "release"
703
768
  ? createReleaseCommandPlan(project.slug, path.resolve(source.svnRootDir ?? path.join(process.cwd(), ".pressship-svn", project.slug)), project.version ?? "unknown", `Release ${project.slug} ${project.version ?? "unknown"}`, await inferWordPressOrgUsername())
704
769
  : undefined;
@@ -708,7 +773,8 @@ async function dryRunPublishJob(localId, requestedAction, approvals, context) {
708
773
  localId,
709
774
  pluginPath: source.inputDir,
710
775
  action: route.action,
711
- version: project.version
776
+ version: project.version,
777
+ ignore: ignorePatterns
712
778
  })
713
779
  : undefined;
714
780
  return {
@@ -741,6 +807,7 @@ async function confirmPublishJob(approvalId, overview, approvals, context) {
741
807
  yes: true,
742
808
  submit: approval.action === "submit",
743
809
  release: approval.action === "release",
810
+ ignore: approval.ignore ?? [],
744
811
  overview: overview ?? project.headers.description ?? ""
745
812
  });
746
813
  approvals.delete(approvalId);
@@ -762,39 +829,366 @@ async function readPluginDetail(scope, id) {
762
829
  readme: await fetchHostedReadme(id).catch(() => undefined)
763
830
  };
764
831
  }
832
+ const studioHiddenFilePatterns = [
833
+ "**/.git/**",
834
+ "**/.svn/**",
835
+ "**/node_modules/**",
836
+ "**/vendor/**",
837
+ "**/build/**",
838
+ "**/dist/**",
839
+ "**/playground/**",
840
+ "**/.wordpress-playground/**"
841
+ ];
842
+ const maxStudioEditableFileBytes = 1_000_000;
843
+ const studioAlwaysHiddenDirectoryNames = new Set([".git"]);
844
+ const studioDeferredDirectoryNames = new Set([
845
+ ...hardIgnoreDirectories,
846
+ "vendor",
847
+ ".wordpress-playground",
848
+ "playground"
849
+ ]);
850
+ const studioDeferredDirectoryPatterns = Array.from(studioDeferredDirectoryNames).flatMap((directory) => [
851
+ `${directory}/**`,
852
+ `**/${directory}/**`
853
+ ]);
854
+ const studioEditableExtensions = new Set([
855
+ ".php",
856
+ ".js",
857
+ ".jsx",
858
+ ".ts",
859
+ ".tsx",
860
+ ".css",
861
+ ".scss",
862
+ ".sass",
863
+ ".html",
864
+ ".htm",
865
+ ".json",
866
+ ".md",
867
+ ".txt",
868
+ ".xml",
869
+ ".yml",
870
+ ".yaml",
871
+ ".po",
872
+ ".pot",
873
+ ".ini",
874
+ ".sh",
875
+ ".svg"
876
+ ]);
877
+ const studioEditableFileNames = new Set([
878
+ "composer.json",
879
+ "package.json",
880
+ "readme.txt",
881
+ "license",
882
+ "license.txt",
883
+ pressshipIgnoreFile
884
+ ]);
765
885
  async function listStudioFiles(pluginPath) {
766
886
  const root = path.resolve(pluginPath);
767
- const files = await fg([
768
- "**/*.{php,js,jsx,ts,tsx,css,scss,sass,html,htm,json,md,txt,xml,yml,yaml,po,pot,ini,sh}",
769
- "composer.json",
770
- "package.json",
771
- "readme.txt"
772
- ], {
887
+ const patterns = await readPressshipIgnorePatterns(root);
888
+ const matcher = createPressshipIgnoreMatcher(patterns);
889
+ const [files, deferredDirectories, ignoredDirectories] = await Promise.all([
890
+ fg("**/*", {
891
+ cwd: root,
892
+ onlyFiles: true,
893
+ dot: true,
894
+ unique: true,
895
+ ignore: studioDeferredDirectoryPatterns
896
+ }),
897
+ listStudioDeferredDirectories(root, matcher),
898
+ listStudioIgnoredPatternDirectories(root, patterns, matcher)
899
+ ]);
900
+ const directories = mergeStudioDirectoryEntries([...deferredDirectories, ...ignoredDirectories]);
901
+ const entries = await Promise.all(files.sort((a, b) => a.localeCompare(b)).map(async (relativePath) => {
902
+ const fileStats = await stat(path.join(root, relativePath));
903
+ const ignoredBy = matcher.ignoredBy(relativePath);
904
+ return {
905
+ path: relativePath,
906
+ name: path.basename(relativePath),
907
+ directory: path.dirname(relativePath) === "." ? "" : path.dirname(relativePath),
908
+ size: fileStats.size,
909
+ hardIgnored: isHardIgnoredPath(relativePath),
910
+ ignored: Boolean(ignoredBy),
911
+ ignoredBy
912
+ };
913
+ }));
914
+ return { files: entries, directories };
915
+ }
916
+ async function listStudioDirectory(pluginPath, relativePath) {
917
+ const root = path.resolve(pluginPath);
918
+ const normalized = normalizeStudioRelativePath(relativePath);
919
+ if (!normalized) {
920
+ throw new Error("Choose a folder first.");
921
+ }
922
+ if (studioExplorerDirectoryIsAlwaysHidden(normalized)) {
923
+ return { files: [], directories: [] };
924
+ }
925
+ const directoryPath = path.resolve(root, normalized);
926
+ if (directoryPath !== root && !directoryPath.startsWith(`${root}${path.sep}`)) {
927
+ throw new Error("Folder is outside the plugin directory.");
928
+ }
929
+ const directoryStats = await stat(directoryPath);
930
+ if (!directoryStats.isDirectory()) {
931
+ throw new Error("Studio can only load folders.");
932
+ }
933
+ const patterns = await readPressshipIgnorePatterns(root);
934
+ const matcher = createPressshipIgnoreMatcher(patterns);
935
+ const ignoredBy = studioIgnoredBy(normalized, matcher);
936
+ const entries = await readdir(directoryPath, { withFileTypes: true });
937
+ const files = [];
938
+ const directories = [
939
+ {
940
+ path: normalized,
941
+ name: path.basename(normalized),
942
+ directory: path.dirname(normalized) === "." ? "" : path.dirname(normalized),
943
+ deferred: false,
944
+ ignored: Boolean(ignoredBy),
945
+ ignoredBy,
946
+ hardIgnored: isHardIgnoredPath(normalized)
947
+ }
948
+ ];
949
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
950
+ const childPath = `${normalized}/${entry.name}`;
951
+ if (studioExplorerDirectoryIsAlwaysHidden(childPath)) {
952
+ continue;
953
+ }
954
+ const childIgnoredBy = studioIgnoredBy(childPath, matcher);
955
+ if (entry.isDirectory()) {
956
+ directories.push({
957
+ path: childPath,
958
+ name: entry.name,
959
+ directory: normalized,
960
+ deferred: true,
961
+ ignored: Boolean(childIgnoredBy),
962
+ ignoredBy: childIgnoredBy,
963
+ hardIgnored: isHardIgnoredPath(childPath)
964
+ });
965
+ continue;
966
+ }
967
+ if (!entry.isFile()) {
968
+ continue;
969
+ }
970
+ const fileStats = await stat(path.join(directoryPath, entry.name));
971
+ files.push({
972
+ path: childPath,
973
+ name: entry.name,
974
+ directory: normalized,
975
+ size: fileStats.size,
976
+ hardIgnored: isHardIgnoredPath(childPath),
977
+ ignored: Boolean(childIgnoredBy),
978
+ ignoredBy: childIgnoredBy
979
+ });
980
+ }
981
+ return { files, directories };
982
+ }
983
+ async function listStudioDeferredDirectories(root, matcher) {
984
+ const entries = await readdir(root, { withFileTypes: true });
985
+ return entries
986
+ .filter((entry) => entry.isDirectory() &&
987
+ studioDeferredDirectoryNames.has(entry.name) &&
988
+ !studioExplorerDirectoryIsAlwaysHidden(entry.name))
989
+ .sort((a, b) => a.name.localeCompare(b.name))
990
+ .map((entry) => {
991
+ const ignoredBy = isHardIgnoredPath(entry.name)
992
+ ? "Pressship package rules"
993
+ : matcher.ignoredBy(entry.name) ?? matcher.ignoredBy(`${entry.name}/.pressship-directory`);
994
+ return {
995
+ path: entry.name,
996
+ name: entry.name,
997
+ directory: "",
998
+ deferred: true,
999
+ ignored: Boolean(ignoredBy),
1000
+ ignoredBy,
1001
+ hardIgnored: isHardIgnoredPath(entry.name)
1002
+ };
1003
+ });
1004
+ }
1005
+ async function listStudioIgnoredPatternDirectories(root, patterns, matcher) {
1006
+ const candidates = Array.from(new Set(patterns.flatMap(studioIgnoredDirectoryCandidates)));
1007
+ const entries = await Promise.all(candidates.map(async (relativePath) => {
1008
+ if (studioExplorerDirectoryIsAlwaysHidden(relativePath)) {
1009
+ return undefined;
1010
+ }
1011
+ const absolutePath = path.join(root, relativePath);
1012
+ if (!pathExists(absolutePath)) {
1013
+ return undefined;
1014
+ }
1015
+ const entryStats = await stat(absolutePath);
1016
+ if (!entryStats.isDirectory()) {
1017
+ return undefined;
1018
+ }
1019
+ const ignoredBy = matcher.ignoredBy(relativePath) ?? matcher.ignoredBy(`${relativePath}/.pressship-directory`);
1020
+ if (!ignoredBy) {
1021
+ return undefined;
1022
+ }
1023
+ return {
1024
+ path: relativePath,
1025
+ name: path.basename(relativePath),
1026
+ directory: path.dirname(relativePath) === "." ? "" : path.dirname(relativePath),
1027
+ deferred: false,
1028
+ ignored: true,
1029
+ ignoredBy,
1030
+ hardIgnored: isHardIgnoredPath(relativePath)
1031
+ };
1032
+ }));
1033
+ return entries
1034
+ .filter((entry) => Boolean(entry))
1035
+ .sort((a, b) => a.path.localeCompare(b.path));
1036
+ }
1037
+ function studioIgnoredDirectoryCandidates(pattern) {
1038
+ const trimmed = pattern.trim();
1039
+ if (!trimmed || trimmed.startsWith("!")) {
1040
+ return [];
1041
+ }
1042
+ let candidate = "";
1043
+ if (trimmed.endsWith("/**")) {
1044
+ candidate = trimmed.slice(0, -3);
1045
+ }
1046
+ else if (trimmed.endsWith("/")) {
1047
+ candidate = trimmed.slice(0, -1);
1048
+ }
1049
+ else if (!hasStudioGlobMagic(trimmed)) {
1050
+ candidate = trimmed;
1051
+ }
1052
+ candidate = candidate.replace(/^\.\/+/, "").replace(/\/+$/, "");
1053
+ if (!candidate || candidate === "." || hasStudioGlobMagic(candidate)) {
1054
+ return [];
1055
+ }
1056
+ return [candidate];
1057
+ }
1058
+ function hasStudioGlobMagic(value) {
1059
+ return /[*?[\]{}]/.test(value);
1060
+ }
1061
+ function studioIgnoredBy(relativePath, matcher) {
1062
+ return isHardIgnoredPath(relativePath)
1063
+ ? "Pressship package rules"
1064
+ : matcher.ignoredBy(relativePath);
1065
+ }
1066
+ function studioExplorerDirectoryIsAlwaysHidden(relativePath) {
1067
+ return relativePath
1068
+ .split("/")
1069
+ .filter(Boolean)
1070
+ .some((segment) => studioAlwaysHiddenDirectoryNames.has(segment));
1071
+ }
1072
+ function mergeStudioDirectoryEntries(entries) {
1073
+ const merged = new Map();
1074
+ for (const entry of entries) {
1075
+ const existing = merged.get(entry.path);
1076
+ if (!existing) {
1077
+ merged.set(entry.path, entry);
1078
+ continue;
1079
+ }
1080
+ merged.set(entry.path, {
1081
+ ...existing,
1082
+ ...entry,
1083
+ deferred: Boolean(existing.deferred || entry.deferred),
1084
+ ignored: Boolean(existing.ignored || entry.ignored),
1085
+ ignoredBy: existing.hardIgnored ? existing.ignoredBy : entry.ignoredBy ?? existing.ignoredBy,
1086
+ hardIgnored: Boolean(existing.hardIgnored || entry.hardIgnored)
1087
+ });
1088
+ }
1089
+ return Array.from(merged.values()).sort((a, b) => a.path.localeCompare(b.path));
1090
+ }
1091
+ async function readStudioIgnoreState(pluginPath) {
1092
+ const root = path.resolve(pluginPath);
1093
+ const patterns = await readPressshipIgnorePatterns(root);
1094
+ const ignoredFiles = await listStudioIgnoredFiles(root, patterns);
1095
+ return {
1096
+ ignorePath: path.join(root, pressshipIgnoreFile),
1097
+ patterns,
1098
+ ignoredFiles
1099
+ };
1100
+ }
1101
+ async function listStudioIgnoredFiles(root, patterns) {
1102
+ if (!patterns.length) {
1103
+ return [];
1104
+ }
1105
+ const matcher = createPressshipIgnoreMatcher(patterns);
1106
+ const files = await fg("**/*", {
773
1107
  cwd: root,
774
1108
  onlyFiles: true,
775
1109
  dot: false,
776
1110
  unique: true,
777
- ignore: [
778
- "**/.git/**",
779
- "**/.svn/**",
780
- "**/node_modules/**",
781
- "**/vendor/**",
782
- "**/build/**",
783
- "**/dist/**",
784
- "**/playground/**",
785
- "**/.wordpress-playground/**"
786
- ]
1111
+ ignore: studioHiddenFilePatterns
787
1112
  });
788
- const entries = await Promise.all(files.sort((a, b) => a.localeCompare(b)).map(async (relativePath) => {
1113
+ const ignored = files
1114
+ .map((relativePath) => ({ relativePath, ignoredBy: matcher.ignoredBy(relativePath) }))
1115
+ .filter((entry) => Boolean(entry.ignoredBy));
1116
+ const entries = await Promise.all(ignored.sort((a, b) => a.relativePath.localeCompare(b.relativePath)).map(async ({ relativePath, ignoredBy }) => {
789
1117
  const fileStats = await stat(path.join(root, relativePath));
790
1118
  return {
791
1119
  path: relativePath,
792
1120
  name: path.basename(relativePath),
793
1121
  directory: path.dirname(relativePath) === "." ? "" : path.dirname(relativePath),
794
- size: fileStats.size
1122
+ size: fileStats.size,
1123
+ ignoredBy
795
1124
  };
796
1125
  }));
797
- return { files: entries.filter((entry) => entry.size <= 1_000_000) };
1126
+ return entries;
1127
+ }
1128
+ function readStudioPackageSize(pluginPath, cache) {
1129
+ const source = resolvePluginProjectPath(pluginPath);
1130
+ const key = source.rootDir;
1131
+ const cached = cache.get(key);
1132
+ if (cached) {
1133
+ return publicStudioPackageSizeEntry(cached);
1134
+ }
1135
+ const entry = {
1136
+ status: "calculating",
1137
+ requestedAt: new Date().toISOString()
1138
+ };
1139
+ cache.set(key, entry);
1140
+ entry.promise = calculateStudioPackageSize(source.rootDir)
1141
+ .then((result) => {
1142
+ entry.status = "ready";
1143
+ entry.result = result;
1144
+ entry.error = undefined;
1145
+ entry.updatedAt = new Date().toISOString();
1146
+ })
1147
+ .catch((error) => {
1148
+ entry.status = "error";
1149
+ entry.error = error instanceof Error ? error.message : String(error);
1150
+ entry.updatedAt = new Date().toISOString();
1151
+ });
1152
+ return publicStudioPackageSizeEntry(entry);
1153
+ }
1154
+ function invalidateStudioPackageSize(pluginPath, cache) {
1155
+ cache.delete(resolvePluginProjectPath(pluginPath).rootDir);
1156
+ }
1157
+ async function calculateStudioPackageSize(rootDir) {
1158
+ const project = await discoverPluginProject(rootDir);
1159
+ const ignorePatterns = await readPressshipIgnorePatterns(rootDir);
1160
+ const analysis = await analyzePluginPackage(project, { ignore: ignorePatterns });
1161
+ return {
1162
+ sizeBytes: analysis.sizeBytes,
1163
+ maxSizeBytes: analysis.maxSizeBytes,
1164
+ overLimit: analysis.overLimit,
1165
+ fileCount: analysis.files.length,
1166
+ topLevelFolder: analysis.topLevelFolder,
1167
+ largestFiles: analysis.largestFiles
1168
+ };
1169
+ }
1170
+ function publicStudioPackageSizeEntry(entry) {
1171
+ if (entry.status === "ready" && entry.result) {
1172
+ return {
1173
+ status: "ready",
1174
+ cached: true,
1175
+ calculatedAt: entry.updatedAt,
1176
+ ...entry.result
1177
+ };
1178
+ }
1179
+ if (entry.status === "error") {
1180
+ return {
1181
+ status: "error",
1182
+ cached: true,
1183
+ error: entry.error ?? "Package size could not be calculated.",
1184
+ calculatedAt: entry.updatedAt
1185
+ };
1186
+ }
1187
+ return {
1188
+ status: "calculating",
1189
+ cached: false,
1190
+ requestedAt: entry.requestedAt
1191
+ };
798
1192
  }
799
1193
  async function readStudioFile(pluginPath, relativePath) {
800
1194
  const filePath = await resolveStudioFilePath(pluginPath, relativePath);
@@ -900,11 +1294,18 @@ async function resolveStudioFilePath(pluginPath, relativePath) {
900
1294
  if (!fileStats.isFile()) {
901
1295
  throw new Error("Studio can only open files.");
902
1296
  }
1297
+ if (fileStats.size > maxStudioEditableFileBytes) {
1298
+ throw new Error(`Studio can list ${normalized}, but files larger than 1 MB are not opened in the editor.`);
1299
+ }
903
1300
  return filePath;
904
1301
  }
905
1302
  function normalizeStudioRelativePath(relativePath) {
906
1303
  return relativePath.replace(/\\/g, "/").replace(/^\/+/, "").split("/").filter(Boolean).join("/");
907
1304
  }
1305
+ function isStudioEditablePath(relativePath) {
1306
+ const name = path.basename(relativePath).toLowerCase();
1307
+ return studioEditableFileNames.has(name) || studioEditableExtensions.has(path.extname(name));
1308
+ }
908
1309
  async function selectFolder() {
909
1310
  if (process.platform === "darwin") {
910
1311
  const result = await execa("osascript", [
@@ -1261,7 +1662,7 @@ async function createStudioAiPreviewWorkspace(pluginPath) {
1261
1662
  async function snapshotStudioFileContents(pluginPath) {
1262
1663
  const root = path.resolve(pluginPath);
1263
1664
  const { files } = await listStudioFiles(root);
1264
- const entries = await Promise.all(files.map(async (file) => {
1665
+ const entries = await Promise.all(files.filter((file) => file.size <= maxStudioEditableFileBytes && isStudioEditablePath(file.path)).map(async (file) => {
1265
1666
  const filePath = path.join(root, file.path);
1266
1667
  const [fileStats, content] = await Promise.all([stat(filePath), readFile(filePath, "utf8")]);
1267
1668
  return [