@jtalk22/slack-mcp 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -13
- package/docs/SETUP.md +64 -29
- package/docs/TROUBLESHOOTING.md +28 -0
- package/lib/handlers.js +156 -0
- package/lib/slack-client.js +11 -3
- package/lib/token-store.js +6 -5
- package/lib/tools.js +131 -0
- package/package.json +15 -8
- package/public/index.html +10 -6
- package/public/share.html +6 -5
- package/scripts/setup-wizard.js +1 -1
- package/server.json +8 -2
- package/src/server-http.js +16 -1
- package/src/server.js +31 -7
- package/src/web-server.js +117 -4
- package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +0 -67
- package/docs/COMMUNICATION-STYLE.md +0 -66
- package/docs/COMPATIBILITY.md +0 -19
- package/docs/DEPLOYMENT-MODES.md +0 -55
- package/docs/HN-LAUNCH.md +0 -72
- package/docs/INDEX.md +0 -41
- package/docs/INSTALL-PROOF.md +0 -18
- package/docs/LAUNCH-COPY-v3.0.0.md +0 -101
- package/docs/LAUNCH-MATRIX.md +0 -22
- package/docs/LAUNCH-OPS.md +0 -71
- package/docs/RELEASE-HEALTH.md +0 -77
- package/docs/SUPPORT-BOUNDARIES.md +0 -49
- package/docs/USE_CASE_RECIPES.md +0 -69
- package/docs/WEB-API.md +0 -303
- package/docs/images/demo-channel-messages.png +0 -0
- package/docs/images/demo-channels.png +0 -0
- package/docs/images/demo-claude-mobile-360x800.png +0 -0
- package/docs/images/demo-claude-mobile-390x844.png +0 -0
- package/docs/images/demo-claude-mobile-poster.png +0 -0
- package/docs/images/demo-main-mobile-360x800.png +0 -0
- package/docs/images/demo-main-mobile-390x844.png +0 -0
- package/docs/images/demo-main.png +0 -0
- package/docs/images/demo-messages.png +0 -0
- package/docs/images/demo-poster.png +0 -0
- package/docs/images/demo-sidebar.png +0 -0
- package/docs/images/diagram-oauth-comparison.svg +0 -80
- package/docs/images/diagram-session-flow.svg +0 -105
- package/docs/images/social-preview-v3.png +0 -0
- package/docs/images/web-api-mobile-360x800.png +0 -0
- package/docs/images/web-api-mobile-390x844.png +0 -0
- package/public/demo-claude.html +0 -1974
- package/public/demo-video.html +0 -244
- package/public/demo.html +0 -1196
- package/scripts/build-mobile-demo.js +0 -168
- package/scripts/build-release-health-delta.js +0 -201
- package/scripts/build-social-preview.js +0 -189
- package/scripts/capture-screenshots.js +0 -152
- package/scripts/check-owner-attribution.sh +0 -131
- package/scripts/check-public-language.sh +0 -26
- package/scripts/check-version-parity.js +0 -218
- package/scripts/cloudflare-browser-tool.js +0 -237
- package/scripts/collect-release-health.js +0 -162
- package/scripts/impact-push-v3.js +0 -781
- package/scripts/record-demo.js +0 -163
- package/scripts/release-preflight.js +0 -247
- package/scripts/setup-git-hooks.sh +0 -15
- package/scripts/update-github-social-preview.js +0 -208
- package/scripts/verify-core.js +0 -159
- package/scripts/verify-install-flow.js +0 -193
- package/scripts/verify-web.js +0 -273
package/scripts/record-demo.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Demo video recording script using Playwright
|
|
4
|
-
* Records the Claude Desktop demo in fullscreen mode with auto-play
|
|
5
|
-
*
|
|
6
|
-
* Usage: npm run record-demo
|
|
7
|
-
* Output: docs/videos/demo-claude-TIMESTAMP.webm
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { chromium } from 'playwright';
|
|
11
|
-
import { fileURLToPath } from 'url';
|
|
12
|
-
import { dirname, join } from 'path';
|
|
13
|
-
import { mkdirSync, existsSync, copyFileSync } from 'fs';
|
|
14
|
-
|
|
15
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
-
const __dirname = dirname(__filename);
|
|
17
|
-
const projectRoot = join(__dirname, '..');
|
|
18
|
-
const argv = process.argv.slice(2);
|
|
19
|
-
const hasArg = (flag) => argv.includes(flag);
|
|
20
|
-
const argValue = (flag) => {
|
|
21
|
-
const idx = argv.indexOf(flag);
|
|
22
|
-
return idx >= 0 && idx + 1 < argv.length ? argv[idx + 1] : null;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// Configuration
|
|
26
|
-
const CONFIG = {
|
|
27
|
-
viewport: { width: 1280, height: 800 },
|
|
28
|
-
speed: '0.5', // Slow speed for video recording
|
|
29
|
-
scenarioCount: 5,
|
|
30
|
-
initialHold: 500,
|
|
31
|
-
// Title and closing card durations (ms)
|
|
32
|
-
introDuration: 4000, // Title card (3s visible + fade)
|
|
33
|
-
outroDuration: 5500, // Closing card (4s visible + fades)
|
|
34
|
-
// Approximate duration per scenario at 0.5x speed (in ms)
|
|
35
|
-
scenarioDurations: {
|
|
36
|
-
search: 26000, // +1s for transition fade
|
|
37
|
-
thread: 26000,
|
|
38
|
-
list: 21000,
|
|
39
|
-
send: 19000,
|
|
40
|
-
multi: 36000
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const canonicalOutput = argValue('--out') || join(projectRoot, 'docs', 'videos', 'demo-claude.webm');
|
|
45
|
-
const archiveOutput = hasArg('--archive');
|
|
46
|
-
|
|
47
|
-
async function recordDemo() {
|
|
48
|
-
console.log('╔════════════════════════════════════════════════════════════╗');
|
|
49
|
-
console.log('║ Slack MCP Server - Demo Video Recording ║');
|
|
50
|
-
console.log('╚════════════════════════════════════════════════════════════╝');
|
|
51
|
-
console.log();
|
|
52
|
-
|
|
53
|
-
// Ensure videos directory exists
|
|
54
|
-
const videosDir = join(projectRoot, 'docs', 'videos');
|
|
55
|
-
if (!existsSync(videosDir)) {
|
|
56
|
-
mkdirSync(videosDir, { recursive: true });
|
|
57
|
-
console.log(`📁 Created directory: ${videosDir}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
61
|
-
const timestampedOutput = join(videosDir, `demo-claude-${timestamp}.webm`);
|
|
62
|
-
|
|
63
|
-
console.log('🚀 Launching browser...');
|
|
64
|
-
const browser = await chromium.launch({
|
|
65
|
-
headless: true,
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const context = await browser.newContext({
|
|
69
|
-
viewport: CONFIG.viewport,
|
|
70
|
-
recordVideo: {
|
|
71
|
-
dir: videosDir,
|
|
72
|
-
size: CONFIG.viewport
|
|
73
|
-
},
|
|
74
|
-
colorScheme: 'dark'
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const page = await context.newPage();
|
|
78
|
-
|
|
79
|
-
// Load the demo
|
|
80
|
-
const demoPath = join(projectRoot, 'public', 'demo-claude.html');
|
|
81
|
-
console.log(`📄 Loading: ${demoPath}`);
|
|
82
|
-
await page.goto(`file://${demoPath}`);
|
|
83
|
-
await page.waitForTimeout(1000);
|
|
84
|
-
|
|
85
|
-
// Keep first frame brief so autoplay reaches full-screen flow quickly.
|
|
86
|
-
console.log(`⏸️ Holding initial frame (${CONFIG.initialHold}ms)...`);
|
|
87
|
-
await page.waitForTimeout(CONFIG.initialHold);
|
|
88
|
-
|
|
89
|
-
// Set slow speed for video recording
|
|
90
|
-
console.log(`⏱️ Setting speed to ${CONFIG.speed}x...`);
|
|
91
|
-
await page.selectOption('#speedSelect', CONFIG.speed);
|
|
92
|
-
await page.waitForTimeout(300);
|
|
93
|
-
|
|
94
|
-
// Start auto-play BEFORE entering fullscreen (button hidden in fullscreen)
|
|
95
|
-
console.log('▶️ Starting auto-play...');
|
|
96
|
-
await page.click('#autoPlayBtn');
|
|
97
|
-
await page.waitForTimeout(500);
|
|
98
|
-
|
|
99
|
-
// Now enter fullscreen mode
|
|
100
|
-
console.log('🖥️ Entering fullscreen mode...');
|
|
101
|
-
console.log();
|
|
102
|
-
await page.keyboard.press('f');
|
|
103
|
-
await page.waitForTimeout(500);
|
|
104
|
-
|
|
105
|
-
// Wait for title card
|
|
106
|
-
console.log('📺 Showing title card...');
|
|
107
|
-
await page.waitForTimeout(CONFIG.introDuration);
|
|
108
|
-
|
|
109
|
-
// Wait for each scenario
|
|
110
|
-
const scenarios = ['search', 'thread', 'list', 'send', 'multi'];
|
|
111
|
-
let totalWait = 0;
|
|
112
|
-
|
|
113
|
-
for (let i = 0; i < scenarios.length; i++) {
|
|
114
|
-
const scenario = scenarios[i];
|
|
115
|
-
const duration = CONFIG.scenarioDurations[scenario];
|
|
116
|
-
totalWait += duration;
|
|
117
|
-
|
|
118
|
-
console.log(` 📍 Scenario ${i + 1}/${scenarios.length}: ${scenario} (${Math.round(duration/1000)}s)`);
|
|
119
|
-
await page.waitForTimeout(duration);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Wait for closing card
|
|
123
|
-
console.log();
|
|
124
|
-
console.log('🎬 Showing closing screen...');
|
|
125
|
-
await page.waitForTimeout(CONFIG.outroDuration);
|
|
126
|
-
|
|
127
|
-
// Exit fullscreen
|
|
128
|
-
console.log('🔚 Exiting fullscreen...');
|
|
129
|
-
await page.keyboard.press('Escape');
|
|
130
|
-
await page.waitForTimeout(500);
|
|
131
|
-
|
|
132
|
-
// Close context to flush video
|
|
133
|
-
console.log('💾 Saving video...');
|
|
134
|
-
const video = await page.video();
|
|
135
|
-
await context.close();
|
|
136
|
-
await browser.close();
|
|
137
|
-
|
|
138
|
-
// Get the actual video path
|
|
139
|
-
const videoPath = await video.path();
|
|
140
|
-
|
|
141
|
-
console.log();
|
|
142
|
-
console.log('╔════════════════════════════════════════════════════════════╗');
|
|
143
|
-
console.log('║ ✅ Recording Complete! ║');
|
|
144
|
-
console.log('╚════════════════════════════════════════════════════════════╝');
|
|
145
|
-
console.log();
|
|
146
|
-
copyFileSync(videoPath, canonicalOutput);
|
|
147
|
-
console.log(`📹 Canonical video: ${canonicalOutput}`);
|
|
148
|
-
if (archiveOutput) {
|
|
149
|
-
copyFileSync(videoPath, timestampedOutput);
|
|
150
|
-
console.log(`🗂️ Archived copy: ${timestampedOutput}`);
|
|
151
|
-
}
|
|
152
|
-
console.log();
|
|
153
|
-
console.log('Next steps:');
|
|
154
|
-
console.log(' 1. Review the canonical video in a media player');
|
|
155
|
-
console.log(' 2. Convert to GIF with FFmpeg:');
|
|
156
|
-
console.log(` ffmpeg -i "${canonicalOutput}" -vf "fps=15,scale=800:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" docs/images/demo-claude.gif`);
|
|
157
|
-
console.log(' 3. Re-run with --archive to keep timestamped historical outputs');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
recordDemo().catch(err => {
|
|
161
|
-
console.error('❌ Recording failed:', err.message);
|
|
162
|
-
process.exit(1);
|
|
163
|
-
});
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { spawnSync } from "child_process";
|
|
4
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
5
|
-
import { dirname, resolve } from "path";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
7
|
-
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const ROOT = resolve(__dirname, "..");
|
|
10
|
-
const REPORT_PATH = process.env.PREPUBLISH_REPORT_PATH
|
|
11
|
-
? resolve(ROOT, process.env.PREPUBLISH_REPORT_PATH)
|
|
12
|
-
: resolve(ROOT, "output", "release-health", "prepublish-dry-run.md");
|
|
13
|
-
|
|
14
|
-
const EXPECTED_NAME = process.env.EXPECTED_GIT_NAME || "jtalk22";
|
|
15
|
-
const EXPECTED_EMAIL = process.env.EXPECTED_GIT_EMAIL || "james@revasser.nyc";
|
|
16
|
-
const OWNER_RANGE = process.env.OWNER_CHECK_RANGE || "origin/main..HEAD";
|
|
17
|
-
|
|
18
|
-
function run(command, args = [], options = {}) {
|
|
19
|
-
return spawnSync(command, args, {
|
|
20
|
-
cwd: ROOT,
|
|
21
|
-
encoding: "utf8",
|
|
22
|
-
env: process.env,
|
|
23
|
-
maxBuffer: 20 * 1024 * 1024,
|
|
24
|
-
...options
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function trimOutput(text = "", maxChars = 1200) {
|
|
29
|
-
const normalized = String(text || "").trim();
|
|
30
|
-
if (!normalized) return "";
|
|
31
|
-
if (normalized.length <= maxChars) return normalized;
|
|
32
|
-
return `${normalized.slice(0, maxChars)}... [truncated]`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function stepResult(name, command, ok, details = "", commandOutput = "") {
|
|
36
|
-
return { name, command, ok, details, commandOutput };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function gitIdentityStep() {
|
|
40
|
-
const nameResult = run("git", ["config", "--get", "user.name"]);
|
|
41
|
-
const emailResult = run("git", ["config", "--get", "user.email"]);
|
|
42
|
-
|
|
43
|
-
const actualName = nameResult.stdout.trim();
|
|
44
|
-
const actualEmail = emailResult.stdout.trim();
|
|
45
|
-
const ok =
|
|
46
|
-
nameResult.status === 0 &&
|
|
47
|
-
emailResult.status === 0 &&
|
|
48
|
-
actualName === EXPECTED_NAME &&
|
|
49
|
-
actualEmail === EXPECTED_EMAIL;
|
|
50
|
-
|
|
51
|
-
const details = ok
|
|
52
|
-
? `Configured as ${actualName} <${actualEmail}>`
|
|
53
|
-
: `Expected ${EXPECTED_NAME} <${EXPECTED_EMAIL}>, found ${actualName || "(missing)"} <${actualEmail || "(missing)"}>`;
|
|
54
|
-
|
|
55
|
-
return stepResult(
|
|
56
|
-
"Git identity",
|
|
57
|
-
"git config --get user.name && git config --get user.email",
|
|
58
|
-
ok,
|
|
59
|
-
details,
|
|
60
|
-
`${nameResult.stdout}${nameResult.stderr}${emailResult.stdout}${emailResult.stderr}`
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function ownerAttributionStep() {
|
|
65
|
-
const result = run("bash", ["scripts/check-owner-attribution.sh", OWNER_RANGE]);
|
|
66
|
-
return stepResult(
|
|
67
|
-
"Owner attribution",
|
|
68
|
-
`bash scripts/check-owner-attribution.sh ${OWNER_RANGE}`,
|
|
69
|
-
result.status === 0,
|
|
70
|
-
result.status === 0 ? "All commits in range are owner-attributed." : "Owner attribution check failed.",
|
|
71
|
-
`${result.stdout}${result.stderr}`
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function publicLanguageStep() {
|
|
76
|
-
const result = run("bash", ["scripts/check-public-language.sh"]);
|
|
77
|
-
return stepResult(
|
|
78
|
-
"Public language",
|
|
79
|
-
"bash scripts/check-public-language.sh",
|
|
80
|
-
result.status === 0,
|
|
81
|
-
result.status === 0 ? "Public wording guardrail passed." : "Disallowed wording found.",
|
|
82
|
-
`${result.stdout}${result.stderr}`
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function markerScanStep() {
|
|
87
|
-
const pattern = "Co-authored-by|co-authored-by|Generated with|generated with";
|
|
88
|
-
const scanPaths = [
|
|
89
|
-
"README.md",
|
|
90
|
-
"docs",
|
|
91
|
-
"public",
|
|
92
|
-
".github/RELEASE_NOTES_TEMPLATE.md",
|
|
93
|
-
".github/ISSUE_REPLY_TEMPLATE.md"
|
|
94
|
-
];
|
|
95
|
-
const result = run("rg", [
|
|
96
|
-
"-n",
|
|
97
|
-
pattern,
|
|
98
|
-
"--glob",
|
|
99
|
-
"!docs/release-health/**",
|
|
100
|
-
"--glob",
|
|
101
|
-
"!output/release-health/**",
|
|
102
|
-
...scanPaths
|
|
103
|
-
]);
|
|
104
|
-
|
|
105
|
-
if (result.status === 1) {
|
|
106
|
-
return stepResult(
|
|
107
|
-
"Public attribution markers",
|
|
108
|
-
"marker-scan",
|
|
109
|
-
true,
|
|
110
|
-
"No non-owner attribution markers found in public surfaces."
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return stepResult(
|
|
115
|
-
"Public attribution markers",
|
|
116
|
-
"marker-scan",
|
|
117
|
-
false,
|
|
118
|
-
result.status === 0
|
|
119
|
-
? "Found disallowed markers on public surfaces."
|
|
120
|
-
: "Marker scan failed.",
|
|
121
|
-
`${result.stdout}${result.stderr}`
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function runNodeStep(name, scriptPath, extraArgs = []) {
|
|
126
|
-
const result = run("node", [scriptPath, ...extraArgs]);
|
|
127
|
-
return stepResult(
|
|
128
|
-
name,
|
|
129
|
-
`node ${scriptPath}${extraArgs.length ? ` ${extraArgs.join(" ")}` : ""}`,
|
|
130
|
-
result.status === 0,
|
|
131
|
-
result.status === 0 ? "Passed." : "Failed.",
|
|
132
|
-
`${result.stdout}${result.stderr}`
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function npmPackSnapshot() {
|
|
137
|
-
const result = run("npm", ["pack", "--dry-run", "--json"]);
|
|
138
|
-
if (result.status !== 0) {
|
|
139
|
-
return {
|
|
140
|
-
ok: false,
|
|
141
|
-
details: "Unable to generate npm pack snapshot.",
|
|
142
|
-
output: `${result.stdout}${result.stderr}`
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
const parsed = JSON.parse(result.stdout);
|
|
148
|
-
const entry = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
149
|
-
const fileCount = Array.isArray(entry.files) ? entry.files.length : 0;
|
|
150
|
-
const details = `package size ${entry.size} bytes, unpacked ${entry.unpackedSize} bytes, files ${fileCount}`;
|
|
151
|
-
return { ok: true, details, output: result.stdout };
|
|
152
|
-
} catch (error) {
|
|
153
|
-
return {
|
|
154
|
-
ok: false,
|
|
155
|
-
details: "npm pack output was not valid JSON.",
|
|
156
|
-
output: `${result.stdout}\n${String(error)}`
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function buildReport(results, packSnapshot) {
|
|
162
|
-
const generated = new Date().toISOString();
|
|
163
|
-
const failed = results.filter((step) => !step.ok);
|
|
164
|
-
const lines = [];
|
|
165
|
-
lines.push("# Prepublish Dry Run");
|
|
166
|
-
lines.push("");
|
|
167
|
-
lines.push(`- Generated: ${generated}`);
|
|
168
|
-
lines.push(`- Expected owner: \`${EXPECTED_NAME} <${EXPECTED_EMAIL}>\``);
|
|
169
|
-
lines.push(`- Owner range: \`${OWNER_RANGE}\``);
|
|
170
|
-
lines.push("");
|
|
171
|
-
lines.push("## Step Matrix");
|
|
172
|
-
lines.push("");
|
|
173
|
-
lines.push("| Step | Status | Command | Details |");
|
|
174
|
-
lines.push("|---|---|---|---|");
|
|
175
|
-
for (const step of results) {
|
|
176
|
-
lines.push(`| ${step.name} | ${step.ok ? "pass" : "fail"} | \`${step.command}\` | ${step.details} |`);
|
|
177
|
-
}
|
|
178
|
-
lines.push("");
|
|
179
|
-
lines.push("## npm Pack Snapshot");
|
|
180
|
-
lines.push("");
|
|
181
|
-
lines.push(`- Status: ${packSnapshot.ok ? "pass" : "fail"}`);
|
|
182
|
-
lines.push(`- Details: ${packSnapshot.details}`);
|
|
183
|
-
lines.push("");
|
|
184
|
-
|
|
185
|
-
if (failed.length === 0 && packSnapshot.ok) {
|
|
186
|
-
lines.push("## Result");
|
|
187
|
-
lines.push("");
|
|
188
|
-
lines.push("Prepublish dry run passed.");
|
|
189
|
-
} else {
|
|
190
|
-
lines.push("## Result");
|
|
191
|
-
lines.push("");
|
|
192
|
-
lines.push("Prepublish dry run failed.");
|
|
193
|
-
lines.push("");
|
|
194
|
-
lines.push("### Failing checks");
|
|
195
|
-
for (const step of failed) {
|
|
196
|
-
lines.push(`- ${step.name}`);
|
|
197
|
-
}
|
|
198
|
-
if (!packSnapshot.ok) lines.push("- npm pack snapshot");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
lines.push("");
|
|
202
|
-
lines.push("## Command Output (Truncated)");
|
|
203
|
-
lines.push("");
|
|
204
|
-
for (const step of results) {
|
|
205
|
-
lines.push(`### ${step.name}`);
|
|
206
|
-
lines.push("");
|
|
207
|
-
lines.push("```text");
|
|
208
|
-
lines.push(trimOutput(step.commandOutput) || "(no output)");
|
|
209
|
-
lines.push("```");
|
|
210
|
-
lines.push("");
|
|
211
|
-
}
|
|
212
|
-
lines.push("### npm Pack Snapshot");
|
|
213
|
-
lines.push("");
|
|
214
|
-
lines.push("```text");
|
|
215
|
-
lines.push(trimOutput(packSnapshot.output, 4000) || "(no output)");
|
|
216
|
-
lines.push("```");
|
|
217
|
-
lines.push("");
|
|
218
|
-
|
|
219
|
-
return lines.join("\n");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function main() {
|
|
223
|
-
const steps = [
|
|
224
|
-
gitIdentityStep(),
|
|
225
|
-
ownerAttributionStep(),
|
|
226
|
-
publicLanguageStep(),
|
|
227
|
-
markerScanStep(),
|
|
228
|
-
runNodeStep("Core verification", "scripts/verify-core.js"),
|
|
229
|
-
runNodeStep("Web verification", "scripts/verify-web.js"),
|
|
230
|
-
runNodeStep("Install-flow verification", "scripts/verify-install-flow.js"),
|
|
231
|
-
runNodeStep("Version parity", "scripts/check-version-parity.js", ["--allow-propagation"])
|
|
232
|
-
];
|
|
233
|
-
|
|
234
|
-
const packSnapshot = npmPackSnapshot();
|
|
235
|
-
const report = buildReport(steps, packSnapshot);
|
|
236
|
-
|
|
237
|
-
mkdirSync(dirname(REPORT_PATH), { recursive: true });
|
|
238
|
-
writeFileSync(REPORT_PATH, `${report}\n`);
|
|
239
|
-
|
|
240
|
-
console.log(`Wrote ${REPORT_PATH}`);
|
|
241
|
-
const failed = steps.some((step) => !step.ok) || !packSnapshot.ok;
|
|
242
|
-
if (failed) {
|
|
243
|
-
process.exitCode = 1;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
main();
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
repo_root="$(git rev-parse --show-toplevel)"
|
|
5
|
-
cd "$repo_root"
|
|
6
|
-
|
|
7
|
-
[[ -d .githooks ]] || {
|
|
8
|
-
echo "Missing .githooks directory." >&2
|
|
9
|
-
exit 1
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
git config core.hooksPath .githooks
|
|
13
|
-
find .githooks -maxdepth 1 -type f -exec chmod +x {} +
|
|
14
|
-
|
|
15
|
-
echo "Configured git hooks path: .githooks"
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { chromium } from "playwright";
|
|
4
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
5
|
-
import { dirname, resolve } from "node:path";
|
|
6
|
-
|
|
7
|
-
const argv = process.argv.slice(2);
|
|
8
|
-
const hasFlag = (flag) => argv.includes(flag);
|
|
9
|
-
const argValue = (flag) => {
|
|
10
|
-
const idx = argv.indexOf(flag);
|
|
11
|
-
return idx >= 0 && idx + 1 < argv.length ? argv[idx + 1] : null;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const repo = argValue("--repo") || "jtalk22/slack-mcp-server";
|
|
15
|
-
const imagePath = resolve(
|
|
16
|
-
argValue("--image") || "docs/images/social-preview-v3.png"
|
|
17
|
-
);
|
|
18
|
-
const profileDir = resolve(
|
|
19
|
-
argValue("--profile-dir") || ".cache/playwright/github-social-preview"
|
|
20
|
-
);
|
|
21
|
-
const evidencePath = resolve(
|
|
22
|
-
argValue("--evidence") || "output/release-health/social-preview-settings.png"
|
|
23
|
-
);
|
|
24
|
-
const headed = hasFlag("--headed");
|
|
25
|
-
const loginTimeoutMs = Number(argValue("--login-timeout-ms") || 10 * 60 * 1000);
|
|
26
|
-
const settingsUrl = `https://github.com/${repo}/settings`;
|
|
27
|
-
|
|
28
|
-
if (!existsSync(imagePath)) {
|
|
29
|
-
console.error(`Missing image: ${imagePath}`);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
console.log(`Repo: ${repo}`);
|
|
34
|
-
console.log(`Image: ${imagePath}`);
|
|
35
|
-
console.log(`Profile: ${profileDir}`);
|
|
36
|
-
console.log(`Opening: ${settingsUrl}`);
|
|
37
|
-
|
|
38
|
-
mkdirSync(profileDir, { recursive: true });
|
|
39
|
-
mkdirSync(dirname(evidencePath), { recursive: true });
|
|
40
|
-
|
|
41
|
-
const context = await chromium.launchPersistentContext(profileDir, {
|
|
42
|
-
channel: "chrome",
|
|
43
|
-
headless: !headed,
|
|
44
|
-
viewport: { width: 1440, height: 1100 },
|
|
45
|
-
});
|
|
46
|
-
const page = await context.newPage();
|
|
47
|
-
|
|
48
|
-
const getSocialPreviewImageSrc = async () => {
|
|
49
|
-
return page.evaluate(() => {
|
|
50
|
-
const extractRepoImageUrl = (value) => {
|
|
51
|
-
if (!value || value === "none") return null;
|
|
52
|
-
const match = value.match(/https:\/\/repository-images\.githubusercontent\.com\/[^")]+/);
|
|
53
|
-
return match ? match[0] : null;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const headings = Array.from(document.querySelectorAll("h1, h2, h3, h4, strong"));
|
|
57
|
-
const socialHeading = headings.find((el) => el.textContent?.trim() === "Social preview");
|
|
58
|
-
if (!socialHeading) return null;
|
|
59
|
-
|
|
60
|
-
let node = socialHeading.parentElement;
|
|
61
|
-
for (let depth = 0; depth < 6 && node; depth += 1) {
|
|
62
|
-
const img = node.querySelector("img");
|
|
63
|
-
if (img?.src) return img.src;
|
|
64
|
-
|
|
65
|
-
const ownBg = extractRepoImageUrl(getComputedStyle(node).backgroundImage);
|
|
66
|
-
if (ownBg) return ownBg;
|
|
67
|
-
|
|
68
|
-
const descendants = Array.from(node.querySelectorAll("*"));
|
|
69
|
-
for (const child of descendants) {
|
|
70
|
-
const bg = extractRepoImageUrl(getComputedStyle(child).backgroundImage);
|
|
71
|
-
if (bg) return bg;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
node = node.parentElement;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const fallback = Array.from(document.querySelectorAll("img")).find((img) =>
|
|
78
|
-
img.src.includes("repository-images.githubusercontent.com")
|
|
79
|
-
);
|
|
80
|
-
if (fallback?.src) return fallback.src;
|
|
81
|
-
|
|
82
|
-
const bgFallback = Array.from(document.querySelectorAll("*"))
|
|
83
|
-
.map((el) => extractRepoImageUrl(getComputedStyle(el).backgroundImage))
|
|
84
|
-
.find(Boolean);
|
|
85
|
-
return bgFallback || null;
|
|
86
|
-
});
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const waitForAuthenticatedSettings = async () => {
|
|
90
|
-
const started = Date.now();
|
|
91
|
-
while (Date.now() - started < loginTimeoutMs) {
|
|
92
|
-
const url = page.url();
|
|
93
|
-
const title = await page.title().catch(() => "");
|
|
94
|
-
const onSettings = url.includes(`/${repo}/settings`) && !title.includes("Page not found");
|
|
95
|
-
if (onSettings) return;
|
|
96
|
-
await page.waitForTimeout(1200);
|
|
97
|
-
}
|
|
98
|
-
throw new Error(
|
|
99
|
-
`Timed out waiting for authenticated settings page after ${loginTimeoutMs}ms`
|
|
100
|
-
);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
await page.goto(settingsUrl, { waitUntil: "domcontentloaded", timeout: 120000 });
|
|
105
|
-
|
|
106
|
-
const initialTitle = await page.title().catch(() => "");
|
|
107
|
-
if (initialTitle.includes("Page not found") || page.url().includes("/login")) {
|
|
108
|
-
console.log("GitHub settings not authenticated in this browser context.");
|
|
109
|
-
console.log("Please sign in in the opened browser window, then keep this process running.");
|
|
110
|
-
await waitForAuthenticatedSettings();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// GitHub settings pages often keep long-lived background requests, so
|
|
114
|
-
// networkidle can hang even when UI is interactive.
|
|
115
|
-
await page.waitForLoadState("domcontentloaded", { timeout: 120000 });
|
|
116
|
-
|
|
117
|
-
const socialHeading = page.getByText("Social preview", { exact: true }).first();
|
|
118
|
-
await socialHeading.waitFor({ timeout: 120000 });
|
|
119
|
-
await socialHeading.scrollIntoViewIfNeeded();
|
|
120
|
-
const beforeImageSrc = await getSocialPreviewImageSrc();
|
|
121
|
-
|
|
122
|
-
// In current GitHub settings, upload is often behind an Edit button.
|
|
123
|
-
const editButtons = page.getByRole("button", { name: /^Edit$/i });
|
|
124
|
-
const editCount = await editButtons.count();
|
|
125
|
-
if (editCount > 0) {
|
|
126
|
-
for (let i = 0; i < editCount; i += 1) {
|
|
127
|
-
const btn = editButtons.nth(i);
|
|
128
|
-
if (await btn.isVisible()) {
|
|
129
|
-
try {
|
|
130
|
-
await btn.click({ timeout: 3000 });
|
|
131
|
-
break;
|
|
132
|
-
} catch {
|
|
133
|
-
// Try next visible Edit button.
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const fileInput = page.locator('#repo-image-file-input, input[type="file"]').first();
|
|
140
|
-
await fileInput.waitFor({ state: "attached", timeout: 90000 });
|
|
141
|
-
await fileInput.setInputFiles(imagePath);
|
|
142
|
-
await page.waitForTimeout(1500);
|
|
143
|
-
const afterUploadImageSrc = await getSocialPreviewImageSrc();
|
|
144
|
-
const previewUpdated =
|
|
145
|
-
Boolean(afterUploadImageSrc) && afterUploadImageSrc !== beforeImageSrc;
|
|
146
|
-
const previewAlreadyPresent =
|
|
147
|
-
Boolean(beforeImageSrc) &&
|
|
148
|
-
Boolean(afterUploadImageSrc) &&
|
|
149
|
-
beforeImageSrc === afterUploadImageSrc;
|
|
150
|
-
|
|
151
|
-
// GitHub UI labels can vary; try likely save/update actions.
|
|
152
|
-
const saveCandidates = [
|
|
153
|
-
/update social preview/i,
|
|
154
|
-
/update social image/i,
|
|
155
|
-
/save changes/i,
|
|
156
|
-
/^save$/i,
|
|
157
|
-
/upload/i,
|
|
158
|
-
/^apply$/i,
|
|
159
|
-
/^done$/i,
|
|
160
|
-
];
|
|
161
|
-
|
|
162
|
-
let clicked = false;
|
|
163
|
-
for (const rx of saveCandidates) {
|
|
164
|
-
const button = page.getByRole("button", { name: rx }).first();
|
|
165
|
-
const count = await button.count();
|
|
166
|
-
if (count > 0) {
|
|
167
|
-
try {
|
|
168
|
-
await button.scrollIntoViewIfNeeded();
|
|
169
|
-
await button.click({ timeout: 5000 });
|
|
170
|
-
clicked = true;
|
|
171
|
-
break;
|
|
172
|
-
} catch {
|
|
173
|
-
// Keep trying other candidates.
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (!clicked && !previewUpdated) {
|
|
179
|
-
const visibleButtons = await page
|
|
180
|
-
.locator("button:visible")
|
|
181
|
-
.evaluateAll((els) => els.map((el) => el.textContent?.trim() || "").filter(Boolean))
|
|
182
|
-
.catch(() => []);
|
|
183
|
-
console.log("Visible buttons during upload:", visibleButtons.join(" | "));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (previewUpdated) {
|
|
187
|
-
console.log("Social preview image updated in settings preview.");
|
|
188
|
-
} else if (previewAlreadyPresent) {
|
|
189
|
-
console.log("Social preview image is already present; no save action required.");
|
|
190
|
-
} else if (!clicked) {
|
|
191
|
-
console.log(
|
|
192
|
-
"Uploaded image file to input; could not confidently click a save button automatically."
|
|
193
|
-
);
|
|
194
|
-
console.log("Complete the final click in the open browser if needed.");
|
|
195
|
-
} else {
|
|
196
|
-
await page.waitForTimeout(2500);
|
|
197
|
-
console.log("Social preview update action submitted.");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
await page.screenshot({ path: evidencePath, fullPage: true });
|
|
201
|
-
console.log(`Saved evidence screenshot: ${evidencePath}`);
|
|
202
|
-
console.log("Done.");
|
|
203
|
-
} catch (error) {
|
|
204
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
205
|
-
process.exitCode = 1;
|
|
206
|
-
} finally {
|
|
207
|
-
await context.close();
|
|
208
|
-
}
|