staklink 0.3.25 → 0.3.26

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.
@@ -34824,7 +34824,7 @@ var SSEManager = class {
34824
34824
  var sseManager = new SSEManager();
34825
34825
 
34826
34826
  // src/proxy/version.ts
34827
- var VERSION = "0.3.25";
34827
+ var VERSION = "0.3.26";
34828
34828
 
34829
34829
  // node_modules/uuid/dist/esm/stringify.js
34830
34830
  var byteToHex = [];
@@ -80756,6 +80756,189 @@ Stderr: ${error82.stderr}`
80756
80756
  // src/proxy/playwright.ts
80757
80757
  var fs10 = __toESM(require("fs/promises"), 1);
80758
80758
  var path9 = __toESM(require("path"), 1);
80759
+ var DEFAULT_CONFIG = `import { defineConfig } from "@playwright/test";
80760
+ export default defineConfig({
80761
+ timeout: 60000,
80762
+ workers: 1,
80763
+ fullyParallel: false,
80764
+ reporter: [
80765
+ ["list"],
80766
+ ["./timestamp-reporter.ts"],
80767
+ ],
80768
+ use: {
80769
+ video: "on",
80770
+ headless: true,
80771
+ browserName: "chromium",
80772
+ trace: "on-first-retry",
80773
+ },
80774
+ });
80775
+ `;
80776
+ var TIMESTAMP_REPORTER_CODE = `import {
80777
+ Reporter,
80778
+ TestCase,
80779
+ TestResult,
80780
+ TestStep,
80781
+ FullConfig,
80782
+ Suite,
80783
+ FullResult,
80784
+ } from "@playwright/test/reporter";
80785
+ import * as fs from "fs";
80786
+ import * as path from "path";
80787
+
80788
+ interface TimestampEntry {
80789
+ title: string;
80790
+ category: string;
80791
+ timestamp: string;
80792
+ relativeMs: number;
80793
+ duration: number;
80794
+ location: {
80795
+ file: string;
80796
+ line: number;
80797
+ column: number;
80798
+ };
80799
+ stepPath: string[];
80800
+ sourceCode?: string;
80801
+ error?: {
80802
+ message: string;
80803
+ stack?: string;
80804
+ };
80805
+ }
80806
+
80807
+ class TimestampReporter implements Reporter {
80808
+ private logs: TimestampEntry[] = [];
80809
+ private testStartTime: number = 0;
80810
+ private outputDir: string = "test-results";
80811
+ private sourceCache: Map<string, string[]> = new Map();
80812
+
80813
+ onBegin(config: FullConfig, suite: Suite) {
80814
+ this.outputDir = config.projects[0]?.outputDir || "test-results";
80815
+ console.log("\\n\u{1F3AC} Timestamp Reporter: Recording action timestamps\\n");
80816
+ }
80817
+
80818
+ private readSourceLine(filePath: string, lineNumber: number): string | undefined {
80819
+ try {
80820
+ // Check cache first
80821
+ if (!this.sourceCache.has(filePath)) {
80822
+ const content = fs.readFileSync(filePath, "utf-8");
80823
+ this.sourceCache.set(filePath, content.split("\\n"));
80824
+ }
80825
+
80826
+ const lines = this.sourceCache.get(filePath);
80827
+ if (lines && lineNumber > 0 && lineNumber <= lines.length) {
80828
+ return lines[lineNumber - 1].trim();
80829
+ }
80830
+ } catch (error) {
80831
+ console.error(\`Error reading source file \${filePath}:\`, error);
80832
+ }
80833
+ return undefined;
80834
+ }
80835
+
80836
+ onTestBegin(test: TestCase, result: TestResult) {
80837
+ this.testStartTime = Date.now();
80838
+ console.log(\`\\n\u{1F4DD} [\${new Date().toISOString()}] Test started: \${test.title}\`);
80839
+ }
80840
+
80841
+ onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
80842
+ // Only log steps that have a location (actual test code)
80843
+ if (!step.location) return;
80844
+
80845
+ const timestamp = new Date().toISOString();
80846
+ const relativeMs = Date.now() - this.testStartTime;
80847
+ const indent = " ".repeat(step.titlePath().length - 1);
80848
+ const sourceCode = this.readSourceLine(step.location.file, step.location.line);
80849
+
80850
+ console.log(
80851
+ \`\${indent}\u23F1\uFE0F [\${timestamp}] +\${relativeMs}ms - \${sourceCode || step.title}\`
80852
+ );
80853
+ }
80854
+
80855
+ onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
80856
+ // Only log steps that have a location (actual test code)
80857
+ if (!step.location) return;
80858
+
80859
+ const timestamp = new Date().toISOString();
80860
+ const relativeMs = Date.now() - this.testStartTime;
80861
+
80862
+ // Read the actual source code line
80863
+ const sourceCode = this.readSourceLine(step.location.file, step.location.line);
80864
+
80865
+ const entry: TimestampEntry = {
80866
+ title: step.title,
80867
+ category: step.category,
80868
+ timestamp: timestamp,
80869
+ relativeMs: relativeMs,
80870
+ duration: step.duration,
80871
+ stepPath: step.titlePath(),
80872
+ location: {
80873
+ file: step.location.file,
80874
+ line: step.location.line,
80875
+ column: step.location.column,
80876
+ },
80877
+ sourceCode: sourceCode,
80878
+ };
80879
+
80880
+ // Add error if present
80881
+ if (step.error) {
80882
+ entry.error = {
80883
+ message: step.error.message || "Unknown error",
80884
+ stack: step.error.stack,
80885
+ };
80886
+ }
80887
+
80888
+ this.logs.push(entry);
80889
+
80890
+ const indent = " ".repeat(step.titlePath().length - 1);
80891
+ const status = step.error ? "\u274C" : "\u2705";
80892
+ console.log(
80893
+ \`\${indent}\${status} [\${timestamp}] \${sourceCode || step.title} (\${step.duration}ms)\`
80894
+ );
80895
+ }
80896
+
80897
+ onTestEnd(test: TestCase, result: TestResult) {
80898
+ const videoAttachment = result.attachments.find((a) => a.name === "video");
80899
+ const videoPath = videoAttachment?.path;
80900
+
80901
+ const output = {
80902
+ testTitle: test.title,
80903
+ testId: test.id,
80904
+ status: result.status,
80905
+ duration: result.duration,
80906
+ startTime: new Date(this.testStartTime).toISOString(),
80907
+ videoPath: videoPath || null,
80908
+ actions: this.logs,
80909
+ };
80910
+
80911
+ // Write to test-results directory
80912
+ const sanitizedTitle = test.title
80913
+ .replace(/[^a-z0-9]/gi, "-")
80914
+ .toLowerCase();
80915
+ const outputPath = path.join(
80916
+ this.outputDir,
80917
+ \`timestamps-\${sanitizedTitle}.json\`
80918
+ );
80919
+
80920
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
80921
+ fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
80922
+
80923
+ console.log(\`\\n\u{1F4BE} Timestamps saved to: \${outputPath}\`);
80924
+ if (videoPath) {
80925
+ console.log(\`\u{1F4F9} Video path: \${videoPath}\`);
80926
+ }
80927
+ console.log("");
80928
+
80929
+ // Reset logs for next test
80930
+ this.logs = [];
80931
+ // Clear source cache to free memory
80932
+ this.sourceCache.clear();
80933
+ }
80934
+
80935
+ onEnd(result: FullResult) {
80936
+ console.log(\`\\n\u2705 Test run complete! Status: \${result.status}\\n\`);
80937
+ }
80938
+ }
80939
+
80940
+ export default TimestampReporter;
80941
+ `;
80759
80942
  async function findRepositoryLocation(repoName) {
80760
80943
  const repoLocation = await findRepoLocation(repoName);
80761
80944
  if (!repoLocation) {
@@ -80790,12 +80973,39 @@ async function findPlaywrightConfig(repoLocation) {
80790
80973
  }
80791
80974
  async function createPlaywrightConfig(repoLocation) {
80792
80975
  const configPath = path9.join(repoLocation, "playwright.config.ts");
80793
- const defaultConfig = `import { defineConfig } from "@playwright/test";
80976
+ await fs10.writeFile(configPath, DEFAULT_CONFIG);
80977
+ console.log(`Created playwright config at ${configPath}`);
80978
+ return configPath;
80979
+ }
80980
+ async function createTimestampReporter(repoLocation) {
80981
+ const reporterPath = path9.join(repoLocation, "timestamp-reporter.ts");
80982
+ await fs10.writeFile(reporterPath, TIMESTAMP_REPORTER_CODE);
80983
+ console.log(`Created timestamp reporter at ${reporterPath}`);
80984
+ return reporterPath;
80985
+ }
80986
+ async function setupPlaywrightConfig(repoLocation) {
80987
+ let configPath = await findPlaywrightConfig(repoLocation);
80988
+ let wasCreated = false;
80989
+ let originalContent = null;
80990
+ let wasModified = false;
80991
+ const reporterPath = await createTimestampReporter(repoLocation);
80992
+ if (!configPath) {
80993
+ configPath = await createPlaywrightConfig(repoLocation);
80994
+ wasCreated = true;
80995
+ console.log("Created new playwright config");
80996
+ } else {
80997
+ originalContent = await fs10.readFile(configPath, "utf-8");
80998
+ wasModified = true;
80999
+ const newConfig = `import { defineConfig } from "@playwright/test";
80794
81000
 
80795
81001
  export default defineConfig({
80796
81002
  timeout: 60000,
80797
81003
  workers: 1,
80798
81004
  fullyParallel: false,
81005
+ reporter: [
81006
+ ["list"],
81007
+ ["./timestamp-reporter.ts"],
81008
+ ],
80799
81009
  use: {
80800
81010
  video: "on",
80801
81011
  headless: true,
@@ -80804,63 +81014,18 @@ export default defineConfig({
80804
81014
  },
80805
81015
  });
80806
81016
  `;
80807
- await fs10.writeFile(configPath, defaultConfig);
80808
- console.log(`Created playwright config at ${configPath}`);
80809
- return configPath;
80810
- }
80811
- async function ensureVideoEnabled(configPath) {
80812
- const configContent = await fs10.readFile(configPath, "utf-8");
80813
- if (configContent.match(/video:\s*["']on["']/)) {
80814
- console.log("Video already enabled in config");
80815
- return null;
80816
- }
80817
- console.log("Updating config to enable video recording");
80818
- let updatedConfig = configContent;
80819
- const useBlockRegex = /use:\s*\{([^}]*)\}/s;
80820
- const match2 = configContent.match(useBlockRegex);
80821
- if (match2) {
80822
- const useBlockContent = match2[1];
80823
- if (useBlockContent.includes("video:")) {
80824
- updatedConfig = configContent.replace(
80825
- /video:\s*["'][^"']*["']/,
80826
- 'video: "on"'
80827
- );
80828
- } else {
80829
- const updatedUseBlock = `use: {
80830
- video: "on",${useBlockContent}}`;
80831
- updatedConfig = configContent.replace(useBlockRegex, updatedUseBlock);
80832
- }
80833
- } else {
80834
- const configObjectRegex = /export\s+default\s+defineConfig\(\s*\{/;
80835
- updatedConfig = configContent.replace(
80836
- configObjectRegex,
80837
- `export default defineConfig({
80838
- use: {
80839
- video: "on",
80840
- },`
81017
+ await fs10.writeFile(configPath, newConfig);
81018
+ console.log(
81019
+ `Overwrote existing config at ${configPath} (backed up for cleanup)`
80841
81020
  );
80842
81021
  }
80843
- await fs10.writeFile(configPath, updatedConfig);
80844
- console.log("Config updated to enable video recording");
80845
- return configContent;
80846
- }
80847
- async function setupPlaywrightConfig(repoLocation) {
80848
- let configPath = await findPlaywrightConfig(repoLocation);
80849
- let wasCreated = false;
80850
- let originalContent = null;
80851
- let wasModified = false;
80852
- if (!configPath) {
80853
- configPath = await createPlaywrightConfig(repoLocation);
80854
- wasCreated = true;
80855
- } else {
80856
- originalContent = await ensureVideoEnabled(configPath);
80857
- wasModified = originalContent !== null;
80858
- }
80859
81022
  return {
80860
81023
  configPath,
80861
81024
  wasCreated,
80862
81025
  originalContent,
80863
- wasModified
81026
+ wasModified,
81027
+ reporterPath,
81028
+ reporterWasCreated: true
80864
81029
  };
80865
81030
  }
80866
81031
  async function ensureTestResultsInGitignore(repoLocation) {
@@ -80933,14 +81098,48 @@ async function findVideoFile(repoLocation) {
80933
81098
  console.log(`Found video at: ${videoPath}`);
80934
81099
  return videoPath;
80935
81100
  }
80936
- async function uploadVideo(videoPath, responseUrl) {
81101
+ async function findTimestampJsonFile(repoLocation) {
81102
+ const testResultsDir = path9.join(repoLocation, "test-results");
81103
+ const walkDir = async (dir) => {
81104
+ try {
81105
+ const files = await fs10.readdir(dir);
81106
+ for (const file3 of files) {
81107
+ const fullPath = path9.join(dir, file3);
81108
+ const stat4 = await fs10.stat(fullPath);
81109
+ if (stat4.isDirectory()) {
81110
+ const result = await walkDir(fullPath);
81111
+ if (result) {
81112
+ return result;
81113
+ }
81114
+ } else if (file3.startsWith("timestamps-") && file3.endsWith(".json")) {
81115
+ return fullPath;
81116
+ }
81117
+ }
81118
+ } catch (error82) {
81119
+ console.error(`Error walking directory ${dir}:`, error82);
81120
+ }
81121
+ return null;
81122
+ };
81123
+ const jsonPath = await walkDir(testResultsDir);
81124
+ if (!jsonPath) {
81125
+ throw new Error("No timestamp JSON file found in test-results directory");
81126
+ }
81127
+ console.log(`Found timestamp JSON at: ${jsonPath}`);
81128
+ return jsonPath;
81129
+ }
81130
+ async function uploadVideo(videoPath, timestampJsonPath, responseUrl) {
80937
81131
  const videoBuffer = await fs10.readFile(videoPath);
81132
+ const jsonBuffer = await fs10.readFile(timestampJsonPath);
80938
81133
  const FormData2 = (await Promise.resolve().then(() => __toESM(require_form_data(), 1))).default;
80939
81134
  const formData = new FormData2();
80940
81135
  formData.append("video", videoBuffer, {
80941
81136
  filename: path9.basename(videoPath),
80942
81137
  contentType: "video/webm"
80943
81138
  });
81139
+ formData.append("timestamps", jsonBuffer, {
81140
+ filename: path9.basename(timestampJsonPath),
81141
+ contentType: "application/json"
81142
+ });
80944
81143
  const uploadResponse = await fetch(responseUrl, {
80945
81144
  method: "POST",
80946
81145
  body: formData,
@@ -80948,34 +81147,49 @@ async function uploadVideo(videoPath, responseUrl) {
80948
81147
  });
80949
81148
  if (!uploadResponse.ok) {
80950
81149
  throw new Error(
80951
- `Failed to upload video: ${uploadResponse.status} ${uploadResponse.statusText}`
81150
+ `Failed to upload files: ${uploadResponse.status} ${uploadResponse.statusText}`
80952
81151
  );
80953
81152
  }
80954
- console.log(`Video uploaded successfully to ${responseUrl}`);
81153
+ console.log(`Video and timestamps uploaded successfully to ${responseUrl}`);
80955
81154
  return uploadResponse.status;
80956
81155
  }
80957
- async function deleteVideo(videoPath) {
81156
+ async function deleteTestFiles(videoPath, timestampJsonPath) {
80958
81157
  try {
80959
81158
  await fs10.unlink(videoPath);
80960
81159
  console.log(`Deleted video file: ${videoPath}`);
80961
81160
  } catch (error82) {
80962
81161
  console.error(`Error deleting video file ${videoPath}:`, error82);
80963
81162
  }
81163
+ try {
81164
+ await fs10.unlink(timestampJsonPath);
81165
+ console.log(`Deleted timestamp JSON: ${timestampJsonPath}`);
81166
+ } catch (error82) {
81167
+ console.error(`Error deleting timestamp JSON ${timestampJsonPath}:`, error82);
81168
+ }
80964
81169
  }
80965
81170
  async function cleanupConfig(configState) {
80966
- if (!configState.configPath) {
80967
- return;
81171
+ if (configState.configPath) {
81172
+ try {
81173
+ if (configState.wasCreated) {
81174
+ await fs10.unlink(configState.configPath);
81175
+ console.log(
81176
+ `Cleaned up created config file: ${configState.configPath}`
81177
+ );
81178
+ } else if (configState.wasModified && configState.originalContent) {
81179
+ await fs10.writeFile(configState.configPath, configState.originalContent);
81180
+ console.log(`Restored original config: ${configState.configPath}`);
81181
+ }
81182
+ } catch (error82) {
81183
+ console.error("Error cleaning up config:", error82);
81184
+ }
80968
81185
  }
80969
- try {
80970
- if (configState.wasCreated) {
80971
- await fs10.unlink(configState.configPath);
80972
- console.log(`Cleaned up created config file: ${configState.configPath}`);
80973
- } else if (configState.wasModified && configState.originalContent) {
80974
- await fs10.writeFile(configState.configPath, configState.originalContent);
80975
- console.log(`Restored original config: ${configState.configPath}`);
81186
+ if (configState.reporterPath && configState.reporterWasCreated) {
81187
+ try {
81188
+ await fs10.unlink(configState.reporterPath);
81189
+ console.log(`Cleaned up reporter file: ${configState.reporterPath}`);
81190
+ } catch (error82) {
81191
+ console.error("Error cleaning up reporter:", error82);
80976
81192
  }
80977
- } catch (error82) {
80978
- console.error("Error cleaning up config:", error82);
80979
81193
  }
80980
81194
  }
80981
81195
  async function runPlaywrightTestWithVideo(options) {
@@ -80988,12 +81202,13 @@ async function runPlaywrightTestWithVideo(options) {
80988
81202
  await ensureTestResultsInGitignore(repoLocation);
80989
81203
  const testResult = await runPlaywrightTest(repoLocation, testFilePath);
80990
81204
  const videoPath = await findVideoFile(repoLocation);
81205
+ const timestampJsonPath = await findTimestampJsonFile(repoLocation);
80991
81206
  let uploadStatus;
80992
81207
  if (responseUrl) {
80993
- uploadStatus = await uploadVideo(videoPath, responseUrl);
80994
- await deleteVideo(videoPath);
81208
+ uploadStatus = await uploadVideo(videoPath, timestampJsonPath, responseUrl);
81209
+ await deleteTestFiles(videoPath, timestampJsonPath);
80995
81210
  } else {
80996
- console.log("No responseUrl provided, keeping video file locally");
81211
+ console.log("No responseUrl provided, keeping files locally");
80997
81212
  }
80998
81213
  await cleanupConfig(configState);
80999
81214
  return {
@@ -10905,7 +10905,7 @@ var glob = Object.assign(glob_, {
10905
10905
  glob.glob = glob;
10906
10906
 
10907
10907
  // src/proxy/version.ts
10908
- var VERSION = "0.3.25";
10908
+ var VERSION = "0.3.26";
10909
10909
 
10910
10910
  // src/cli.ts
10911
10911
  var STAKLINK_PROXY = "staklink-proxy";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "staklink",
3
3
  "displayName": "staklink",
4
4
  "description": "staklink process manager",
5
- "version": "0.3.25",
5
+ "version": "0.3.26",
6
6
  "type": "module",
7
7
  "publisher": "stakwork",
8
8
  "engines": {
@@ -0,0 +1,106 @@
1
+ {
2
+ "testTitle": "YOLO page should display YOLO text",
3
+ "testId": "ea70bc1c4bc8cb3ef066-ef50a3f3fbda7cffd2c6",
4
+ "status": "passed",
5
+ "duration": 2661,
6
+ "startTime": "2025-11-20T05:03:47.154Z",
7
+ "videoPath": "/Users/evanfeenstra/code/sphinx2/staklink/test-results/scripts-demo-test-YOLO-page-should-display-YOLO-text/video.webm",
8
+ "actions": [
9
+ {
10
+ "title": "Navigate to \"/\"",
11
+ "category": "pw:api",
12
+ "timestamp": "2025-11-20T05:03:47.791Z",
13
+ "relativeMs": 637,
14
+ "duration": 505,
15
+ "stepPath": [
16
+ "Navigate to \"/\""
17
+ ],
18
+ "location": {
19
+ "file": "/Users/evanfeenstra/code/sphinx2/staklink/scripts/demo-test.spec.ts",
20
+ "line": 5,
21
+ "column": 14
22
+ },
23
+ "sourceCode": "await page.goto('http://localhost:8080', { waitUntil: 'networkidle' });"
24
+ },
25
+ {
26
+ "title": "Wait for load state \"domcontentloaded\"",
27
+ "category": "pw:api",
28
+ "timestamp": "2025-11-20T05:03:47.793Z",
29
+ "relativeMs": 639,
30
+ "duration": 1,
31
+ "stepPath": [
32
+ "Wait for load state \"domcontentloaded\""
33
+ ],
34
+ "location": {
35
+ "file": "/Users/evanfeenstra/code/sphinx2/staklink/scripts/demo-test.spec.ts",
36
+ "line": 8,
37
+ "column": 14
38
+ },
39
+ "sourceCode": "await page.waitForLoadState('domcontentloaded');"
40
+ },
41
+ {
42
+ "title": "Expect \"toHaveText\"",
43
+ "category": "expect",
44
+ "timestamp": "2025-11-20T05:03:47.815Z",
45
+ "relativeMs": 661,
46
+ "duration": 21,
47
+ "stepPath": [
48
+ "Expect \"toHaveText\""
49
+ ],
50
+ "location": {
51
+ "file": "/Users/evanfeenstra/code/sphinx2/staklink/scripts/demo-test.spec.ts",
52
+ "line": 11,
53
+ "column": 36
54
+ },
55
+ "sourceCode": "await expect(page.locator('h1')).toHaveText('YOLO');"
56
+ },
57
+ {
58
+ "title": "Expect \"toBeVisible\"",
59
+ "category": "expect",
60
+ "timestamp": "2025-11-20T05:03:47.817Z",
61
+ "relativeMs": 663,
62
+ "duration": 2,
63
+ "stepPath": [
64
+ "Expect \"toBeVisible\""
65
+ ],
66
+ "location": {
67
+ "file": "/Users/evanfeenstra/code/sphinx2/staklink/scripts/demo-test.spec.ts",
68
+ "line": 14,
69
+ "column": 40
70
+ },
71
+ "sourceCode": "await expect(page.locator('button')).toBeVisible();"
72
+ },
73
+ {
74
+ "title": "Expect \"toHaveText\"",
75
+ "category": "expect",
76
+ "timestamp": "2025-11-20T05:03:47.819Z",
77
+ "relativeMs": 665,
78
+ "duration": 2,
79
+ "stepPath": [
80
+ "Expect \"toHaveText\""
81
+ ],
82
+ "location": {
83
+ "file": "/Users/evanfeenstra/code/sphinx2/staklink/scripts/demo-test.spec.ts",
84
+ "line": 15,
85
+ "column": 40
86
+ },
87
+ "sourceCode": "await expect(page.locator('button')).toHaveText('Test');"
88
+ },
89
+ {
90
+ "title": "Wait for timeout",
91
+ "category": "pw:api",
92
+ "timestamp": "2025-11-20T05:03:49.821Z",
93
+ "relativeMs": 2667,
94
+ "duration": 2002,
95
+ "stepPath": [
96
+ "Wait for timeout"
97
+ ],
98
+ "location": {
99
+ "file": "/Users/evanfeenstra/code/sphinx2/staklink/scripts/demo-test.spec.ts",
100
+ "line": 18,
101
+ "column": 14
102
+ },
103
+ "sourceCode": "await page.waitForTimeout(2000);"
104
+ }
105
+ ]
106
+ }