pressship 0.2.1 → 0.2.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.
package/assets/web/app.js CHANGED
@@ -219,6 +219,7 @@ function createInitialStudioRelease() {
219
219
  newTagError: "",
220
220
  customVersionDraft: "",
221
221
  customVersionError: "",
222
+ skipReadmeValidation: false,
222
223
  bumpInFlight: null,
223
224
  bumpSuccess: null,
224
225
  bumpError: "",
@@ -412,6 +413,8 @@ function onStudioResizerKeydown(event) {
412
413
 
413
414
  const state = {
414
415
  bootstrap: null,
416
+ pluginCheckSummaries: {},
417
+ latestWordPressVersion: "",
415
418
  remote: [],
416
419
  remoteUsername: "",
417
420
  remoteLoading: true,
@@ -642,6 +645,12 @@ document.addEventListener("input", (event) => {
642
645
  });
643
646
 
644
647
  document.addEventListener("change", (event) => {
648
+ if (event.target?.id === "studio-skip-readme-validation") {
649
+ state.studio.release.skipReadmeValidation = Boolean(event.target.checked);
650
+ updateStudioSidebar();
651
+ return;
652
+ }
653
+
645
654
  if (event.target?.id !== "studio-plugin-picker") {
646
655
  return;
647
656
  }
@@ -921,6 +930,8 @@ async function boot() {
921
930
  state.settings = state.bootstrap.settings ?? null;
922
931
  state.playgrounds = state.bootstrap.playgrounds ?? [];
923
932
  state.aiAssistance.harnesses = state.bootstrap.aiHarnesses ?? [];
933
+ state.pluginCheckSummaries = state.bootstrap.pluginCheckSummaries ?? {};
934
+ state.latestWordPressVersion = state.bootstrap.latestWordPressVersion ?? "";
924
935
  const initialRoute = parseLocationRoute();
925
936
  primeInitialRouteState(initialRoute);
926
937
  renderAccount();
@@ -1991,7 +2002,7 @@ function normalizeStudioPlaygroundVersion(value) {
1991
2002
 
1992
2003
  async function runStudioCheck() {
1993
2004
  if (state.studio.scope !== "local" || !state.studio.id) {
1994
- notice("Plugin Check is available for local plugins.", "warning");
2005
+ notice("Verify is available for local plugins.", "warning");
1995
2006
  return;
1996
2007
  }
1997
2008
 
@@ -2008,13 +2019,22 @@ async function runStudioCheck() {
2008
2019
  state.studio.checkRanAt = null;
2009
2020
  state.studio.terminalOpen = true;
2010
2021
  captureStudioEditorValue();
2011
- appendStudioTerminal("Running WordPress.org Plugin Check…", "status");
2022
+ appendStudioTerminal(
2023
+ state.studio.release.skipReadmeValidation
2024
+ ? "Running verify with readme validator skipped…"
2025
+ : "Running verify with readme validation and Plugin Check…",
2026
+ "status"
2027
+ );
2012
2028
  applyStudioCheckMarkers();
2013
2029
  renderStudio();
2014
2030
  remountStudioEditorIfNeeded();
2015
2031
  updateStudioControls();
2016
2032
 
2017
- const job = await createJob({ type: "check", localId: state.studio.id });
2033
+ const job = await createJob({
2034
+ type: "check",
2035
+ localId: state.studio.id,
2036
+ skipReadmeValidator: Boolean(state.studio.release.skipReadmeValidation)
2037
+ });
2018
2038
  state.studio.checkJobId = job.id;
2019
2039
  updateStudioControls();
2020
2040
  }
@@ -2344,9 +2364,9 @@ function renderStudio() {
2344
2364
  <span class="dashicons dashicons-saved" aria-hidden="true"></span>
2345
2365
  <span>Save</span>
2346
2366
  </button>
2347
- <button class="studio-action-button studio-compact-button" type="button" data-action="studio-check" id="studio-check-button" aria-label="Run Plugin Check" title="Run Plugin Check" disabled>
2367
+ <button class="studio-action-button studio-compact-button" type="button" data-action="studio-check" id="studio-check-button" aria-label="Run Verify" title="Run Verify" disabled>
2348
2368
  <span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
2349
- <span>Check</span>
2369
+ <span>Verify</span>
2350
2370
  </button>
2351
2371
  ${renderStudioPackageSizeButton()}
2352
2372
  ${renderStudioThemeToggle()}
@@ -2478,7 +2498,7 @@ function renderStudioStatusBar() {
2478
2498
  const file = state.studio.selectedFile?.path ?? "No file selected";
2479
2499
  const check = state.studio.checkSummary
2480
2500
  ? `${state.studio.checkSummary.error || 0} errors, ${state.studio.checkSummary.warning || 0} warnings`
2481
- : "Plugin Check idle";
2501
+ : "Verify idle";
2482
2502
  const assistant = selectedStudioAiAssistant();
2483
2503
  return `
2484
2504
  <footer class="studio-statusbar" aria-label="Studio status">
@@ -3658,10 +3678,10 @@ function renderStudioTerminal() {
3658
3678
  function renderStudioCheckNotes() {
3659
3679
  if (state.studio.checking) {
3660
3680
  return `
3661
- <aside class="studio-check-notes" aria-label="Plugin Check notes">
3681
+ <aside class="studio-check-notes" aria-label="Validation notes">
3662
3682
  <div class="studio-check-note studio-check-note-status">
3663
3683
  <span class="dashicons dashicons-update" aria-hidden="true"></span>
3664
- <span>Plugin Check is running…</span>
3684
+ <span>Verify is running…</span>
3665
3685
  </div>
3666
3686
  </aside>
3667
3687
  `;
@@ -3675,19 +3695,19 @@ function renderStudioCheckNotes() {
3675
3695
  if (!findings.length) {
3676
3696
  const total = state.studio.checkSummary.total ?? 0;
3677
3697
  return `
3678
- <aside class="studio-check-notes" aria-label="Plugin Check notes">
3698
+ <aside class="studio-check-notes" aria-label="Validation notes">
3679
3699
  <div class="studio-check-note studio-check-note-${total ? "info" : "success"}">
3680
3700
  <span class="dashicons ${total ? "dashicons-info-outline" : "dashicons-yes-alt"}" aria-hidden="true"></span>
3681
- <span>${escapeHtml(total ? `${formatCheckCounts(state.studio.checkSummary)} in other files.` : "Plugin Check reported no findings.")}</span>
3701
+ <span>${escapeHtml(total ? `${formatCheckCounts(state.studio.checkSummary)} in other files.` : "Verify reported no findings.")}</span>
3682
3702
  </div>
3683
3703
  </aside>
3684
3704
  `;
3685
3705
  }
3686
3706
 
3687
3707
  return `
3688
- <aside class="studio-check-notes" aria-label="Plugin Check notes">
3708
+ <aside class="studio-check-notes" aria-label="Validation notes">
3689
3709
  <header>
3690
- <strong>Plugin Check</strong>
3710
+ <strong>Validation</strong>
3691
3711
  <span>${escapeHtml(formatCheckCounts(studioCheckCountsForPath(state.studio.selectedFile?.path)))}</span>
3692
3712
  </header>
3693
3713
  <div class="studio-check-note-list">
@@ -3959,7 +3979,7 @@ function studioCliCommandForJob(input) {
3959
3979
  return studioCliCommand([
3960
3980
  "verify",
3961
3981
  localPluginCliTarget(input.localId),
3962
- "--skip-readme-validator",
3982
+ ...(input.skipReadmeValidator ? ["--skip-readme-validator"] : []),
3963
3983
  ...studioCliIgnoreFlags(input.localId)
3964
3984
  ]);
3965
3985
  case "dry-run-publish":
@@ -4040,6 +4060,18 @@ function applyStudioCheckState(checkState) {
4040
4060
  state.studio.checkFindings = checkState.findings ?? [];
4041
4061
  state.studio.checkSummary = checkState.summary ?? null;
4042
4062
  state.studio.checkRanAt = checkState.checkedAt ?? null;
4063
+
4064
+ if (state.studio.id && state.studio.scope === "local" && checkState.summary) {
4065
+ state.pluginCheckSummaries[state.studio.id] = {
4066
+ slug: checkState.slug ?? state.studio.plugin?.slug,
4067
+ name: checkState.name ?? state.studio.plugin?.name,
4068
+ checkedAt: checkState.checkedAt,
4069
+ skipped: checkState.skipped ?? false,
4070
+ available: checkState.available ?? true,
4071
+ summary: checkState.summary
4072
+ };
4073
+ renderDashboard();
4074
+ }
4043
4075
  }
4044
4076
 
4045
4077
  function applyStudioIgnoreState(ignoreState) {
@@ -4693,11 +4725,11 @@ function updateStudioControls() {
4693
4725
  }
4694
4726
  if (studioCheck) {
4695
4727
  studioCheck.disabled = !canCheck;
4696
- studioCheck.setAttribute("aria-label", state.studio.checkSummary ? "Re-run Plugin Check" : "Run Plugin Check");
4697
- studioCheck.title = state.studio.checkSummary ? "Re-run Plugin Check" : "Run Plugin Check";
4728
+ studioCheck.setAttribute("aria-label", state.studio.checkSummary ? "Re-run Verify" : "Run Verify");
4729
+ studioCheck.title = state.studio.checkSummary ? "Re-run Verify" : "Run Verify";
4698
4730
  studioCheck.innerHTML = state.studio.checking
4699
- ? `<span class="dashicons dashicons-update" aria-hidden="true"></span><span>Checking</span>`
4700
- : `<span class="dashicons dashicons-yes-alt" aria-hidden="true"></span><span>${state.studio.checkSummary ? "Re-check" : "Check"}</span>`;
4731
+ ? `<span class="dashicons dashicons-update" aria-hidden="true"></span><span>Verifying</span>`
4732
+ : `<span class="dashicons dashicons-yes-alt" aria-hidden="true"></span><span>${state.studio.checkSummary ? "Re-verify" : "Verify"}</span>`;
4701
4733
  }
4702
4734
  if (studioSave) {
4703
4735
  studioSave.disabled = !canSaveStudioFile();
@@ -5329,13 +5361,17 @@ function renderDashboard() {
5329
5361
  <div id="dashboard-widgets" class="metabox-holder columns-2">
5330
5362
  <div id="postbox-container-1" class="postbox-container">
5331
5363
  <div class="meta-box-sortables">
5364
+ ${renderDashboardOnboardingCard()}
5332
5365
  ${renderDashboardLocalWidget()}
5333
- ${renderDashboardAiCard()}
5366
+ ${renderDashboardReleaseReadinessWidget()}
5367
+ ${renderDashboardActivityWidget()}
5334
5368
  </div>
5335
5369
  </div>
5336
5370
  <div id="postbox-container-2" class="postbox-container">
5337
5371
  <div class="meta-box-sortables">
5338
5372
  ${renderDashboardAtGlanceWidget()}
5373
+ ${renderDashboardPluginCheckWidget()}
5374
+ ${renderDashboardCompatibilityWidget()}
5339
5375
  ${renderDashboardPlaygroundsCard()}
5340
5376
  ${renderDashboardAccountCard()}
5341
5377
  </div>
@@ -5423,8 +5459,6 @@ function renderDashboardAtGlanceWidget() {
5423
5459
  const localLabel = state.local.length === 1 ? "local plugin" : "local plugins";
5424
5460
  const remoteLabel = state.remote.length === 1 ? "WordPress.org plugin" : "WordPress.org plugins";
5425
5461
  const playgroundLabel = state.playgrounds.length === 1 ? "Playground running" : "Playgrounds running";
5426
- const account = state.bootstrap?.account?.username;
5427
- const accountLabel = account ? `Signed in as ${account}` : "WordPress.org not connected";
5428
5462
 
5429
5463
  return renderDashboardPostbox({
5430
5464
  id: "dashboard-at-a-glance",
@@ -5453,14 +5487,51 @@ function renderDashboardAtGlanceWidget() {
5453
5487
  view: "studio"
5454
5488
  })}
5455
5489
  </ul>
5456
- <div class="ps-dashboard-status">
5457
- <span class="dashicons ${account ? "dashicons-yes-alt" : "dashicons-warning"}" aria-hidden="true"></span>
5458
- <span>${escapeHtml(accountLabel)}</span>
5459
- </div>
5490
+ ${renderDashboardReleaseStatusLine()}
5460
5491
  `
5461
5492
  });
5462
5493
  }
5463
5494
 
5495
+ function renderDashboardReleaseStatusLine() {
5496
+ if (state.localLoading && !state.local.length) {
5497
+ return `
5498
+ <div class="ps-dashboard-status">
5499
+ <span class="dashicons dashicons-update" aria-hidden="true"></span>
5500
+ <span>Checking release state…</span>
5501
+ </div>
5502
+ `;
5503
+ }
5504
+
5505
+ const summary = dashboardReleaseSummary();
5506
+ let tone = "info";
5507
+ let icon = "dashicons-info";
5508
+ let text = "No local plugins to release yet.";
5509
+
5510
+ if (summary.total) {
5511
+ if (summary.blocked > 0) {
5512
+ tone = "error";
5513
+ icon = "dashicons-warning";
5514
+ text = `${summary.blocked} blocked from release · ${summary.ready} ready`;
5515
+ } else if (summary.behind > 0) {
5516
+ tone = "warning";
5517
+ icon = "dashicons-update";
5518
+ text = `${summary.behind} behind WordPress.org · ${summary.ready} ready`;
5519
+ } else {
5520
+ tone = "success";
5521
+ icon = "dashicons-yes-alt";
5522
+ text = `All ${summary.ready} plugin${summary.ready === 1 ? "" : "s"} ready to ship`;
5523
+ }
5524
+ }
5525
+
5526
+ return `
5527
+ <button class="ps-dashboard-status ps-dashboard-status-${escapeAttr(tone)}" type="button" data-view-button="release">
5528
+ <span class="dashicons ${escapeAttr(icon)}" aria-hidden="true"></span>
5529
+ <span>${escapeHtml(text)}</span>
5530
+ <span class="dashicons dashicons-arrow-right-alt2 ps-dashboard-status-arrow" aria-hidden="true"></span>
5531
+ </button>
5532
+ `;
5533
+ }
5534
+
5464
5535
  function renderDashboardGlanceItem({ icon, value, label, view, loading }) {
5465
5536
  return `
5466
5537
  <li class="ps-glance-item${loading ? " is-loading" : ""}">
@@ -5609,11 +5680,94 @@ function renderDashboardPlaygroundsCard() {
5609
5680
  `;
5610
5681
  }
5611
5682
 
5683
+ function dashboardRemoteSummary() {
5684
+ let cloned = 0;
5685
+ let committer = 0;
5686
+ let contributor = 0;
5687
+
5688
+ for (const plugin of state.remote) {
5689
+ if (remotePluginLocalState(plugin).entry) {
5690
+ cloned += 1;
5691
+ }
5692
+ const roles = Array.isArray(plugin.roles) ? plugin.roles : [];
5693
+ if (roles.includes("committer")) {
5694
+ committer += 1;
5695
+ } else if (roles.includes("contributor")) {
5696
+ contributor += 1;
5697
+ }
5698
+ }
5699
+
5700
+ const total = state.remote.length;
5701
+ return { total, cloned, notCloned: Math.max(0, total - cloned), committer, contributor };
5702
+ }
5703
+
5612
5704
  function renderDashboardAccountCard() {
5613
- const account = state.bootstrap?.account?.username;
5705
+ const account = state.bootstrap?.account;
5706
+ const username = account?.username;
5707
+ const displayName = account?.displayName;
5708
+ const profileUrl = account?.profileUrl;
5614
5709
  const loggedIn = Boolean(state.bootstrap?.loggedIn);
5615
5710
  const tone = loggedIn ? "success" : "warning";
5616
5711
  const label = loggedIn ? "Signed in" : "Not signed in";
5712
+
5713
+ if (!loggedIn) {
5714
+ return renderDashboardPostbox({
5715
+ id: "dashboard-wordpress-org",
5716
+ title: "WordPress.org",
5717
+ icon: "dashicons-admin-users",
5718
+ actions: badge(label, tone),
5719
+ body: `
5720
+ <div class="ps-account-body">
5721
+ <div class="ps-account-row">
5722
+ <span class="ps-account-icon" aria-hidden="true">
5723
+ <span class="dashicons dashicons-admin-users"></span>
5724
+ </span>
5725
+ <span class="ps-account-text">
5726
+ <strong>Not connected</strong>
5727
+ <small>Run <code>pressship login</code> in a terminal to clone, submit, and release.</small>
5728
+ </span>
5729
+ </div>
5730
+ <div class="ps-widget-footer">
5731
+ <button class="button button-ghost button-small" type="button" data-view-button="settings">
5732
+ Settings
5733
+ <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
5734
+ </button>
5735
+ </div>
5736
+ </div>
5737
+ `
5738
+ });
5739
+ }
5740
+
5741
+ const summary = dashboardRemoteSummary();
5742
+ const remoteValue = state.remoteLoading ? "…" : String(summary.total);
5743
+ const clonedValue = state.remoteLoading ? "…" : String(summary.cloned);
5744
+
5745
+ const reach = dashboardReachSummary();
5746
+ const reachBlock = !state.remoteLoading && reach.known
5747
+ ? `<div class="ps-account-reach">
5748
+ <span class="ps-account-reach-value">${escapeHtml(formatInstallCount(reach.total))}${reach.plus ? "+" : ""}</span>
5749
+ <span class="ps-account-reach-label">combined active installs${reach.topName ? ` · ${escapeHtml(reach.topName)} leads` : ""}</span>
5750
+ </div>`
5751
+ : "";
5752
+
5753
+ const roleParts = [];
5754
+ if (summary.committer) {
5755
+ roleParts.push(`Committer on ${summary.committer}`);
5756
+ }
5757
+ if (summary.contributor) {
5758
+ roleParts.push(`Contributor on ${summary.contributor}`);
5759
+ }
5760
+ const roleLine = !state.remoteLoading && roleParts.length
5761
+ ? `<div class="ps-account-roles"><span class="dashicons dashicons-groups" aria-hidden="true"></span><span>${escapeHtml(roleParts.join(" · "))}</span></div>`
5762
+ : "";
5763
+
5764
+ const profileLink = profileUrl
5765
+ ? `<a class="button button-ghost button-small" href="${escapeAttr(profileUrl)}" target="_blank" rel="noopener noreferrer">
5766
+ <span class="dashicons dashicons-external" aria-hidden="true"></span>
5767
+ View profile
5768
+ </a>`
5769
+ : "";
5770
+
5617
5771
  return renderDashboardPostbox({
5618
5772
  id: "dashboard-wordpress-org",
5619
5773
  title: "WordPress.org",
@@ -5626,11 +5780,28 @@ function renderDashboardAccountCard() {
5626
5780
  <span class="dashicons dashicons-admin-users"></span>
5627
5781
  </span>
5628
5782
  <span class="ps-account-text">
5629
- <strong>${escapeHtml(account ?? "not logged in")}</strong>
5630
- <small>${escapeHtml(loggedIn ? "Used for clone, submit, and release." : "Run pressship login in a terminal to connect.")}</small>
5783
+ <strong>${escapeHtml(displayName || username || "WordPress.org account")}</strong>
5784
+ <small>${escapeHtml(username ? `@${username}` : "Used for clone, submit, and release.")}</small>
5631
5785
  </span>
5632
5786
  </div>
5787
+ ${reachBlock}
5788
+ <ul class="ps-account-stats">
5789
+ <li>
5790
+ <button class="ps-account-stat" type="button" data-view-button="remote">
5791
+ <strong>${escapeHtml(remoteValue)}</strong>
5792
+ <span>on WordPress.org</span>
5793
+ </button>
5794
+ </li>
5795
+ <li>
5796
+ <button class="ps-account-stat" type="button" data-view-button="local">
5797
+ <strong>${escapeHtml(clonedValue)}</strong>
5798
+ <span>cloned locally</span>
5799
+ </button>
5800
+ </li>
5801
+ </ul>
5802
+ ${roleLine}
5633
5803
  <div class="ps-widget-footer">
5804
+ ${profileLink}
5634
5805
  <button class="button button-ghost button-small" type="button" data-view-button="settings">
5635
5806
  Settings
5636
5807
  <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
@@ -5641,73 +5812,681 @@ function renderDashboardAccountCard() {
5641
5812
  });
5642
5813
  }
5643
5814
 
5644
- function renderDashboardAiCard() {
5645
- const selected = state.settings?.aiAssistant ?? "none";
5646
- const providers = aiAssistanceProviders();
5647
- const ready = providers.filter((p) => p.status === "ready");
5648
- const installed = providers.filter((p) => p.status === "installed" || p.status === "ready");
5649
- const active = providers.find((p) => p.id === selected);
5815
+ const VERSION_STATUS_META = {
5816
+ missing_version: { label: "Missing version", tone: "error", icon: "dashicons-warning", action: "manage-release", actionLabel: "Fix" },
5817
+ header_readme_mismatch: { label: "Version mismatch", tone: "error", icon: "dashicons-randomize", action: "manage-release", actionLabel: "Fix" },
5818
+ duplicate_tag_blocked: { label: "Tag already shipped", tone: "error", icon: "dashicons-tag", action: "manage-release", actionLabel: "Bump" },
5819
+ remote_newer: { label: "Behind WordPress.org", tone: "warning", icon: "dashicons-update", action: "version-state", actionLabel: "Details" },
5820
+ unknown_svn_state: { label: "SVN state unknown", tone: "warning", icon: "dashicons-editor-help", action: "version-state", actionLabel: "Details" },
5821
+ ready: { label: "Ready", tone: "success", icon: "dashicons-yes-alt", action: "manage-release", actionLabel: "Release" }
5822
+ };
5823
+
5824
+ const BLOCKING_STATUS_ORDER = ["missing_version", "header_readme_mismatch", "duplicate_tag_blocked"];
5825
+
5826
+ function versionStatusMeta(status) {
5827
+ return VERSION_STATUS_META[status] ?? { label: labelize(status), tone: "info", icon: "dashicons-info", action: "version-state", actionLabel: "Details" };
5828
+ }
5829
+
5830
+ function primaryVersionIssue(versionState) {
5831
+ const statuses = versionState?.statuses ?? [];
5832
+ const status =
5833
+ BLOCKING_STATUS_ORDER.find((candidate) => statuses.includes(candidate)) ??
5834
+ (statuses.includes("remote_newer") ? "remote_newer" : statuses.find((candidate) => candidate !== "ready"));
5835
+ if (!status) {
5836
+ return null;
5837
+ }
5838
+ const meta = versionStatusMeta(status);
5839
+ const messageIndex = statuses.indexOf(status);
5840
+ const message = versionState.messages?.[messageIndex] ?? versionState.messages?.[0] ?? meta.label;
5841
+ return { status, message, ...meta };
5842
+ }
5843
+
5844
+ function dashboardReleaseSummary() {
5845
+ let ready = 0;
5846
+ let blocked = 0;
5847
+ let behind = 0;
5848
+ let unknown = 0;
5849
+ const attention = [];
5850
+
5851
+ for (const plugin of state.local) {
5852
+ const versionState = state.versionStates.get(plugin.id);
5853
+ if (!versionState || versionState.error || !Array.isArray(versionState.statuses)) {
5854
+ unknown += 1;
5855
+ continue;
5856
+ }
5857
+
5858
+ const isBehind = versionState.statuses.includes("remote_newer");
5859
+ if (versionState.releaseBlocked) {
5860
+ blocked += 1;
5861
+ } else if (isBehind) {
5862
+ behind += 1;
5863
+ } else {
5864
+ ready += 1;
5865
+ }
5866
+
5867
+ if (versionState.releaseBlocked || isBehind) {
5868
+ const issue = primaryVersionIssue(versionState);
5869
+ if (issue) {
5870
+ attention.push({ plugin, versionState, issue });
5871
+ }
5872
+ }
5873
+ }
5874
+
5875
+ attention.sort((left, right) => issueWeight(right.issue) - issueWeight(left.issue));
5876
+ return { total: state.local.length, ready, blocked, behind, unknown, attention };
5877
+ }
5878
+
5879
+ function issueWeight(issue) {
5880
+ if (issue.tone === "error") return 2;
5881
+ if (issue.tone === "warning") return 1;
5882
+ return 0;
5883
+ }
5884
+
5885
+ function renderDashboardReleaseReadinessWidget() {
5886
+ const loading = state.localLoading && !state.local.length;
5887
+ const summary = dashboardReleaseSummary();
5650
5888
 
5651
5889
  let tone = "info";
5652
- let label = "Disabled";
5653
- if (selected === "none") {
5654
- tone = "warning";
5655
- label = "Disabled";
5656
- } else if (active?.status === "ready") {
5657
- tone = "success";
5658
- label = "Ready";
5659
- } else if (active?.status === "not_authenticated") {
5660
- tone = "warning";
5661
- label = "Needs login";
5662
- } else if (active?.status === "not_installed") {
5663
- tone = "error";
5664
- label = "Not installed";
5665
- }
5666
-
5667
- const chips = providers
5668
- .map((provider) => {
5669
- const isSelected = provider.id === selected && selected !== "none";
5670
- return `
5671
- <span class="ps-ai-chip ps-ai-chip-${escapeAttr(provider.status)}${isSelected ? " is-selected" : ""}" title="${escapeAttr(provider.detail)}">
5672
- <span class="ps-ai-chip-dot" aria-hidden="true"></span>
5673
- ${escapeHtml(provider.label)}
5890
+ let label = "";
5891
+ if (!loading) {
5892
+ if (!summary.total) {
5893
+ tone = "info";
5894
+ label = "No plugins";
5895
+ } else if (summary.blocked > 0) {
5896
+ tone = "error";
5897
+ label = `${summary.blocked} blocked`;
5898
+ } else if (summary.behind > 0) {
5899
+ tone = "warning";
5900
+ label = `${summary.behind} behind`;
5901
+ } else {
5902
+ tone = "success";
5903
+ label = "All ready";
5904
+ }
5905
+ }
5906
+
5907
+ return renderDashboardPostbox({
5908
+ id: "dashboard-release-readiness",
5909
+ title: "Release readiness",
5910
+ icon: "ps-icon-rocket",
5911
+ className: "ps-dashboard-main",
5912
+ actions: loading ? "" : badge(label, tone),
5913
+ body: renderDashboardReadinessBody(summary, loading)
5914
+ });
5915
+ }
5916
+
5917
+ function renderDashboardReadinessBody(summary, loading) {
5918
+ if (loading) {
5919
+ return `
5920
+ <p class="ps-widget-intro">Checking version and release state for every local plugin…</p>
5921
+ ${renderDashboardSkeletonRows(2)}
5922
+ `;
5923
+ }
5924
+
5925
+ if (!summary.total) {
5926
+ return `
5927
+ <div class="ps-empty-card">
5928
+ <span class="dashicons ps-icon-rocket" aria-hidden="true"></span>
5929
+ <strong>Nothing to release yet</strong>
5930
+ <p>Add a local plugin folder, then Pressship flags version mismatches and duplicate tags before you publish.</p>
5931
+ <div class="ps-empty-card-actions">
5932
+ <button class="button button-primary" type="button" data-action="choose-local-folder">
5933
+ <span class="dashicons dashicons-open-folder" aria-hidden="true"></span>
5934
+ Choose Folder
5935
+ </button>
5936
+ </div>
5937
+ </div>
5938
+ `;
5939
+ }
5940
+
5941
+ if (!summary.attention.length) {
5942
+ return `
5943
+ <div class="ps-readiness-clear">
5944
+ <span class="ps-readiness-clear-icon" aria-hidden="true">
5945
+ <span class="dashicons dashicons-yes-alt"></span>
5674
5946
  </span>
5947
+ <div class="ps-readiness-clear-text">
5948
+ <strong>Everything looks release-ready</strong>
5949
+ <p>${escapeHtml(`All ${summary.ready} plugin${summary.ready === 1 ? "" : "s"} pass version checks. Open Release Management to ship.`)}</p>
5950
+ </div>
5951
+ <button class="button button-primary button-small" type="button" data-view-button="release">
5952
+ <span class="dashicons ps-icon-rocket" aria-hidden="true"></span>
5953
+ Release Management
5954
+ </button>
5955
+ </div>
5956
+ `;
5957
+ }
5958
+
5959
+ const visible = summary.attention.slice(0, 5);
5960
+ const rows = visible.map(dashboardReadinessRow).join("");
5961
+ const overflow = summary.attention.length > visible.length
5962
+ ? `<div class="ps-widget-footer"><button class="button button-ghost button-small" type="button" data-view-button="release">See all ${summary.attention.length} flagged plugins<span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span></button></div>`
5963
+ : "";
5964
+
5965
+ const blockedNote = summary.blocked
5966
+ ? `${summary.blocked} blocked from release`
5967
+ : `${summary.behind} behind WordPress.org`;
5968
+
5969
+ return `
5970
+ <p class="ps-widget-intro">${escapeHtml(`${summary.attention.length} plugin${summary.attention.length === 1 ? "" : "s"} need a fix before release — ${blockedNote}.`)}</p>
5971
+ <ul class="ps-attention-list">${rows}</ul>
5972
+ ${overflow}
5973
+ `;
5974
+ }
5975
+
5976
+ function dashboardReadinessRow({ plugin, issue }) {
5977
+ const name = plugin.name || plugin.slug || plugin.id;
5978
+ return `
5979
+ <li class="ps-attention-row ps-attention-${escapeAttr(issue.tone)}">
5980
+ <span class="ps-attention-icon" aria-hidden="true">
5981
+ <span class="dashicons ${escapeAttr(issue.icon)}"></span>
5982
+ </span>
5983
+ <button class="ps-attention-main" type="button" data-action="studio" data-scope="local" data-id="${escapeAttr(plugin.id)}" title="Open ${escapeAttr(name)} in Studio">
5984
+ <span class="ps-attention-text">
5985
+ <strong>${escapeHtml(name)}</strong>
5986
+ <small>${escapeHtml(issue.message)}</small>
5987
+ </span>
5988
+ </button>
5989
+ <span class="ps-attention-action">
5990
+ ${badge(issue.label, issue.tone)}
5991
+ <button class="button button-small" type="button" data-action="${escapeAttr(issue.action)}" data-id="${escapeAttr(plugin.id)}">${escapeHtml(issue.actionLabel)}</button>
5992
+ </span>
5993
+ </li>
5994
+ `;
5995
+ }
5996
+
5997
+ /* ===================================================================
5998
+ * Dashboard: Recent activity
5999
+ * =================================================================== */
6000
+
6001
+ function renderDashboardActivityWidget() {
6002
+ const jobs = Array.from(state.jobs.values()).sort((left, right) =>
6003
+ String(right.createdAt ?? "").localeCompare(String(left.createdAt ?? ""))
6004
+ );
6005
+ const running = jobs.filter((job) => job.status === "running" || job.status === "queued").length;
6006
+
6007
+ const body = jobs.length
6008
+ ? `<ul class="ps-activity-list">${jobs.slice(0, 5).map(dashboardActivityRow).join("")}</ul>`
6009
+ : `
6010
+ <div class="ps-side-empty ps-activity-empty">
6011
+ <span class="dashicons dashicons-clock" aria-hidden="true"></span>
6012
+ <span>No recent activity yet. Clone, check, dry-run, or release a plugin to see it here.</span>
6013
+ </div>
5675
6014
  `;
5676
- })
5677
- .join("");
5678
6015
 
5679
- const hint =
5680
- selected === "none"
5681
- ? "Pick an assistant in Settings to enable AI inside Studio."
5682
- : active?.status === "ready"
5683
- ? `${active.label} is ready in Studio.`
5684
- : active?.status === "not_authenticated"
5685
- ? `${active.label} is installed but not signed in.`
5686
- : `${active?.label ?? "Assistant"} ${active?.status === "not_installed" ? "is not on PATH." : "needs attention."}`;
6016
+ return renderDashboardPostbox({
6017
+ id: "dashboard-activity",
6018
+ title: "Recent activity",
6019
+ icon: "dashicons-backup",
6020
+ className: "ps-dashboard-main",
6021
+ actions: running ? `<span class="ps-count-pill">${running} active</span>` : "",
6022
+ body
6023
+ });
6024
+ }
6025
+
6026
+ function dashboardActivityRow(job) {
6027
+ const tone =
6028
+ job.status === "failed"
6029
+ ? "error"
6030
+ : job.status === "succeeded"
6031
+ ? "success"
6032
+ : job.status === "cancelled"
6033
+ ? "warning"
6034
+ : "info";
6035
+ return `
6036
+ <li class="ps-activity-row">
6037
+ <span class="ps-activity-icon ps-activity-${escapeAttr(tone)}" aria-hidden="true">
6038
+ <span class="dashicons ${jobIcon(job.type)}"></span>
6039
+ </span>
6040
+ <span class="ps-activity-text">
6041
+ <strong>${escapeHtml(job.title || jobTypeLabel(job.type))}</strong>
6042
+ <small>${escapeHtml(formatRelativeTime(job.createdAt))}</small>
6043
+ </span>
6044
+ ${badge(job.status, tone)}
6045
+ </li>
6046
+ `;
6047
+ }
6048
+
6049
+ function jobTypeLabel(type) {
6050
+ switch (type) {
6051
+ case "clone":
6052
+ return "Clone plugin";
6053
+ case "play":
6054
+ return "Launch Playground";
6055
+ case "check":
6056
+ return "Verify plugin";
6057
+ case "dry-run-publish":
6058
+ return "Dry-run publish";
6059
+ case "confirm-publish":
6060
+ return "Publish";
6061
+ default:
6062
+ return "Task";
6063
+ }
6064
+ }
6065
+
6066
+ function formatRelativeTime(iso) {
6067
+ if (!iso) {
6068
+ return "";
6069
+ }
6070
+ const then = new Date(iso).getTime();
6071
+ if (!Number.isFinite(then)) {
6072
+ return String(iso);
6073
+ }
6074
+ const diffSeconds = Math.round((Date.now() - then) / 1000);
6075
+ if (diffSeconds < 45) {
6076
+ return "just now";
6077
+ }
6078
+ const minutes = Math.round(diffSeconds / 60);
6079
+ if (minutes < 60) {
6080
+ return `${minutes}m ago`;
6081
+ }
6082
+ const hours = Math.round(minutes / 60);
6083
+ if (hours < 24) {
6084
+ return `${hours}h ago`;
6085
+ }
6086
+ const days = Math.round(hours / 24);
6087
+ if (days < 7) {
6088
+ return `${days}d ago`;
6089
+ }
6090
+ try {
6091
+ return new Date(iso).toLocaleDateString([], { month: "short", day: "numeric" });
6092
+ } catch {
6093
+ return String(iso);
6094
+ }
6095
+ }
6096
+
6097
+ /* ===================================================================
6098
+ * Dashboard: Validation health
6099
+ * =================================================================== */
6100
+
6101
+ function dashboardPluginCheckSummary() {
6102
+ let checked = 0;
6103
+ let errors = 0;
6104
+ let warnings = 0;
6105
+ let clean = 0;
6106
+ const flagged = [];
6107
+
6108
+ for (const plugin of state.local) {
6109
+ const entry = state.pluginCheckSummaries?.[plugin.id];
6110
+ if (!entry || !entry.summary) {
6111
+ continue;
6112
+ }
6113
+ checked += 1;
6114
+ const summary = entry.summary;
6115
+ if (summary.error > 0) {
6116
+ errors += 1;
6117
+ flagged.push({ plugin, entry, tone: "error" });
6118
+ } else if (summary.warning > 0) {
6119
+ warnings += 1;
6120
+ flagged.push({ plugin, entry, tone: "warning" });
6121
+ } else {
6122
+ clean += 1;
6123
+ }
6124
+ }
6125
+
6126
+ flagged.sort(
6127
+ (left, right) =>
6128
+ right.entry.summary.error - left.entry.summary.error ||
6129
+ right.entry.summary.warning - left.entry.summary.warning
6130
+ );
6131
+
6132
+ return { checked, errors, warnings, clean, unchecked: state.local.length - checked, flagged };
6133
+ }
6134
+
6135
+ function renderDashboardPluginCheckWidget() {
6136
+ if (!state.local.length) {
6137
+ return "";
6138
+ }
6139
+
6140
+ const summary = dashboardPluginCheckSummary();
6141
+
6142
+ let tone = "info";
6143
+ let label = "Not run";
6144
+ if (summary.checked > 0) {
6145
+ if (summary.errors > 0) {
6146
+ tone = "error";
6147
+ label = `${summary.errors} with errors`;
6148
+ } else if (summary.warnings > 0) {
6149
+ tone = "warning";
6150
+ label = `${summary.warnings} with warnings`;
6151
+ } else {
6152
+ tone = "success";
6153
+ label = "All clean";
6154
+ }
6155
+ }
6156
+
6157
+ let body;
6158
+ if (summary.checked === 0) {
6159
+ body = `
6160
+ <div class="ps-side-empty">
6161
+ <span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
6162
+ <span>No Verify results yet. Open a plugin in Studio and run Verify.</span>
6163
+ </div>
6164
+ `;
6165
+ } else if (!summary.flagged.length) {
6166
+ body = `
6167
+ <div class="ps-readiness-clear">
6168
+ <span class="ps-readiness-clear-icon" aria-hidden="true">
6169
+ <span class="dashicons dashicons-yes-alt"></span>
6170
+ </span>
6171
+ <div class="ps-readiness-clear-text">
6172
+ <strong>No findings</strong>
6173
+ <p>${escapeHtml(`All ${summary.checked} checked plugin${summary.checked === 1 ? "" : "s"} passed Verify.`)}</p>
6174
+ </div>
6175
+ </div>
6176
+ ${dashboardPluginCheckFooter(summary)}
6177
+ `;
6178
+ } else {
6179
+ const rows = summary.flagged.slice(0, 4).map(dashboardPluginCheckRow).join("");
6180
+ body = `
6181
+ <ul class="ps-attention-list">${rows}</ul>
6182
+ ${dashboardPluginCheckFooter(summary)}
6183
+ `;
6184
+ }
5687
6185
 
5688
6186
  return renderDashboardPostbox({
5689
- id: "dashboard-ai-assistance",
5690
- title: "AI Assistance",
5691
- icon: "dashicons-superhero",
6187
+ id: "dashboard-plugin-check",
6188
+ title: "Validation health",
6189
+ icon: "dashicons-shield",
5692
6190
  actions: badge(label, tone),
5693
- body: `
5694
- <div class="ps-ai-chips">${chips}</div>
5695
- <p class="ps-widget-hint">${escapeHtml(hint)}</p>
5696
- <div class="ps-widget-footer">
5697
- <button class="button button-ghost button-small" type="button" data-action="refresh-ai-assistance">
5698
- <span class="dashicons dashicons-update" aria-hidden="true"></span>
5699
- Refresh
5700
- </button>
5701
- <button class="button button-ghost button-small" type="button" data-view-button="settings">
5702
- Configure
5703
- <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
5704
- </button>
6191
+ body
6192
+ });
6193
+ }
6194
+
6195
+ function dashboardPluginCheckFooter(summary) {
6196
+ return `
6197
+ <small class="ps-widget-meta">${escapeHtml(
6198
+ `${summary.checked} of ${state.local.length} checked · ${summary.clean} clean${summary.unchecked ? ` · ${summary.unchecked} not run` : ""}`
6199
+ )}</small>
6200
+ `;
6201
+ }
6202
+
6203
+ function dashboardPluginCheckRow({ plugin, entry, tone }) {
6204
+ const name = entry.name || plugin.name || plugin.slug || plugin.id;
6205
+ const counts = [];
6206
+ if (entry.summary.error) {
6207
+ counts.push(`${entry.summary.error} error${entry.summary.error === 1 ? "" : "s"}`);
6208
+ }
6209
+ if (entry.summary.warning) {
6210
+ counts.push(`${entry.summary.warning} warning${entry.summary.warning === 1 ? "" : "s"}`);
6211
+ }
6212
+ const detail = `${counts.join(" · ")}${entry.checkedAt ? ` · ${formatRelativeTime(entry.checkedAt)}` : ""}`;
6213
+ return `
6214
+ <li class="ps-attention-row ps-attention-${escapeAttr(tone)}">
6215
+ <span class="ps-attention-icon" aria-hidden="true">
6216
+ <span class="dashicons ${tone === "error" ? "dashicons-warning" : "dashicons-flag"}"></span>
6217
+ </span>
6218
+ <button class="ps-attention-main" type="button" data-action="studio" data-scope="local" data-id="${escapeAttr(plugin.id)}" title="Open ${escapeAttr(name)} in Studio">
6219
+ <span class="ps-attention-text">
6220
+ <strong>${escapeHtml(name)}</strong>
6221
+ <small>${escapeHtml(detail)}</small>
6222
+ </span>
6223
+ </button>
6224
+ <span class="ps-attention-action">
6225
+ <button class="button button-small" type="button" data-action="studio" data-scope="local" data-id="${escapeAttr(plugin.id)}">Open</button>
6226
+ </span>
6227
+ </li>
6228
+ `;
6229
+ }
6230
+
6231
+ /* ===================================================================
6232
+ * Dashboard: Compatibility watch
6233
+ * =================================================================== */
6234
+
6235
+ function dashboardCompatibilitySummary() {
6236
+ const latest = state.latestWordPressVersion || "";
6237
+ const latestBranch = latest ? versionBranch(latest) : "";
6238
+ let behind = 0;
6239
+ let current = 0;
6240
+ let unknown = 0;
6241
+ const outdated = [];
6242
+
6243
+ for (const plugin of state.remote) {
6244
+ const tested = parseTestedWith(plugin.testedWith);
6245
+ if (!tested) {
6246
+ unknown += 1;
6247
+ continue;
6248
+ }
6249
+ if (latestBranch && compareVersionStrings(versionBranch(tested), latestBranch) < 0) {
6250
+ behind += 1;
6251
+ outdated.push({ plugin, tested });
6252
+ } else {
6253
+ current += 1;
6254
+ }
6255
+ }
6256
+
6257
+ outdated.sort((left, right) => compareVersionStrings(left.tested, right.tested));
6258
+ return { latest, latestBranch, behind, current, unknown, outdated };
6259
+ }
6260
+
6261
+ function renderDashboardCompatibilityWidget() {
6262
+ if (!state.bootstrap?.loggedIn || (!state.remote.length && !state.remoteLoading)) {
6263
+ return "";
6264
+ }
6265
+
6266
+ const summary = dashboardCompatibilitySummary();
6267
+ const latestLabel = summary.latest ? `WP ${summary.latest}` : "WordPress";
6268
+
6269
+ let tone = "info";
6270
+ let label = summary.latestBranch ? "Up to date" : "Unknown";
6271
+ if (summary.behind > 0) {
6272
+ tone = "warning";
6273
+ label = `${summary.behind} behind`;
6274
+ } else if (summary.latestBranch && summary.current > 0) {
6275
+ tone = "success";
6276
+ label = "Up to date";
6277
+ }
6278
+
6279
+ let body;
6280
+ if (state.remoteLoading) {
6281
+ body = `<div class="ps-side-empty"><span class="dashicons dashicons-update" aria-hidden="true"></span><span>Loading WordPress.org plugins…</span></div>`;
6282
+ } else if (!summary.latestBranch) {
6283
+ body = `<div class="ps-side-empty"><span class="dashicons dashicons-editor-help" aria-hidden="true"></span><span>Could not determine the latest WordPress version right now.</span></div>`;
6284
+ } else if (!summary.outdated.length) {
6285
+ body = `
6286
+ <div class="ps-readiness-clear">
6287
+ <span class="ps-readiness-clear-icon" aria-hidden="true">
6288
+ <span class="dashicons dashicons-yes-alt"></span>
6289
+ </span>
6290
+ <div class="ps-readiness-clear-text">
6291
+ <strong>${escapeHtml(`Tested with ${latestLabel}`)}</strong>
6292
+ <p>${escapeHtml(`All ${summary.current} plugin${summary.current === 1 ? "" : "s"} declare compatibility with the latest WordPress.`)}</p>
5705
6293
  </div>
5706
- <small class="ps-widget-meta">${installed.length} installed · ${ready.length} ready</small>
5707
- `
6294
+ </div>
6295
+ `;
6296
+ } else {
6297
+ const rows = summary.outdated.slice(0, 4).map((item) => dashboardCompatibilityRow(item, summary)).join("");
6298
+ body = `
6299
+ <p class="ps-widget-intro">${escapeHtml(`Latest WordPress is ${summary.latest}. Bump "Tested up to" so users keep installing.`)}</p>
6300
+ <ul class="ps-attention-list">${rows}</ul>
6301
+ `;
6302
+ }
6303
+
6304
+ return renderDashboardPostbox({
6305
+ id: "dashboard-compatibility",
6306
+ title: "Compatibility watch",
6307
+ icon: "dashicons-wordpress",
6308
+ actions: badge(label, tone),
6309
+ body
6310
+ });
6311
+ }
6312
+
6313
+ function dashboardCompatibilityRow({ plugin, tested }, summary) {
6314
+ return `
6315
+ <li class="ps-attention-row ps-attention-warning">
6316
+ <span class="ps-attention-icon" aria-hidden="true">
6317
+ <span class="dashicons dashicons-wordpress"></span>
6318
+ </span>
6319
+ <button class="ps-attention-main" type="button" data-action="details" data-scope="remote" data-id="${escapeAttr(plugin.slug)}" title="View ${escapeAttr(plugin.name || plugin.slug)}">
6320
+ <span class="ps-attention-text">
6321
+ <strong>${escapeHtml(plugin.name || plugin.slug)}</strong>
6322
+ <small>${escapeHtml(`Tested up to ${tested} · WordPress is ${summary.latest}`)}</small>
6323
+ </span>
6324
+ </button>
6325
+ <span class="ps-attention-action">
6326
+ ${badge(`${tested}`, "warning")}
6327
+ </span>
6328
+ </li>
6329
+ `;
6330
+ }
6331
+
6332
+ function parseTestedWith(value) {
6333
+ const match = String(value ?? "").match(/(\d+\.\d+(?:\.\d+)?)/);
6334
+ return match ? match[1] : "";
6335
+ }
6336
+
6337
+ function versionBranch(value) {
6338
+ const parts = String(value ?? "").split(".");
6339
+ return `${parts[0] ?? "0"}.${parts[1] ?? "0"}`;
6340
+ }
6341
+
6342
+ function compareVersionStrings(left, right) {
6343
+ const leftParts = String(left).split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
6344
+ const rightParts = String(right).split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
6345
+ for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
6346
+ const diff = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
6347
+ if (diff !== 0) {
6348
+ return diff > 0 ? 1 : -1;
6349
+ }
6350
+ }
6351
+ return 0;
6352
+ }
6353
+
6354
+ /* ===================================================================
6355
+ * Dashboard: WordPress.org reach
6356
+ * =================================================================== */
6357
+
6358
+ function dashboardReachSummary() {
6359
+ let total = 0;
6360
+ let known = false;
6361
+ let plus = false;
6362
+ let topName = "";
6363
+ let topCount = -1;
6364
+
6365
+ for (const plugin of state.remote) {
6366
+ const parsed = parseActiveInstalls(plugin.activeInstalls);
6367
+ if (!parsed.known) {
6368
+ continue;
6369
+ }
6370
+ known = true;
6371
+ total += parsed.count;
6372
+ if (parsed.plus) {
6373
+ plus = true;
6374
+ }
6375
+ if (parsed.count > topCount) {
6376
+ topCount = parsed.count;
6377
+ topName = plugin.name || plugin.slug || "";
6378
+ }
6379
+ }
6380
+
6381
+ return { total, known, plus, topName: total > 0 ? topName : "" };
6382
+ }
6383
+
6384
+ function parseActiveInstalls(value) {
6385
+ const text = String(value ?? "").replace(/,/g, "");
6386
+ const match = text.match(/(\d+)/);
6387
+ if (!match) {
6388
+ return { count: 0, plus: false, known: false };
6389
+ }
6390
+ return { count: Number.parseInt(match[1], 10), plus: /\+/.test(text), known: true };
6391
+ }
6392
+
6393
+ function formatInstallCount(value) {
6394
+ try {
6395
+ return Number(value).toLocaleString();
6396
+ } catch {
6397
+ return String(value);
6398
+ }
6399
+ }
6400
+
6401
+ /* ===================================================================
6402
+ * Dashboard: Getting-started checklist
6403
+ * =================================================================== */
6404
+
6405
+ function dashboardOnboardingSteps() {
6406
+ const loggedIn = Boolean(state.bootstrap?.loggedIn);
6407
+ const hasLocal = state.local.length > 0;
6408
+ const ranCheck = Object.values(state.pluginCheckSummaries ?? {}).some((entry) => entry?.summary);
6409
+ const released =
6410
+ state.local.some((plugin) => state.versionStates.get(plugin.id)?.remoteVersion) ||
6411
+ Array.from(state.jobs.values()).some(
6412
+ (job) => job.type === "confirm-publish" && job.status === "succeeded"
6413
+ );
6414
+
6415
+ return [
6416
+ {
6417
+ id: "login",
6418
+ done: loggedIn,
6419
+ label: "Connect your WordPress.org account",
6420
+ hint: "Run pressship login in a terminal, then refresh.",
6421
+ actionAttrs: 'data-view-button="settings"',
6422
+ actionLabel: "Settings"
6423
+ },
6424
+ {
6425
+ id: "add",
6426
+ done: hasLocal,
6427
+ label: "Add a local plugin",
6428
+ hint: "Point Pressship at a plugin folder you're working on.",
6429
+ actionAttrs: 'data-action="choose-local-folder"',
6430
+ actionLabel: "Choose Folder"
6431
+ },
6432
+ {
6433
+ id: "check",
6434
+ done: ranCheck,
6435
+ label: "Run Verify on a plugin",
6436
+ hint: "Open a plugin in Studio and run Verify.",
6437
+ actionAttrs: 'data-view-button="local"',
6438
+ actionLabel: "Open Library"
6439
+ },
6440
+ {
6441
+ id: "release",
6442
+ done: released,
6443
+ label: "Prepare or ship a release",
6444
+ hint: "Review version state and walk the publish funnel.",
6445
+ actionAttrs: 'data-view-button="release"',
6446
+ actionLabel: "Release"
6447
+ }
6448
+ ];
6449
+ }
6450
+
6451
+ function renderDashboardOnboardingCard() {
6452
+ if (state.localLoading && !state.local.length) {
6453
+ return "";
6454
+ }
6455
+
6456
+ const steps = dashboardOnboardingSteps();
6457
+ const done = steps.filter((step) => step.done).length;
6458
+ if (done === steps.length) {
6459
+ return "";
6460
+ }
6461
+
6462
+ return renderDashboardPostbox({
6463
+ id: "dashboard-getting-started",
6464
+ title: "Getting started",
6465
+ icon: "dashicons-flag",
6466
+ className: "ps-dashboard-main ps-onboarding-card",
6467
+ actions: `<span class="ps-count-pill">${done}/${steps.length}</span>`,
6468
+ body: `<ol class="ps-onboarding-list">${steps.map(dashboardOnboardingRow).join("")}</ol>`
5708
6469
  });
5709
6470
  }
5710
6471
 
6472
+ function dashboardOnboardingRow(step) {
6473
+ const action = step.done
6474
+ ? ""
6475
+ : `<button class="button button-small" type="button" ${step.actionAttrs}>${escapeHtml(step.actionLabel)}</button>`;
6476
+ return `
6477
+ <li class="ps-onboarding-row${step.done ? " is-done" : ""}">
6478
+ <span class="ps-onboarding-check" aria-hidden="true">
6479
+ <span class="dashicons ${step.done ? "dashicons-yes-alt" : "dashicons-marker"}"></span>
6480
+ </span>
6481
+ <span class="ps-onboarding-label">
6482
+ <strong>${escapeHtml(step.label)}</strong>
6483
+ ${step.hint && !step.done ? `<small>${escapeHtml(step.hint)}</small>` : ""}
6484
+ </span>
6485
+ ${action}
6486
+ </li>
6487
+ `;
6488
+ }
6489
+
5711
6490
  function renderPlaygroundsMenu() {
5712
6491
  if (!els.playgroundsSection || !els.playgroundsMenu) {
5713
6492
  return;
@@ -7851,13 +8630,13 @@ function renderStudioReleaseTagRow(tag) {
7851
8630
  function renderStudioReleaseStepValidate(versionState, release) {
7852
8631
  const summary = state.studio.checkSummary;
7853
8632
  const summaryLine = summary
7854
- ? `Plugin Check: ${escapeHtml(String(summary.error || 0))} errors, ${escapeHtml(String(summary.warning || 0))} warnings`
7855
- : "Plugin Check has not been run yet.";
8633
+ ? `Verify: ${escapeHtml(String(summary.error || 0))} errors, ${escapeHtml(String(summary.warning || 0))} warnings`
8634
+ : "Verify has not been run yet.";
7856
8635
  const validationBlocked = release.dryRun?.validationBlocked;
7857
8636
  const body = `
7858
8637
  <ul class="ps-release-validate-list">
7859
8638
  <li>
7860
- <span><span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>Plugin Check</span>
8639
+ <span><span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>Readme + Plugin Check</span>
7861
8640
  <span class="ps-release-validate-state">${escapeHtml(summary ? "Ran" : "Pending")}</span>
7862
8641
  <button class="button button-small" type="button" data-action="studio-check">${summary ? "Re-run" : "Run"}</button>
7863
8642
  </li>
@@ -7871,6 +8650,10 @@ function renderStudioReleaseStepValidate(versionState, release) {
7871
8650
  <span class="ps-release-validate-state">${escapeHtml(release.dryRun?.package?.file ? "Ready" : "Build on dry-run")}</span>
7872
8651
  </li>
7873
8652
  </ul>
8653
+ <label class="ps-release-skip-readme">
8654
+ <input type="checkbox" id="studio-skip-readme-validation" ${release.skipReadmeValidation ? "checked" : ""} ${state.studio.checking ? "disabled" : ""} />
8655
+ <span>skip readme validation</span>
8656
+ </label>
7874
8657
  ${validationBlocked
7875
8658
  ? `<p class="ps-release-step-error">Validation reported blocking findings. Fix them before publishing.</p>`
7876
8659
  : ""}