helixlife-v5-cli 1.2.8 → 1.2.9

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.
Files changed (46) hide show
  1. package/README.md +33 -58
  2. package/helix-cli.js +7 -2
  3. package/lib/.cli.runtime.generated.json +10 -1
  4. package/lib/automation-file-chooser-guard.js +182 -0
  5. package/lib/automation-overlay.init.js +332 -68
  6. package/lib/browser-css-cache-guard.init-page.js +49 -0
  7. package/lib/browser-window-maximize.init-page.js +64 -0
  8. package/lib/cli.js +316 -14
  9. package/lib/helix-reload-bypass-cache.run-code.js +7 -0
  10. package/lib/helix-styles-guard.js +77 -0
  11. package/lib/hide-nested-skills.js +120 -0
  12. package/lib/native-mode-patch.js +288 -0
  13. package/lib/postinstall.js +39 -0
  14. package/lib/profile-lock-guard.js +187 -0
  15. package/lib/runtime-config.js +90 -0
  16. package/lib/token-file.js +189 -0
  17. package/lib/visual-page-wrap.js +311 -0
  18. package/lib/visual.js +255 -86
  19. package/package.json +9 -21
  20. package/scripts/README.md +81 -0
  21. package/scripts/{analysis-detail-upload-file.js → analysis/detail-upload-file.js} +1 -1
  22. package/scripts/{jigsaw-inspect-dom.js → analysis/jigsaw-inspect-dom.js} +15 -15
  23. package/scripts/{jigsaw-verify-enumerate.js → analysis/jigsaw-verify-enumerate.js} +1 -1
  24. package/scripts/common/.gitkeep +0 -0
  25. package/scripts/{chrome-pdf-enumerate-a11y.js → edu/chrome-pdf-enumerate-a11y.js} +1 -1
  26. package/scripts/{chrome-pdf-goto-page.js → edu/chrome-pdf-goto-page.js} +1 -1
  27. package/scripts/{chrome-pdf-zoom-out.js → edu/chrome-pdf-zoom-out.js} +1 -1
  28. package/scripts/{hover-partial-chapter-match.js → edu/hover-partial-chapter-match.js} +1 -1
  29. package/scripts/maintainer/strip-extra-skills.ps1 +27 -0
  30. package/scripts/workbench/jova/outline-edit-root-title.js +33 -0
  31. package/scripts/workbench/jova/outline-keyword-edit.js +122 -0
  32. package/scripts/workbench/jova/outline-ops.js +344 -0
  33. package/scripts/workbench/jova/outline-probe-hover.js +56 -0
  34. package/scripts/workbench/jova/proofread-ai-rewrite-section.js +94 -0
  35. package/scripts/workbench/jova/proofread-ops.js +261 -0
  36. package/scripts/workbench/jova/writing-ops.js +376 -0
  37. package/scripts/workbench/jova/writing-proofread-abstract.js +103 -0
  38. package/lib/cli.runtime.config.json +0 -5
  39. package/references/vip.helixlife.cn.site-ops.md +0 -1469
  40. package/scripts/workbench-jova-outline-regenerate-node.js +0 -173
  41. package/scripts/workbench-jova-proofread-section.js +0 -262
  42. package/scripts/workbench-jova-writing-tool.js +0 -308
  43. /package/scripts/{analysis-detail-upload-both.js → analysis/detail-upload-both.js} +0 -0
  44. /package/scripts/{jigsaw-verify-bio-tools.js → analysis/jigsaw-verify-bio-tools.js} +0 -0
  45. /package/scripts/{jigsaw-verify-drag-tool.js → analysis/jigsaw-verify-drag-tool.js} +0 -0
  46. /package/scripts/{jigsaw-verify-reference-lines.js → analysis/jigsaw-verify-reference-lines.js} +0 -0
package/lib/cli.js CHANGED
@@ -1,22 +1,201 @@
1
1
  'use strict';
2
2
 
3
+ const os = require('node:os');
3
4
  const path = require('node:path');
4
5
  const { spawnSync } = require('node:child_process');
5
6
 
6
7
  const PACKAGE_JSON = require(path.join(__dirname, '..', 'package.json'));
8
+
9
+ /** Skill 根目录(含 helix-cli.js / package.json 的目录) */
10
+ const SKILL_ROOT = path.join(__dirname, '..');
11
+
12
+ /** 运行时所需的最低 Node 主版本 */
13
+ const MIN_NODE_MAJOR = 18;
14
+
15
+ /** 运行时核心依赖(缺失时触发自动安装) */
16
+ const RUNTIME_DEP = '@playwright/cli';
17
+ const RUNTIME_DEP_ENTRY = '@playwright/cli/playwright-cli.js';
7
18
  const {
8
19
  parseVisualFlags,
9
20
  injectVisualConfig,
10
21
  shouldWrapWithVisual,
11
22
  runWithVisualFeedback,
23
+ resolveCommandName,
12
24
  DEFAULT_VISUAL_ENABLED,
13
25
  } = require('./visual');
26
+ const {
27
+ applyRunCodeAutomationGuard,
28
+ runUploadCommandWithGuard,
29
+ } = require('./automation-file-chooser-guard');
30
+ const { hideNestedSkillsOnce } = require('./hide-nested-skills');
31
+ const { applyAll: applyNativeModePatches } = require('./native-mode-patch');
32
+ const {
33
+ isHelixSiteOpen,
34
+ repairHelixStylesAfterOpen,
35
+ } = require('./helix-styles-guard');
36
+ const {
37
+ extractProfileDirFromOpenArgs,
38
+ runOpenWithProfileRecovery,
39
+ } = require('./profile-lock-guard');
40
+ const {
41
+ resolveTokenFile,
42
+ resolveHelixConfigDir,
43
+ readTokenFromBrowser,
44
+ writeTokenFile,
45
+ maybePersistLoginToken,
46
+ shouldTryPersistTokenAfterCommand,
47
+ DEFAULT_TOKEN_FILE_NAME,
48
+ } = require('./token-file');
14
49
 
15
- /** 默认快照/下载等工作区目录名(相对 cwd),替代引擎默认的 .playwright-cli */
16
- const DEFAULT_OUTPUT_DIR_NAME = 'helixlife-v5-cli';
50
+ /** 默认快照/下载/日志等工作区目录名(相对 cwd),替代引擎默认的 .playwright-cli */
51
+ const DEFAULT_OUTPUT_DIR_NAME = '.helix-output';
52
+
53
+ /** 默认持久化 profile 目录名(相对用户主目录) */
54
+ const DEFAULT_PROFILE_DIR_NAME = '.helixlife-profile';
55
+
56
+ /**
57
+ * 解析持久化 profile 目录为绝对路径。
58
+ * 相对路径基于用户主目录(非 cwd),避免 WorkBuddy 等环境每次会话 cwd 不同导致登录态丢失。
59
+ * @param {string | undefined} [dir]
60
+ * @returns {string}
61
+ */
62
+ function resolveProfileDir(dir) {
63
+ const raw = (dir ?? process.env.HELIX_PROFILE_DIR ?? DEFAULT_PROFILE_DIR_NAME).trim();
64
+ if (path.isAbsolute(raw)) {
65
+ return path.normalize(raw);
66
+ }
67
+ return path.join(os.homedir(), raw);
68
+ }
69
+
70
+ /**
71
+ * 将透传参数中的 --profile 规范化为绝对路径。
72
+ * @param {string[]} tokens
73
+ * @returns {string[]}
74
+ */
75
+ function normalizeProfileArgs(tokens) {
76
+ const out = [...tokens];
77
+ for (let i = 0; i < out.length; i += 1) {
78
+ const t = out[i];
79
+ if (t === '--profile') {
80
+ const next = out[i + 1];
81
+ if (next !== undefined) {
82
+ out[i + 1] = resolveProfileDir(next);
83
+ }
84
+ } else if (t.startsWith('--profile=')) {
85
+ out[i] = `--profile=${resolveProfileDir(t.slice('--profile='.length))}`;
86
+ }
87
+ }
88
+ return out;
89
+ }
17
90
 
18
91
  function playwrightCliEntry() {
19
- return require.resolve('@playwright/cli/playwright-cli.js');
92
+ return require.resolve(RUNTIME_DEP_ENTRY);
93
+ }
94
+
95
+ /** 当前 Node 主版本号(解析失败返回 NaN) */
96
+ function currentNodeMajor() {
97
+ return parseInt(String(process.versions.node).split('.')[0], 10);
98
+ }
99
+
100
+ /**
101
+ * 运行时依赖 @playwright/cli 是否已可解析。
102
+ * 仅吞 MODULE_NOT_FOUND;其他错误(如依赖损坏)原样抛出,避免误判为"未安装"。
103
+ * @returns {boolean}
104
+ */
105
+ function runtimeDepInstalled() {
106
+ try {
107
+ playwrightCliEntry();
108
+ return true;
109
+ } catch (e) {
110
+ if (e && e.code === 'MODULE_NOT_FOUND') {
111
+ return false;
112
+ }
113
+ throw e;
114
+ }
115
+ }
116
+
117
+ /** 是否允许缺失依赖时自动 npm install(默认开启,HELIX_AUTO_INSTALL=0/false/no 关闭) */
118
+ function autoInstallEnabled() {
119
+ const v = String(process.env.HELIX_AUTO_INSTALL ?? '').trim().toLowerCase();
120
+ return v !== '0' && v !== 'false' && v !== 'no';
121
+ }
122
+
123
+ /**
124
+ * 在 SKILL_ROOT 执行一次 `npm install`,安装进度直出到终端。
125
+ * @returns {boolean} 安装命令是否成功(exit 0)
126
+ */
127
+ function runNpmInstall() {
128
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
129
+ console.error(
130
+ `[${PACKAGE_JSON.name}] 缺少运行时依赖 ${RUNTIME_DEP},正在 ${SKILL_ROOT} 自动执行 \`npm install\` …`
131
+ );
132
+ const result = spawnSync(npmCmd, ['install'], {
133
+ cwd: SKILL_ROOT,
134
+ stdio: 'inherit',
135
+ windowsHide: true,
136
+ });
137
+ if (result.error) {
138
+ console.error(
139
+ `[${PACKAGE_JSON.name}] 无法启动 npm(${result.error.message})。请确认已安装 npm,并手动在 ${SKILL_ROOT} 执行 \`npm install\`。`
140
+ );
141
+ return false;
142
+ }
143
+ return result.status === 0;
144
+ }
145
+
146
+ /**
147
+ * 运行前置校验:Node 版本 + 运行时依赖(缺失则自动安装)。
148
+ * 失败时打印友好提示并返回 false(由 main 转换为非零退出码)。
149
+ * @returns {boolean}
150
+ */
151
+ function ensureRuntimeReady() {
152
+ const major = currentNodeMajor();
153
+ if (Number.isFinite(major) && major < MIN_NODE_MAJOR) {
154
+ console.error(
155
+ `[${PACKAGE_JSON.name}] 需要 Node.js >= ${MIN_NODE_MAJOR},当前为 ${process.versions.node}。请升级 Node 后重试。`
156
+ );
157
+ return false;
158
+ }
159
+
160
+ if (runtimeDepInstalled()) {
161
+ // 兜底:覆盖「旧版本已装好 node_modules、postinstall 未运行过」的存量场景。
162
+ // 由哨兵保证仅扫描一次,稳态快路径开销可忽略(一次 existsSync)。
163
+ hideNestedSkillsOnce(SKILL_ROOT, {
164
+ log: (msg) => console.error(`[${PACKAGE_JSON.name}] ${msg}`),
165
+ });
166
+ applyNativeModePatches(SKILL_ROOT, {
167
+ log: (msg) => console.error(`[${PACKAGE_JSON.name}][native-mode-patch] ${msg}`),
168
+ });
169
+ return true;
170
+ }
171
+
172
+ if (!autoInstallEnabled()) {
173
+ console.error(`[${PACKAGE_JSON.name}] 缺少运行时依赖 ${RUNTIME_DEP}(已通过 HELIX_AUTO_INSTALL=0 关闭自动安装)。`);
174
+ console.error(`[${PACKAGE_JSON.name}] 请手动执行: cd "${SKILL_ROOT}" && npm install`);
175
+ return false;
176
+ }
177
+
178
+ if (!runNpmInstall()) {
179
+ console.error(`[${PACKAGE_JSON.name}] 自动安装失败。请手动在 ${SKILL_ROOT} 执行 \`npm install\` 后重试。`);
180
+ return false;
181
+ }
182
+
183
+ if (!runtimeDepInstalled()) {
184
+ console.error(
185
+ `[${PACKAGE_JSON.name}] 安装已完成但仍无法解析 ${RUNTIME_DEP},请检查 ${path.join(SKILL_ROOT, 'node_modules')}。`
186
+ );
187
+ return false;
188
+ }
189
+
190
+ // postinstall 钩子通常已隐藏嵌套 Skill;此处兜底覆盖钩子未触发(如 --ignore-scripts)的情况。
191
+ hideNestedSkillsOnce(SKILL_ROOT, {
192
+ log: (msg) => console.error(`[${PACKAGE_JSON.name}] ${msg}`),
193
+ });
194
+ applyNativeModePatches(SKILL_ROOT, {
195
+ log: (msg) => console.error(`[${PACKAGE_JSON.name}][native-mode-patch] ${msg}`),
196
+ });
197
+ console.error(`[${PACKAGE_JSON.name}] 依赖就绪,继续执行。`);
198
+ return true;
20
199
  }
21
200
 
22
201
  /**
@@ -29,12 +208,20 @@ function resolveOutputDir() {
29
208
  return path.isAbsolute(dir) ? dir : path.resolve(process.cwd(), dir);
30
209
  }
31
210
 
32
- /** 传给底层引擎子进程的环境变量(含输出目录覆盖) */
211
+ /** 透传给底层引擎子进程的环境变量(含输出目录与原生交互模式默认开关)。 */
33
212
  function buildChildEnv() {
34
213
  const env = { ...process.env };
35
214
  if (!env.PLAYWRIGHT_MCP_OUTPUT_DIR) {
36
215
  env.PLAYWRIGHT_MCP_OUTPUT_DIR = resolveOutputDir();
37
216
  }
217
+ // 原生交互模式默认开启:与 CDP 直连基线对齐(手动可弹原生上传 / 下载用真实文件名 + 原生图标)。
218
+ // 用户已显式设置 0 时尊重用户选择,便于回退或定向调试 browser_file_upload 工具。
219
+ if (env.HELIX_NATIVE_FILE_CHOOSER === undefined) {
220
+ env.HELIX_NATIVE_FILE_CHOOSER = '1';
221
+ }
222
+ if (env.HELIX_NATIVE_DOWNLOADS === undefined) {
223
+ env.HELIX_NATIVE_DOWNLOADS = '1';
224
+ }
38
225
  return env;
39
226
  }
40
227
 
@@ -79,8 +266,7 @@ function parseHelixFlags(tokens, startIndex) {
79
266
  const options = {
80
267
  headless: false,
81
268
  cdpUrl: process.env.HELIX_CDP_URL || 'http://127.0.0.1:9222',
82
- profile:
83
- process.env.HELIX_PROFILE_DIR || path.join(process.cwd(), '.helixlife-profile'),
269
+ profile: resolveProfileDir(),
84
270
  visual: { enabled: DEFAULT_VISUAL_ENABLED, durationMs: null },
85
271
  };
86
272
  for (let i = startIndex; i < tokens.length; i += 1) {
@@ -102,10 +288,10 @@ function parseHelixFlags(tokens, startIndex) {
102
288
  console.error(`[${PACKAGE_JSON.name}] --profile requires a value`);
103
289
  return null;
104
290
  }
105
- options.profile = next;
291
+ options.profile = resolveProfileDir(next);
106
292
  i += 1;
107
293
  } else if (t.startsWith('--profile=')) {
108
- options.profile = t.slice('--profile='.length);
294
+ options.profile = resolveProfileDir(t.slice('--profile='.length));
109
295
  } else if (t === '--cdp-url') {
110
296
  const next = tokens[i + 1];
111
297
  if (next === undefined) {
@@ -146,22 +332,34 @@ function printHelp() {
146
332
  console.log(
147
333
  ` ${n} helix doctor [--cdp-url <url>] [--profile <dir>] 先 list,再执行 helix home`
148
334
  );
335
+ console.log(
336
+ ` ${n} helix token save 从当前浏览器会话读取 token 并保存到用户目录`
337
+ );
338
+ console.log(
339
+ ` ${n} helix token path 输出 token 文件绝对路径(供后台 CLI 读取)`
340
+ );
149
341
  console.log('');
150
- console.log('页面内视觉反馈(默认开启,作用于 click/fill/hover 等元素命令):');
342
+ console.log('页面内视觉反馈(默认开启,作用于 click/fill/hover/run-code 等命令):');
151
343
  console.log(` ${n} click e5 操作前黄框 + 标签,成功后绿框`);
344
+ console.log(` ${n} run-code --filename=script.js 脚本内 click/hover/fill 自动逐步高亮 + HUD`);
152
345
  console.log(` ${n} click e5 --no-visual 静默执行,无页面高亮`);
153
346
  console.log(` ${n} fill e3 "文本" --visual-ms 700 自定义高亮时长(毫秒)`);
347
+ console.log(` ${n} click e5 --visual-persist HUD 与高亮不自动消失(改样式用)`);
154
348
  console.log('');
155
349
  console.log('本地开发可使用:');
156
350
  console.log(' node helix-cli.js <同上参数>');
157
351
  console.log('');
158
352
  console.log(
159
- '环境变量:HELIX_BASE_URL、HELIX_CDP_URL、HELIX_PROFILE_DIR(默认 profile 目录)、'
353
+ `环境变量:HELIX_BASE_URL、HELIX_CDP_URL、HELIX_PROFILE_DIR(默认 ${DEFAULT_PROFILE_DIR_NAME} 于用户主目录)、`
354
+ );
355
+ console.log(
356
+ ` HELIX_CONFIG_DIR / HELIX_TOKEN_FILE(默认 ~/.config/helix/${DEFAULT_TOKEN_FILE_NAME},全平台一致)、`
160
357
  );
161
358
  console.log(
162
359
  ` HELIX_OUTPUT_DIR / PLAYWRIGHT_MCP_OUTPUT_DIR(默认输出目录 <cwd>/${DEFAULT_OUTPUT_DIR_NAME})、`
163
360
  );
164
361
  console.log(' HELIX_VISUAL=0(全局关闭视觉反馈,可用 --visual 单次开启)');
362
+ console.log(' HELIX_VISUAL_PERSIST=1(全局常驻反馈层,可用 --no-visual-persist 单次关闭)');
165
363
  }
166
364
 
167
365
  function getBaseUrl() {
@@ -177,12 +375,47 @@ function helixHome(options) {
177
375
  if (!options.headless) {
178
376
  openArgs.push('--headed');
179
377
  }
180
- const code = runPlaywrightCliPassthrough(openArgs);
378
+ const code = runOpenWithProfileRecovery(openArgs, runPlaywrightCliPassthrough, {
379
+ resolveProfileDir,
380
+ log: (msg) => console.error(`[${PACKAGE_JSON.name}] ${msg}`),
381
+ });
181
382
  if (code !== 0) {
182
383
  return code;
183
384
  }
385
+ repairHelixStylesAfterOpen(runPlaywrightCliPassthrough);
184
386
  console.error(`[${PACKAGE_JSON.name}] opened: ${baseUrl}`);
185
- return runPlaywrightCliPassthrough(['eval', 'document.title']);
387
+ return maybePersistLoginToken(runPlaywrightCliPassthrough, {
388
+ log: (msg) => console.error(`[${PACKAGE_JSON.name}] ${msg}`),
389
+ }).then((saved) => {
390
+ if (!saved) {
391
+ console.error(
392
+ `[${PACKAGE_JSON.name}] 未检测到登录 token;登录后可执行 helix token save`
393
+ );
394
+ }
395
+ return runPlaywrightCliPassthrough(['eval', 'document.title']);
396
+ });
397
+ }
398
+
399
+ function helixTokenSave() {
400
+ const read = readTokenFromBrowser(runPlaywrightCliPassthrough);
401
+ if (!read.ok) {
402
+ if (read.reason === 'not_logged_in') {
403
+ console.error(`[${PACKAGE_JSON.name}] 未检测到登录 token,请先登录 ${getBaseUrl()}`);
404
+ } else {
405
+ console.error(
406
+ `[${PACKAGE_JSON.name}] 读取 token 失败:${read.detail || read.reason}`
407
+ );
408
+ }
409
+ return 1;
410
+ }
411
+ const filePath = writeTokenFile(read.token);
412
+ console.log(filePath);
413
+ return 0;
414
+ }
415
+
416
+ function helixTokenPath() {
417
+ console.log(resolveTokenFile());
418
+ return 0;
186
419
  }
187
420
 
188
421
  function helixDoctor(options) {
@@ -200,22 +433,56 @@ function helixDoctor(options) {
200
433
  }
201
434
 
202
435
  async function runPassthroughWithVisual(tokens) {
436
+ const normalized = normalizeProfileArgs(tokens);
203
437
  let visual;
204
438
  let stripped;
205
439
  try {
206
- ({ tokens: stripped, visual } = parseVisualFlags(tokens));
440
+ ({ tokens: stripped, visual } = parseVisualFlags(normalized));
207
441
  } catch (e) {
208
442
  console.error(`[${PACKAGE_JSON.name}] ${e.message}`);
209
443
  return 3;
210
444
  }
211
445
  const withConfig = injectVisualConfig(stripped, visual);
446
+ const passthrough = (args, opts) => runPlaywrightCliPassthrough(args, opts);
447
+ const nativeFileChooser =
448
+ process.env.HELIX_NATIVE_FILE_CHOOSER === undefined ||
449
+ process.env.HELIX_NATIVE_FILE_CHOOSER === '1';
450
+ const cmd = resolveCommandName(stripped);
451
+
212
452
  if (shouldWrapWithVisual(stripped, visual)) {
213
453
  return runWithVisualFeedback(stripped, visual, (args, opts) => {
214
454
  const result = runPlaywrightCliPassthrough(args, opts);
215
455
  return opts && opts.capture ? result.code : result;
216
456
  });
217
457
  }
218
- return runPlaywrightCliPassthrough(withConfig);
458
+
459
+ let code;
460
+ if (nativeFileChooser && (cmd === 'upload' || cmd === 'drop')) {
461
+ code = runUploadCommandWithGuard(withConfig, passthrough);
462
+ } else if (nativeFileChooser && cmd === 'run-code') {
463
+ const guarded = injectVisualConfig(applyRunCodeAutomationGuard(stripped), visual);
464
+ code = passthrough(guarded);
465
+ } else {
466
+ const isOpenWithProfile = Boolean(extractProfileDirFromOpenArgs(withConfig, resolveProfileDir));
467
+ code = isOpenWithProfile
468
+ ? runOpenWithProfileRecovery(withConfig, passthrough, {
469
+ resolveProfileDir,
470
+ log: (msg) => console.error(`[${PACKAGE_JSON.name}] ${msg}`),
471
+ })
472
+ : passthrough(withConfig);
473
+ }
474
+
475
+ if (code === 0) {
476
+ if (isHelixSiteOpen(stripped)) {
477
+ repairHelixStylesAfterOpen(runPlaywrightCliPassthrough);
478
+ }
479
+ if (shouldTryPersistTokenAfterCommand(stripped)) {
480
+ await maybePersistLoginToken(runPlaywrightCliPassthrough, {
481
+ log: (msg) => console.error(`[${PACKAGE_JSON.name}] ${msg}`),
482
+ });
483
+ }
484
+ }
485
+ return code;
219
486
  }
220
487
 
221
488
  /**
@@ -241,8 +508,31 @@ async function main(argv = process.argv) {
241
508
  return 1;
242
509
  }
243
510
 
511
+ if (tokens[0] === 'helix' && tokens[1] === 'token' && tokens[2] === 'path') {
512
+ return helixTokenPath();
513
+ }
514
+
515
+ if (!ensureRuntimeReady()) {
516
+ return 1;
517
+ }
518
+
244
519
  if (tokens[0] === 'helix') {
245
520
  const sub = tokens[1];
521
+ if (sub === 'token') {
522
+ const tokenSub = tokens[2];
523
+ if (tokenSub === 'save') {
524
+ return helixTokenSave();
525
+ }
526
+ if (tokenSub === 'help' || tokenSub === '--help' || tokenSub === '-h') {
527
+ console.log('helix token save 从当前浏览器会话读取 token 并保存');
528
+ console.log('helix token path 输出 token 文件绝对路径');
529
+ return 0;
530
+ }
531
+ console.error(
532
+ `[${PACKAGE_JSON.name}] unknown helix token subcommand: ${tokenSub ?? '(missing)'}. Try: helix token save | helix token path`
533
+ );
534
+ return 2;
535
+ }
246
536
  const rest = parseHelixFlags(tokens, 2);
247
537
  if (rest === null) {
248
538
  return 2;
@@ -273,7 +563,19 @@ main.playwrightCliEntry = playwrightCliEntry;
273
563
  module.exports = {
274
564
  main,
275
565
  resolveOutputDir,
566
+ resolveProfileDir,
567
+ resolveTokenFile,
568
+ resolveHelixConfigDir,
569
+ normalizeProfileArgs,
276
570
  buildChildEnv,
571
+ ensureRuntimeReady,
572
+ runtimeDepInstalled,
573
+ autoInstallEnabled,
574
+ currentNodeMajor,
277
575
  DEFAULT_OUTPUT_DIR_NAME,
576
+ DEFAULT_PROFILE_DIR_NAME,
577
+ DEFAULT_TOKEN_FILE_NAME,
578
+ SKILL_ROOT,
579
+ MIN_NODE_MAJOR,
278
580
  runPassthroughWithVisual,
279
581
  };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Helix 样式修复:绕过磁盘缓存硬刷新(profile 缓存旧 index.html 导致 CSS 404→HTML 时触发)。
3
+ */
4
+ async (page) => {
5
+ await page.reload({ waitUntil: 'networkidle', bypassCache: true });
6
+ return { ok: true };
7
+ };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ const HELIX_HOST_RE = /helixlife\.(cn|net|org)/i;
6
+ const RELOAD_SCRIPT = path.join(__dirname, 'helix-reload-bypass-cache.run-code.js');
7
+
8
+ /** 页面内检测 Helix 主样式表是否加载失败(缓存 HTML 指向已下线 CSS 时 link.sheet 为 null) */
9
+ const BROKEN_STYLES_EVAL = [
10
+ 'eval',
11
+ '--raw',
12
+ `[...document.querySelectorAll('link[rel="stylesheet"]')].filter(l=>l.href.includes('/static/css/')).some(l=>{try{return!l.sheet}catch{return true}})`,
13
+ ];
14
+
15
+ function resolveCommandName(tokens) {
16
+ let i = 0;
17
+ if (tokens[i] === '--raw') i += 1;
18
+ if (tokens[i] === '--json') i += 1;
19
+ if (tokens[i] && tokens[i].startsWith('-s=')) i += 1;
20
+ return tokens[i] || '';
21
+ }
22
+
23
+ function extractOpenUrl(tokens) {
24
+ let i = 0;
25
+ if (tokens[i] === '--raw') i += 1;
26
+ if (tokens[i] === '--json') i += 1;
27
+ if (tokens[i] && tokens[i].startsWith('-s=')) i += 1;
28
+ if (tokens[i] !== 'open') return null;
29
+ i += 1;
30
+ for (; i < tokens.length; i += 1) {
31
+ const t = tokens[i];
32
+ if (t.startsWith('--')) {
33
+ if (!t.includes('=')) {
34
+ const next = tokens[i + 1];
35
+ if (next !== undefined && !next.startsWith('-')) i += 1;
36
+ }
37
+ continue;
38
+ }
39
+ if (t.startsWith('http://') || t.startsWith('https://')) return t;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function isHelixSiteOpen(tokens) {
45
+ if (resolveCommandName(tokens) !== 'open') return false;
46
+ const url = extractOpenUrl(tokens);
47
+ return Boolean(url && HELIX_HOST_RE.test(url));
48
+ }
49
+
50
+ /**
51
+ * open 完成后检测样式表;若主 CSS 未生效则 bypassCache 硬刷新一次。
52
+ * @param {(args: string[], opts?: object) => number | { code: number, stdout: string, stderr: string }} runPassthrough
53
+ * @returns {number} 附加步骤退出码(0 表示成功或未触发修复)
54
+ */
55
+ function repairHelixStylesAfterOpen(runPassthrough) {
56
+ const check = runPassthrough(BROKEN_STYLES_EVAL, { capture: true });
57
+ const code = typeof check === 'object' ? check.code : check;
58
+ if (code !== 0) return code;
59
+
60
+ const broken = String(check.stdout || '').trim() === 'true';
61
+ if (!broken) return 0;
62
+
63
+ const reload = runPassthrough(
64
+ ['run-code', `--filename=${RELOAD_SCRIPT.replace(/\\/g, '/')}`],
65
+ { capture: true },
66
+ );
67
+ return typeof reload === 'object' ? reload.code : reload;
68
+ }
69
+
70
+ module.exports = {
71
+ HELIX_HOST_RE,
72
+ BROKEN_STYLES_EVAL,
73
+ RELOAD_SCRIPT,
74
+ resolveCommandName,
75
+ isHelixSiteOpen,
76
+ repairHelixStylesAfterOpen,
77
+ };
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 隐藏依赖包内的嵌套 Skill(如 @playwright/cli、playwright-core 自带的
5
+ * `SKILL.md`),避免被 WorkBuddy / Cursor 等按 `**\/SKILL.md` 扫描后注册为
6
+ * playwright-cli、playwright-trace 等额外工具项。
7
+ *
8
+ * 处理方式:把 `node_modules/**\/SKILL.md` 重命名为 `SKILL.md.disabled`。
9
+ * - 仅改文件名(basename 不再等于 `SKILL.md`),技能扫描器不再发现;
10
+ * - 这些文件只是给 Agent 看的说明文档,底层运行时不读取,重命名不影响功能;
11
+ * - 可逆:需要时把 `.disabled` 改回即可;重新 `npm install` 也会还原后由
12
+ * postinstall 再次处理。
13
+ */
14
+
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+
18
+ const SKILL_FILENAME = 'SKILL.md';
19
+ const DISABLED_SUFFIX = '.disabled';
20
+
21
+ /** 写入 node_modules 根的哨兵文件,标记已处理,供 CLI 快路径跳过重复扫描。 */
22
+ const SENTINEL_NAME = '.helix-nested-skills-hidden';
23
+
24
+ /** 遍历目录时跳过的子目录(避免无意义的深扫)。 */
25
+ const SKIP_DIRS = new Set(['.bin', '.cache']);
26
+
27
+ /**
28
+ * 递归收集 dir 下所有 basename 为 `SKILL.md` 的文件绝对路径。
29
+ * @param {string} dir
30
+ * @param {string[]} acc
31
+ */
32
+ function collectSkillFiles(dir, acc) {
33
+ let entries;
34
+ try {
35
+ entries = fs.readdirSync(dir, { withFileTypes: true });
36
+ } catch {
37
+ return;
38
+ }
39
+ for (const entry of entries) {
40
+ const full = path.join(dir, entry.name);
41
+ if (entry.isDirectory()) {
42
+ if (SKIP_DIRS.has(entry.name)) {
43
+ continue;
44
+ }
45
+ collectSkillFiles(full, acc);
46
+ } else if (entry.isFile() && entry.name === SKILL_FILENAME) {
47
+ acc.push(full);
48
+ }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * 隐藏 <skillRoot>/node_modules 内的全部嵌套 SKILL.md。
54
+ * 幂等:已重命名过的文件不会被再次发现。
55
+ * @param {string} skillRoot 含本仓库 package.json 的目录
56
+ * @param {{ log?: (msg: string) => void }} [options]
57
+ * @returns {{ hidden: number, files: string[] }}
58
+ */
59
+ function hideNestedSkills(skillRoot, options = {}) {
60
+ const log = options.log;
61
+ const nodeModules = path.join(skillRoot, 'node_modules');
62
+ if (!fs.existsSync(nodeModules)) {
63
+ return { hidden: 0, files: [] };
64
+ }
65
+
66
+ const found = [];
67
+ collectSkillFiles(nodeModules, found);
68
+
69
+ const hiddenFiles = [];
70
+ for (const file of found) {
71
+ const target = file + DISABLED_SUFFIX;
72
+ try {
73
+ // 覆盖式重命名:若上次的 .disabled 残留(如 reinstall 重新生成了 SKILL.md),先清理。
74
+ if (fs.existsSync(target)) {
75
+ fs.rmSync(target, { force: true });
76
+ }
77
+ fs.renameSync(file, target);
78
+ hiddenFiles.push(file);
79
+ } catch (e) {
80
+ if (log) {
81
+ log(`无法隐藏嵌套 Skill ${file}: ${e && e.message ? e.message : e}`);
82
+ }
83
+ }
84
+ }
85
+
86
+ // 始终刷新哨兵,标记已处理(即便本次没有可隐藏文件,也避免后续重复扫描)。
87
+ try {
88
+ fs.writeFileSync(path.join(nodeModules, SENTINEL_NAME), new Date().toISOString());
89
+ } catch {
90
+ // 哨兵写入失败不影响主流程。
91
+ }
92
+
93
+ if (log && hiddenFiles.length > 0) {
94
+ log(`已隐藏 ${hiddenFiles.length} 个依赖内嵌套 Skill(不对外注册)。`);
95
+ }
96
+ return { hidden: hiddenFiles.length, files: hiddenFiles };
97
+ }
98
+
99
+ /**
100
+ * 哨兵不存在时执行一次隐藏(供 CLI 快路径低成本调用)。
101
+ * @param {string} skillRoot
102
+ * @param {{ log?: (msg: string) => void }} [options]
103
+ * @returns {boolean} 本次是否执行了扫描
104
+ */
105
+ function hideNestedSkillsOnce(skillRoot, options = {}) {
106
+ const sentinel = path.join(skillRoot, 'node_modules', SENTINEL_NAME);
107
+ if (fs.existsSync(sentinel)) {
108
+ return false;
109
+ }
110
+ hideNestedSkills(skillRoot, options);
111
+ return true;
112
+ }
113
+
114
+ module.exports = {
115
+ hideNestedSkills,
116
+ hideNestedSkillsOnce,
117
+ SENTINEL_NAME,
118
+ SKILL_FILENAME,
119
+ DISABLED_SUFFIX,
120
+ };