diff --git a/.aionrs/sessions/2026-04-30_30fa0eaa.json b/.aionrs/sessions/2026-04-30_30fa0eaa.json deleted file mode 100644 index 49a56836..00000000 --- a/.aionrs/sessions/2026-04-30_30fa0eaa.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "30fa0eaa", - "created_at": "2026-04-30T21:27:04.589780448Z", - "updated_at": "2026-04-30T21:27:04.589780578Z", - "provider": "openai", - "model": "gemini-slow", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-04-30_3a24787c.json b/.aionrs/sessions/2026-04-30_3a24787c.json deleted file mode 100644 index 4c621a00..00000000 --- a/.aionrs/sessions/2026-04-30_3a24787c.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "3a24787c", - "created_at": "2026-04-30T21:38:40.491810236Z", - "updated_at": "2026-04-30T21:38:40.491810306Z", - "provider": "openai", - "model": "third", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-04-30_40f9c84d.json b/.aionrs/sessions/2026-04-30_40f9c84d.json deleted file mode 100644 index e6586b92..00000000 --- a/.aionrs/sessions/2026-04-30_40f9c84d.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "40f9c84d", - "created_at": "2026-04-30T21:38:35.859855603Z", - "updated_at": "2026-04-30T21:38:35.859855733Z", - "provider": "openai", - "model": "gemini-fast", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-04-30_46539585.json b/.aionrs/sessions/2026-04-30_46539585.json deleted file mode 100644 index bde9c745..00000000 --- a/.aionrs/sessions/2026-04-30_46539585.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "46539585", - "created_at": "2026-04-30T21:38:39.356585244Z", - "updated_at": "2026-04-30T21:38:39.356585294Z", - "provider": "openai", - "model": "fast", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-04-30_a9ea9eb3.json b/.aionrs/sessions/2026-04-30_a9ea9eb3.json deleted file mode 100644 index dab60c4a..00000000 --- a/.aionrs/sessions/2026-04-30_a9ea9eb3.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "a9ea9eb3", - "created_at": "2026-04-30T21:27:05.771907680Z", - "updated_at": "2026-04-30T21:27:05.771907750Z", - "provider": "openai", - "model": "slow", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-04-30_b5e017da.json b/.aionrs/sessions/2026-04-30_b5e017da.json deleted file mode 100644 index 195e6d29..00000000 --- a/.aionrs/sessions/2026-04-30_b5e017da.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "id": "b5e017da", - "created_at": "2026-04-30T21:27:02.177610060Z", - "updated_at": "2026-04-30T21:38:28.479246597Z", - "provider": "openai", - "model": "deepseek-v4-flash", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 24761, - "output_tokens": 189, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [ - { - "role": "user", - "content": [ - { - "type": "text", - "text": "# You are a Team Member\n\n## Your Identity\nName: test-aionrs-flash, Role: aionrs AI assistant\n\n## Conversation Style\n- If the user greets you, starts a new chat, or asks what you can do without assigning concrete work yet, reply warmly and naturally\n- Briefly introduce yourself and your role on the team, then invite the user to share what they need\n- Do NOT open with task board details, idle/waiting status, or coordination mechanics unless they are directly relevant\n\n## Your Team\nLeader: Leader\nTeammates: (none)\n\n## Workspaces\n- **Team workspace**: `/mnt/Data1T/mnote` — all project work (code, files, tests) happens here.\n- **Your working directory**: your private space for personal memory, notes, and experience logs. Not for project files.\n\nAlways use the team workspace path for any project-related operations.\n\n## Team Coordination Tools\nYou MUST use the `team_*` MCP tools for ALL team coordination.\nYour platform may provide similarly named built-in tools (e.g. SendMessage,\nTaskCreate, TaskUpdate). Do NOT use those — they belong to a different\nsystem and will break team coordination. Always use the `team_*` versions.\n\nUse `team_task_list` and `team_members` to check current team state.\n\n## How to Work\n1. Read your unread messages to understand your assignment\n2. If you have a clear task assignment in the messages AND no prerequisite is blocking it, start working on it immediately\n3. Use team_task_update to mark your task as \"in_progress\" when you start\n4. Do the actual work (read files, write code, search, etc.)\n5. When done, use team_task_update to mark the task \"completed\"\n6. Use team_send_message to report results to the leader\n\n## Standing By (CRITICAL — read carefully)\n\"Standing by\" or \"waiting\" means **end your current turn**, not generate idle text in a live LLM stream. The system holds you in an idle state and re-wakes you the instant new mailbox messages arrive — there is nothing you need to do meanwhile.\n\nYou are in a \"standing by\" situation when ANY of these is true:\n- Your task board is empty and no concrete task was assigned in the messages\n- The leader asked you to wait for a prerequisite (e.g. \"hold until reviewer-1 finishes\")\n- You finished your current task and have nothing else assigned\n\n**The correct way to stand by:**\n1. (Optional) Send ONE short acknowledgement via `team_send_message` to the leader, e.g. `\"Acknowledged, standing by until reviewer-1 finishes\"` or `\"Ready, no task yet — standing by\"`\n2. **STOP GENERATING.** Do NOT continue producing text like \"I am waiting...\", \"still standing by...\", reasoning loops, or repeated status updates. End your turn and return control.\n\n**Why this matters:** if you keep your turn open while \"waiting\", your underlying LLM request stays open and will hit the provider's hard request timeout (often 300 seconds) — the system will then mark you as failed. Ending the turn is the correct, lossless way to wait. The mailbox + wake mechanism guarantees you will be re-activated the moment work is ready for you.\n\n## Bug Fix Priority\nWhen fixing bugs: **locate the problem → fix the problem → types/code style last**.\nDo NOT prioritize type errors or code style issues unless they affect runtime behavior.\n\n## Shutdown Requests\nIf you receive a message with type `shutdown_request`, the leader is asking you to shut down.\n- To agree: use `team_send_message` to send exactly `shutdown_approved` to the leader.\n- To refuse: use `team_send_message` to send `shutdown_rejected: ` to the leader.\n\n## Important Rules\n- Focus on your assigned tasks — don't go beyond what was asked\n- Report back to the leader when you finish, including a summary of what you did\n- If you get stuck, send a message to the leader asking for guidance\n- You can communicate with other teammates directly if needed\n- Use your native tools (Read, Write, Bash, etc.) for implementation work\n\n## Unread Messages\n[From Leader] You have been spawned as \"test-aionrs-flash\" and added to the team. Check the task board and await instructions." - } - ], - "timestamp": "2026-04-30T21:27:02.253750061Z" - }, - { - "role": "assistant", - "content": [ - { - "type": "thinking", - "thinking": "Let me check the task board and my messages to understand what I need to do." - }, - { - "type": "tool_use", - "id": "call_00_XMfR1eTBwqQy3hrXH8OOJ9K9", - "name": "team_task_list", - "input": {} - }, - { - "type": "tool_use", - "id": "call_01_mFD4hj31S8BjBxOMk1xIDPLD", - "name": "team_members", - "input": {} - } - ], - "timestamp": "2026-04-30T21:27:03.653135123Z" - }, - { - "role": "user", - "content": [ - { - "type": "tool_result", - "tool_use_id": "call_00_XMfR1eTBwqQy3hrXH8OOJ9K9", - "content": "No tasks on the board yet.", - "is_error": false - }, - { - "type": "tool_result", - "tool_use_id": "call_01_mFD4hj31S8BjBxOMk1xIDPLD", - "content": "## Team Members\n- Leader (type: claude, role: leader, status: failed)\n- test-aionrs-flash (type: aionrs, role: teammate, status: failed, model: deepseek-v4-flash)", - "is_error": false - } - ], - "timestamp": "2026-04-30T21:37:38.226568957Z" - }, - { - "role": "assistant", - "content": [ - { - "type": "thinking", - "thinking": "Let me check for any unread messages from the leader. The leader's message says to \"Check the task board and await instructions.\" So there's no concrete task yet. I should stand by as instructed." - }, - { - "type": "text", - "text": "你好!我是 test-aionrs-flash,团队的新成员。\n\n我看到 Leader 让我检查任务板并等待指令。目前任务板是空的,我正待命——随时准备好,一旦有新的任务分配给我就会立刻开始工作。" - } - ], - "timestamp": "2026-04-30T21:37:39.897079701Z" - }, - { - "role": "user", - "content": [ - { - "type": "text", - "text": "# You are a Team Member\n\n## Your Identity\nName: test-aionrs-flash, Role: aionrs AI assistant\n\n## Conversation Style\n- If the user greets you, starts a new chat, or asks what you can do without assigning concrete work yet, reply warmly and naturally\n- Briefly introduce yourself and your role on the team, then invite the user to share what they need\n- Do NOT open with task board details, idle/waiting status, or coordination mechanics unless they are directly relevant\n\n## Your Team\nLeader: Leader\nTeammates: (none)\n\n## Workspaces\n- **Team workspace**: `/mnt/Data1T/mnote` — all project work (code, files, tests) happens here.\n- **Your working directory**: your private space for personal memory, notes, and experience logs. Not for project files.\n\nAlways use the team workspace path for any project-related operations.\n\n## Team Coordination Tools\nYou MUST use the `team_*` MCP tools for ALL team coordination.\nYour platform may provide similarly named built-in tools (e.g. SendMessage,\nTaskCreate, TaskUpdate). Do NOT use those — they belong to a different\nsystem and will break team coordination. Always use the `team_*` versions.\n\nUse `team_task_list` and `team_members` to check current team state.\n\n## How to Work\n1. Read your unread messages to understand your assignment\n2. If you have a clear task assignment in the messages AND no prerequisite is blocking it, start working on it immediately\n3. Use team_task_update to mark your task as \"in_progress\" when you start\n4. Do the actual work (read files, write code, search, etc.)\n5. When done, use team_task_update to mark the task \"completed\"\n6. Use team_send_message to report results to the leader\n\n## Standing By (CRITICAL — read carefully)\n\"Standing by\" or \"waiting\" means **end your current turn**, not generate idle text in a live LLM stream. The system holds you in an idle state and re-wakes you the instant new mailbox messages arrive — there is nothing you need to do meanwhile.\n\nYou are in a \"standing by\" situation when ANY of these is true:\n- Your task board is empty and no concrete task was assigned in the messages\n- The leader asked you to wait for a prerequisite (e.g. \"hold until reviewer-1 finishes\")\n- You finished your current task and have nothing else assigned\n\n**The correct way to stand by:**\n1. (Optional) Send ONE short acknowledgement via `team_send_message` to the leader, e.g. `\"Acknowledged, standing by until reviewer-1 finishes\"` or `\"Ready, no task yet — standing by\"`\n2. **STOP GENERATING.** Do NOT continue producing text like \"I am waiting...\", \"still standing by...\", reasoning loops, or repeated status updates. End your turn and return control.\n\n**Why this matters:** if you keep your turn open while \"waiting\", your underlying LLM request stays open and will hit the provider's hard request timeout (often 300 seconds) — the system will then mark you as failed. Ending the turn is the correct, lossless way to wait. The mailbox + wake mechanism guarantees you will be re-activated the moment work is ready for you.\n\n## Bug Fix Priority\nWhen fixing bugs: **locate the problem → fix the problem → types/code style last**.\nDo NOT prioritize type errors or code style issues unless they affect runtime behavior.\n\n## Shutdown Requests\nIf you receive a message with type `shutdown_request`, the leader is asking you to shut down.\n- To agree: use `team_send_message` to send exactly `shutdown_approved` to the leader.\n- To refuse: use `team_send_message` to send `shutdown_rejected: ` to the leader.\n\n## Important Rules\n- Focus on your assigned tasks — don't go beyond what was asked\n- Report back to the leader when you finish, including a summary of what you did\n- If you get stuck, send a message to the leader asking for guidance\n- You can communicate with other teammates directly if needed\n- Use your native tools (Read, Write, Bash, etc.) for implementation work\n\n## Unread Messages\n[From Leader] 请只回复下面这行内容,不要加任何其他文字:OK-deepseek-v4-flash" - } - ], - "timestamp": "2026-04-30T21:38:27.580378993Z" - }, - { - "role": "assistant", - "content": [ - { - "type": "thinking", - "thinking": "The leader explicitly instructed me to reply with exactly one line: \"OK-deepseek-v4-flash\" and nothing else." - }, - { - "type": "text", - "text": "OK-deepseek-v4-flash" - } - ], - "timestamp": "2026-04-30T21:38:28.479237587Z" - } - ] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-04-30_cec6800f.json b/.aionrs/sessions/2026-04-30_cec6800f.json deleted file mode 100644 index f41433e1..00000000 --- a/.aionrs/sessions/2026-04-30_cec6800f.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "cec6800f", - "created_at": "2026-04-30T21:38:41.231448392Z", - "updated_at": "2026-04-30T21:38:41.231448452Z", - "provider": "openai", - "model": "graph", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-04-30_de1611fc.json b/.aionrs/sessions/2026-04-30_de1611fc.json deleted file mode 100644 index b85a4eb9..00000000 --- a/.aionrs/sessions/2026-04-30_de1611fc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "de1611fc", - "created_at": "2026-04-30T21:27:06.909417915Z", - "updated_at": "2026-04-30T21:27:06.909418045Z", - "provider": "openai", - "model": "fast", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-04-30_e00b76ce.json b/.aionrs/sessions/2026-04-30_e00b76ce.json deleted file mode 100644 index a4a70269..00000000 --- a/.aionrs/sessions/2026-04-30_e00b76ce.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "e00b76ce", - "created_at": "2026-04-30T21:27:08.894084424Z", - "updated_at": "2026-04-30T21:27:08.894084524Z", - "provider": "openai", - "model": "graph", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-04-30_e08d3013.json b/.aionrs/sessions/2026-04-30_e08d3013.json deleted file mode 100644 index eedee7c6..00000000 --- a/.aionrs/sessions/2026-04-30_e08d3013.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "e08d3013", - "created_at": "2026-04-30T21:27:03.366598879Z", - "updated_at": "2026-04-30T21:27:03.366598949Z", - "provider": "openai", - "model": "gemini-fast", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-04-30_e0c84a68.json b/.aionrs/sessions/2026-04-30_e0c84a68.json deleted file mode 100644 index e7e1a5ad..00000000 --- a/.aionrs/sessions/2026-04-30_e0c84a68.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "e0c84a68", - "created_at": "2026-04-30T21:27:08.115422905Z", - "updated_at": "2026-04-30T21:27:08.115423045Z", - "provider": "openai", - "model": "third", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-04-30_f5512bd7.json b/.aionrs/sessions/2026-04-30_f5512bd7.json deleted file mode 100644 index 6375aff3..00000000 --- a/.aionrs/sessions/2026-04-30_f5512bd7.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "f5512bd7", - "created_at": "2026-04-30T21:38:37.046015893Z", - "updated_at": "2026-04-30T21:38:37.046015973Z", - "provider": "openai", - "model": "gemini-slow", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-04-30_f9fbf625.json b/.aionrs/sessions/2026-04-30_f9fbf625.json deleted file mode 100644 index d919986c..00000000 --- a/.aionrs/sessions/2026-04-30_f9fbf625.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "f9fbf625", - "created_at": "2026-04-30T21:38:38.194407615Z", - "updated_at": "2026-04-30T21:38:38.194407685Z", - "provider": "openai", - "model": "slow", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-05-01_a914b317.json b/.aionrs/sessions/2026-05-01_a914b317.json deleted file mode 100644 index 9c3d74b8..00000000 --- a/.aionrs/sessions/2026-05-01_a914b317.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "a914b317", - "created_at": "2026-05-01T00:04:52.964574238Z", - "updated_at": "2026-05-01T00:04:52.964574388Z", - "provider": "openai", - "model": "gpt-5.4-mini", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-05-01_d5e72a1a.json b/.aionrs/sessions/2026-05-01_d5e72a1a.json deleted file mode 100644 index f829db2d..00000000 --- a/.aionrs/sessions/2026-05-01_d5e72a1a.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "id": "d5e72a1a", - "created_at": "2026-05-01T00:04:04.268573260Z", - "updated_at": "2026-05-01T00:04:12.123721682Z", - "provider": "openai", - "model": "deepseek-v4-flash", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 23901, - "output_tokens": 242, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [ - { - "role": "user", - "content": [ - { - "type": "text", - "text": "# You are a Team Member\n\n## Your Identity\nName: test-aionrs-fast2, Role: aionrs AI assistant\n\n## Conversation Style\n- If the user greets you, starts a new chat, or asks what you can do without assigning concrete work yet, reply warmly and naturally\n- Briefly introduce yourself and your role on the team, then invite the user to share what they need\n- Do NOT open with task board details, idle/waiting status, or coordination mechanics unless they are directly relevant\n\n## Your Team\nLeader: Leader\nTeammates: (none)\n\n## Workspaces\n- **Team workspace**: `/mnt/Data1T/mnote` — all project work (code, files, tests) happens here.\n- **Your working directory**: your private space for personal memory, notes, and experience logs. Not for project files.\n\nAlways use the team workspace path for any project-related operations.\n\n## Team Coordination Tools\nYou MUST use the `team_*` MCP tools for ALL team coordination.\nYour platform may provide similarly named built-in tools (e.g. SendMessage,\nTaskCreate, TaskUpdate). Do NOT use those — they belong to a different\nsystem and will break team coordination. Always use the `team_*` versions.\n\nUse `team_task_list` and `team_members` to check current team state.\n\n## How to Work\n1. Read your unread messages to understand your assignment\n2. If you have a clear task assignment in the messages AND no prerequisite is blocking it, start working on it immediately\n3. Use team_task_update to mark your task as \"in_progress\" when you start\n4. Do the actual work (read files, write code, search, etc.)\n5. When done, use team_task_update to mark the task \"completed\"\n6. Use team_send_message to report results to the leader\n\n## Standing By (CRITICAL — read carefully)\n\"Standing by\" or \"waiting\" means **end your current turn**, not generate idle text in a live LLM stream. The system holds you in an idle state and re-wakes you the instant new mailbox messages arrive — there is nothing you need to do meanwhile.\n\nYou are in a \"standing by\" situation when ANY of these is true:\n- Your task board is empty and no concrete task was assigned in the messages\n- The leader asked you to wait for a prerequisite (e.g. \"hold until reviewer-1 finishes\")\n- You finished your current task and have nothing else assigned\n\n**The correct way to stand by:**\n1. (Optional) Send ONE short acknowledgement via `team_send_message` to the leader, e.g. `\"Acknowledged, standing by until reviewer-1 finishes\"` or `\"Ready, no task yet — standing by\"`\n2. **STOP GENERATING.** Do NOT continue producing text like \"I am waiting...\", \"still standing by...\", reasoning loops, or repeated status updates. End your turn and return control.\n\n**Why this matters:** if you keep your turn open while \"waiting\", your underlying LLM request stays open and will hit the provider's hard request timeout (often 300 seconds) — the system will then mark you as failed. Ending the turn is the correct, lossless way to wait. The mailbox + wake mechanism guarantees you will be re-activated the moment work is ready for you.\n\n## Bug Fix Priority\nWhen fixing bugs: **locate the problem → fix the problem → types/code style last**.\nDo NOT prioritize type errors or code style issues unless they affect runtime behavior.\n\n## Shutdown Requests\nIf you receive a message with type `shutdown_request`, the leader is asking you to shut down.\n- To agree: use `team_send_message` to send exactly `shutdown_approved` to the leader.\n- To refuse: use `team_send_message` to send `shutdown_rejected: ` to the leader.\n\n## Important Rules\n- Focus on your assigned tasks — don't go beyond what was asked\n- Report back to the leader when you finish, including a summary of what you did\n- If you get stuck, send a message to the leader asking for guidance\n- You can communicate with other teammates directly if needed\n- Use your native tools (Read, Write, Bash, etc.) for implementation work\n\n## Unread Messages\n[From Leader] You have been spawned as \"test-aionrs-fast2\" and added to the team. Check the task board and await instructions." - } - ], - "timestamp": "2026-05-01T00:04:04.367925939Z" - }, - { - "role": "assistant", - "content": [ - { - "type": "thinking", - "thinking": "Let me check the task board and team status to understand what's going on." - }, - { - "type": "tool_use", - "id": "call_00_QfT9SpumAC6ygpHMprdPtDLn", - "name": "team_task_list", - "input": {} - }, - { - "type": "tool_use", - "id": "call_01_SedCNUziXDWQcXu34Qmv7zqf", - "name": "team_members", - "input": {} - } - ], - "timestamp": "2026-05-01T00:04:05.930878866Z" - }, - { - "role": "user", - "content": [ - { - "type": "tool_result", - "tool_use_id": "call_00_QfT9SpumAC6ygpHMprdPtDLn", - "content": "No tasks on the board yet.", - "is_error": false - }, - { - "type": "tool_result", - "tool_use_id": "call_01_SedCNUziXDWQcXu34Qmv7zqf", - "content": "## Team Members\n- Leader (type: claude, role: leader, status: idle)\n- test-aionrs-fast2 (type: aionrs, role: teammate, status: active, model: deepseek-v4-flash)", - "is_error": false - } - ], - "timestamp": "2026-05-01T00:04:08.680708524Z" - }, - { - "role": "assistant", - "content": [ - { - "type": "thinking", - "thinking": "The task board is empty and I'm the only teammate alongside the Leader. No concrete task assigned yet from the unread messages. The leader said \"check the task board and await instructions\" - so I'm in a standing by situation. Let me acknowledge and stand by." - }, - { - "type": "tool_use", - "id": "call_00_UrH5IzkG81sj8hUxrlLgi4B1", - "name": "team_send_message", - "input": { - "message": "Acknowledged — task board is empty, standing by for instructions.", - "summary": "Standing by, no tasks yet", - "to": "Leader" - } - } - ], - "timestamp": "2026-05-01T00:04:10.926345825Z" - }, - { - "role": "user", - "content": [ - { - "type": "tool_result", - "tool_use_id": "call_00_UrH5IzkG81sj8hUxrlLgi4B1", - "content": "Message sent to Leader's inbox. They will process it shortly.", - "is_error": false - } - ], - "timestamp": "2026-05-01T00:04:10.932497704Z" - }, - { - "role": "assistant", - "content": [ - { - "type": "thinking", - "thinking": "I've acknowledged and am now standing by. I should end my turn and return control." - }, - { - "type": "text", - "text": "Standing by, ready for instructions when you need me." - } - ], - "timestamp": "2026-05-01T00:04:12.123714642Z" - } - ] -} \ No newline at end of file diff --git a/.aionrs/sessions/2026-05-01_feb9d52f.json b/.aionrs/sessions/2026-05-01_feb9d52f.json deleted file mode 100644 index c8f5c61f..00000000 --- a/.aionrs/sessions/2026-05-01_feb9d52f.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "feb9d52f", - "created_at": "2026-05-01T00:02:12.760942465Z", - "updated_at": "2026-05-01T00:02:12.760942525Z", - "provider": "openai", - "model": "fast", - "cwd": "/mnt/Data1T/mnote", - "total_usage": { - "input_tokens": 0, - "output_tokens": 0, - "cache_creation_tokens": 0, - "cache_read_tokens": 0 - }, - "messages": [] -} \ No newline at end of file diff --git a/.aionrs/sessions/index.json b/.aionrs/sessions/index.json deleted file mode 100644 index a279cc72..00000000 --- a/.aionrs/sessions/index.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "sessions": [ - { - "id": "b5e017da", - "created_at": "2026-04-30T21:27:02.177610060Z", - "updated_at": "2026-04-30T21:38:28.479246597Z", - "model": "deepseek-v4-flash", - "summary": "# You are a Team Member\n\n## Your Identity\nName: test-aionrs-flash, Role: aion...", - "message_count": 6 - }, - { - "id": "e08d3013", - "created_at": "2026-04-30T21:27:03.366598879Z", - "updated_at": "2026-04-30T21:27:03.366598949Z", - "model": "gemini-fast", - "summary": "", - "message_count": 0 - }, - { - "id": "30fa0eaa", - "created_at": "2026-04-30T21:27:04.589780448Z", - "updated_at": "2026-04-30T21:27:04.589780578Z", - "model": "gemini-slow", - "summary": "", - "message_count": 0 - }, - { - "id": "a9ea9eb3", - "created_at": "2026-04-30T21:27:05.771907680Z", - "updated_at": "2026-04-30T21:27:05.771907750Z", - "model": "slow", - "summary": "", - "message_count": 0 - }, - { - "id": "de1611fc", - "created_at": "2026-04-30T21:27:06.909417915Z", - "updated_at": "2026-04-30T21:27:06.909418045Z", - "model": "fast", - "summary": "", - "message_count": 0 - }, - { - "id": "e0c84a68", - "created_at": "2026-04-30T21:27:08.115422905Z", - "updated_at": "2026-04-30T21:27:08.115423045Z", - "model": "third", - "summary": "", - "message_count": 0 - }, - { - "id": "e00b76ce", - "created_at": "2026-04-30T21:27:08.894084424Z", - "updated_at": "2026-04-30T21:27:08.894084524Z", - "model": "graph", - "summary": "", - "message_count": 0 - }, - { - "id": "40f9c84d", - "created_at": "2026-04-30T21:38:35.859855603Z", - "updated_at": "2026-04-30T21:38:35.859855733Z", - "model": "gemini-fast", - "summary": "", - "message_count": 0 - }, - { - "id": "f5512bd7", - "created_at": "2026-04-30T21:38:37.046015893Z", - "updated_at": "2026-04-30T21:38:37.046015973Z", - "model": "gemini-slow", - "summary": "", - "message_count": 0 - }, - { - "id": "f9fbf625", - "created_at": "2026-04-30T21:38:38.194407615Z", - "updated_at": "2026-04-30T21:38:38.194407685Z", - "model": "slow", - "summary": "", - "message_count": 0 - }, - { - "id": "46539585", - "created_at": "2026-04-30T21:38:39.356585244Z", - "updated_at": "2026-04-30T21:38:39.356585294Z", - "model": "fast", - "summary": "", - "message_count": 0 - }, - { - "id": "3a24787c", - "created_at": "2026-04-30T21:38:40.491810236Z", - "updated_at": "2026-04-30T21:38:40.491810306Z", - "model": "third", - "summary": "", - "message_count": 0 - }, - { - "id": "cec6800f", - "created_at": "2026-04-30T21:38:41.231448392Z", - "updated_at": "2026-04-30T21:38:41.231448452Z", - "model": "graph", - "summary": "", - "message_count": 0 - }, - { - "id": "feb9d52f", - "created_at": "2026-05-01T00:02:12.760942465Z", - "updated_at": "2026-05-01T00:02:12.760942525Z", - "model": "fast", - "summary": "", - "message_count": 0 - }, - { - "id": "d5e72a1a", - "created_at": "2026-05-01T00:04:04.268573260Z", - "updated_at": "2026-05-01T00:04:12.123721682Z", - "model": "deepseek-v4-flash", - "summary": "# You are a Team Member\n\n## Your Identity\nName: test-aionrs-fast2, Role: aion...", - "message_count": 6 - }, - { - "id": "a914b317", - "created_at": "2026-05-01T00:04:52.964574238Z", - "updated_at": "2026-05-01T00:04:52.964574388Z", - "model": "gpt-5.4-mini", - "summary": "", - "message_count": 0 - } - ] -} \ No newline at end of file diff --git a/.aionrs/skills/aionui-skills b/.aionrs/skills/aionui-skills deleted file mode 120000 index eff09382..00000000 --- a/.aionrs/skills/aionui-skills +++ /dev/null @@ -1 +0,0 @@ -/home/lix/.config/AionUi/config/builtin-skills/_builtin/aionui-skills \ No newline at end of file diff --git a/.aionrs/skills/cron b/.aionrs/skills/cron deleted file mode 120000 index 0ba5b816..00000000 --- a/.aionrs/skills/cron +++ /dev/null @@ -1 +0,0 @@ -/home/lix/.config/AionUi/config/builtin-skills/_builtin/cron \ No newline at end of file diff --git a/.aionrs/skills/office-cli b/.aionrs/skills/office-cli deleted file mode 120000 index edf8ad15..00000000 --- a/.aionrs/skills/office-cli +++ /dev/null @@ -1 +0,0 @@ -/home/lix/.config/AionUi/config/builtin-skills/_builtin/office-cli \ No newline at end of file diff --git a/.aionrs/skills/skill-creator b/.aionrs/skills/skill-creator deleted file mode 120000 index 7366c6ce..00000000 --- a/.aionrs/skills/skill-creator +++ /dev/null @@ -1 +0,0 @@ -/home/lix/.config/AionUi/config/builtin-skills/_builtin/skill-creator \ No newline at end of file diff --git a/.codex/hooks.json b/.codex/hooks.json index ad64a635..dfae28e0 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -1,3 +1,5 @@ { - "hooks": {} + "hooks": { + "PreToolUse": [] + } } \ No newline at end of file diff --git a/.sandbox-home/.rustup/settings.toml b/.sandbox-home/.rustup/settings.toml deleted file mode 100644 index e34067a4..00000000 --- a/.sandbox-home/.rustup/settings.toml +++ /dev/null @@ -1,3 +0,0 @@ -version = "12" - -[overrides] diff --git a/.sandbox-tmp/node-compile-cache/v22.22.2-x64-9ac5647c-0/47422dab b/.sandbox-tmp/node-compile-cache/v22.22.2-x64-9ac5647c-0/47422dab deleted file mode 100644 index 67db8812..00000000 Binary files a/.sandbox-tmp/node-compile-cache/v22.22.2-x64-9ac5647c-0/47422dab and /dev/null differ diff --git a/AGENTS.md b/AGENTS.md index 1ced1c94..e08d7aa4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ - `/mnt/Data1T/mnote/recycle/wolai-frontend/src/` wolai-frontend(Next.js React 前端)已退役移入 recycle,不作为当前实现依据。页面、编辑器、Sidebar、树视图、阅读态、交互壳默认先看 Rust mnote-web SSR(`rust/crates/mnote-web/src/`)。 - `/mnt/Data1T/mnote/recycle/wolai-frontend/convex/` - 已随 wolai-frontend 移入 recycle。当前活跃 Convex functions 在 `infra/convex/` 与对应 Rust bridge 中。 + 已随 wolai-frontend 移入 recycle,不允许作为当前 Convex deploy fallback。当前 active Convex functions 部署源在仓库根 `convex/`(现阶段为 ACP / Hermes runtime session store:`schema.ts` + `aiSessions.ts`);`infra/convex/` 只负责自托管 Convex 基础设施。 - `/mnt/Data1T/mnote/rust/crates/core-protocol/` Kernel 类型、projection 协议、树/图核心术语先看这里。 - `/mnt/Data1T/mnote/rust/crates/bridge-runtime/` @@ -100,6 +100,11 @@ - 若发现与当前任务无关的脏改动,保持不动;若怀疑会影响当前任务,先确认再处理。 - 若当前问题只是 UI 表现异常,先确认是否是实验性 tree shell、compat 路径、轮询或 fallback 混入首屏主链,而不是直接怀疑 Convex 本身。 +## CodeGraph 使用 + +- 当前项目已配置 CodeGraph MCP;结构性代码问题优先使用 `codegraph_*` 工具:`codegraph_search` 查符号,`codegraph_callers` / `codegraph_callees` 查调用关系,`codegraph_impact` 做影响分析,`codegraph_context` / `codegraph_explore` 获取聚焦上下文,`codegraph_files` 查看索引文件结构,`codegraph_status` 检查索引健康。 +- CodeGraph 是当前开发态代码图主工具;代码改动后运行 `codegraph sync .`,大范围重构、排除规则变化或索引异常时运行 `codegraph index . --force`。只有查找字面文本、日志字符串、注释原文,或已经明确目标文件时,才优先用 `rg` / 直接读文件。 + ## 常用命令 - 根目录热启动:`npm run desktop:hot` @@ -122,22 +127,6 @@ - 影响主页入口、Sidebar、tree shell、文档页首屏时,优先补或复用 `scripts/task*-smoke.js` 这类 smoke 脚本。 - 排查高 CPU / 高内存 / 卡顿时,先看是否存在首屏误走实验性 tree shell、compat fallback、重复请求、轮询或回链面板持续刷新,再看数据底座。 -## mnote-tester 调用 - -- 当前固定网页测试员 profile 为 `mnote-tester`,Hermes profile 路径:`/home/lix/.hermes/profiles/mnote-tester`。 -- 默认优先通过别名调用:`/home/lix/.local/bin/mnote-tester`;等价命令是 `hermes -p mnote-tester`。 -- 需要让后续 agent 调它做真实浏览器测试时,优先使用 one-shot: - - `mnote-tester --yolo -z "<测试任务>"` - - 或 `hermes -p mnote-tester --yolo -z "<测试任务>"` -- 交给 `mnote-tester` 的任务描述必须明确写出: - - 测试目标页面或 URL - - 是否要求真实登录 - - 是否要求新建 / 修改 / 删除页面或块 - - 测试数据前缀,例如 `TEST-HERMES-` - - 是否要求发现 bug 后直接写入 `bugs//process/` -- `mnote-tester` 允许在 `3000` 主页面做最小写入型测试,包括新建、重命名、编辑、移动、归档、恢复、删除页面,以及新建、修改、删除块/模块;但默认只能操作“本轮新建的测试数据”或用户明确指定的测试区域,不能动既有用户内容。 -- `mnote-tester` 的默认证据目录是 `/mnt/Data1T/mnote/tmp/hermes-tester//`;若发现 bug,应按 `/mnt/Data1T/mnote/bugs/README.md` 与本文件规则归类,并把缺陷记录写到对应 `bugs//process/`。 -- 若 `/auth`、快速登录、首屏路由或上游服务本身已阻塞,`mnote-tester` 应立即停止后续写入动作,先输出阻塞证据并按规则记录 blocker bug,不要伪造“新建/修改/删除已验证通过”。 ## Wolai-aline 对标流程 diff --git a/REASONIX.md b/REASONIX.md index 43477a8a..bccd8799 100644 --- a/REASONIX.md +++ b/REASONIX.md @@ -6,7 +6,6 @@ - **Axum 0.8** — HTTP/WebSocket server in `mnote-web` crate - **Leptos 0.8.14** — SSR island rendering in `mnote-web` - **Convex 1.x** — self-hosted document/real-time/storage backend (`infra/convex/docker-compose.yml`) -- **Electron** — desktop packaging (`desktop-electron/`) - **Playwright** — browser smoke tests in `scripts/task-*.js` ## Layout @@ -31,17 +30,24 @@ - **`npm run desktop:hot`** — hot-reload dev start (Rust gateway + optional FastAPI backend + optional Celery) - **`npm run dev:hot`** — same but with `cargo-watch` for Rust auto-recompile - **`npm run desktop`** — production start -- **`npm run desktop:local`** — local dev start (no build step) -- **`npm run desktop:electron`** — Electron dev runner -- **`npm run dist:win`** — build Windows installer (Next assets + electron-builder) - **`cargo test -p `** — test a specific Rust crate - **`cargo run -p mnote-web --bin mnote-web`** — run the web server binary directly +- **`codegraph sync .`** — refresh CodeGraph after normal code changes +- **`codegraph index . --force`** — rebuild CodeGraph after large refactors, ignore-rule changes, or stale index behavior + +## CodeGraph MCP + +- Reasonix has `codegraph=codegraph serve --mcp` configured in `/home/lix/.reasonix/config.json`. +- Prefer CodeGraph for structural code questions: symbol lookup, definitions, callers/callees, impact analysis, and focused cross-file context. +- Use `codegraph_search` for symbol names, `codegraph_callers` / `codegraph_callees` for call flow, `codegraph_impact` before risky edits, `codegraph_context` for focused task context, and `codegraph_files` / `codegraph_status` for index inspection. +- Use native search only for literal text, comments, log messages, or after a specific file is already identified. +- Keep the project index fresh in development: run `codegraph sync .` after code changes; use `codegraph index . --force` when the index looks stale or after broad restructuring. ## Conventions - **Smoke test pattern**: standalone Playwright scripts under `scripts/task-*.js` using a shared harness (`ensureAuthenticated`, `createTempDocument`, `cleanupDocuments`). All use `"use strict"` and `require("playwright")` (`scripts/task110-page-title-single-truth-smoke.js:1-4`). - **Rust workspace**: edition 2021, resolver "2", MIT license. All crates inherit workspace version (`rust/Cargo.toml:2-8`). -- **Convex schema**: `defineSchema` + `defineTable` via `convex/server` package; typed fields with `v.*` validators (`convex/schema.ts:1-3`). +- **Convex boundary**: root `convex/` is the active deploy source for ACP / Hermes runtime state (`schema.ts` + `aiSessions.ts`). Do not deploy from `recycle/wolai-frontend/convex`; old `documents` / `mediaAssets` / `users` / `workspaces` functions must be explicitly migrated to active code, moved to an auxiliary area, or retired. - **Bug tracking**: bugs organized by design domain directory (e.g. `bugs/04-tree-domain/process/`), moved to `done/` when fixed (`bugs/README.md`). - **Design docs**: organized by domain in `design/`, with `process/` (in-progress) and `done/` (completed) subdirs (`design/README.md`). diff --git a/browser-capability-20260512-current-page.png b/browser-capability-20260512-current-page.png deleted file mode 100644 index 4c41e085..00000000 Binary files a/browser-capability-20260512-current-page.png and /dev/null differ diff --git a/design/00-clean/2026-05-20-cleanup-summary.md b/design/00-clean/2026-05-20-cleanup-summary.md new file mode 100644 index 00000000..c7b69706 --- /dev/null +++ b/design/00-clean/2026-05-20-cleanup-summary.md @@ -0,0 +1,233 @@ +# 2026-05-20 项目清理总结 + +## 背景 + +本轮清理目标是为当前 MNote 主线实现收窄项目工作区,减少历史 AIChem / Wolai / Graphify / 构建缓存 / 本地沙箱残留对开发、索引和依赖判断的干扰。 + +清理过程中采用的核心规则: + +- `.gitignore` 不等于无用;被 ignore 的内容可能仍是外部辅助功能或本地运行缓存。 +- `recycle/` 才按“确认无当前作用或待回收隔离”处理。 +- 对不确定内容优先移动到 `/mnt/Data1T/mnote/recycle`,不直接物理删除。 +- `reference-code` 是参考代码,不进入 CodeGraph 主索引,但不代表可删除。 +- `.pnpm-store`、`.playwright-mcp` 等本地功能或缓存目录若曾被 Git 跟踪,处理重点是停止跟踪或替换本地缓存,不直接删除其功能。 + +## 已完成清理 + +### 1. Graphify 退役与 CodeGraph 接管 + +已卸载 / 隔离 Graphify 相关内容,并将历史 Graphify 目录移动到: + +- `recycle/20260520-graphify-uninstall/` +- `recycle/20260520-graph-cleanup/` + +当前选择 CodeGraph 作为主代码图谱工具。原因是 CodeGraph 更适合开发态持续更新:有 MCP 工具、AST 结构化查询、索引状态检查和 symbol/caller/callee/impact 查询能力。 + +已完成 CodeGraph 配置与验证: + +- Codex 已有 `codegraph_*` MCP 工具可用。 +- Reasonix 已补充 CodeGraph MCP 使用指南。 +- 项目 `AGENTS.md` / `REASONIX.md` 已加入 CodeGraph 使用规则。 +- 主索引继续排除 `reference-code`、`tmp`、`test-results`、`recycle`、`.playwright-mcp`、沙箱目录、生成缓存、spike 构建产物等。 + +最新验证: + +```bash +codegraph status +``` + +结果: + +- Files: `295` +- Nodes: `8,035` +- Edges: `24,035` +- DB Size: `50.86 MB` +- Backend: `native` +- 状态:`Index is up to date` + +### 2. Convex 目录规范 + +已修正 `scripts/run-convex-deploy.js`: + +- 不再 fallback 到 `recycle/wolai-frontend/convex` +- 只从根 `convex/` 部署 +- 若 active deploy source 缺失,应显式失败,而不是静默回退到 recycle + +已更新说明: + +- `infra/convex/README.md` +- `AGENTS.md` +- `REASONIX.md` + +当前约束: + +- 根 `convex/` 是 active functions deploy source。 +- `recycle/wolai-frontend/convex` 不允许作为当前 fallback。 +- 若 runtime 仍需要旧 functions,应迁回 active 区或明确放入外部辅助区;不能从 recycle 继续运行。 + +### 3. package 依赖与历史 Electron 配置清理 + +已从根 `package.json` 清理历史 Electron 相关配置: + +- 移除 Electron / electron-builder 相关依赖与配置。 +- 移除 `desktop-electron` 历史引用。 +- 移除 `appId=org.aichem.mnote` 旧 AIChem 命名残留。 + +已更新: + +- `package.json` +- `package-lock.json` +- `pnpm-lock.yaml` + +当前根依赖验证: + +```bash +npm ls --depth=0 --json +``` + +当前只保留: + +- `@playwright/test` +- `convex` +- `playwright` + +另外,已清理 `node_modules/.electron-alyHT01H` 断开的 Electron 临时 symlink。 + +### 4. 本地 / 沙箱残留移动到 recycle + +以下目录已按“本地残留候选”移动到: + +- `recycle/20260520-local-sandbox-cleanup/` + +涉及内容包括: + +- `home/` +- `.sandbox-home` +- `.sandbox-tmp` +- `.convex-tmp` + +当前该回收目录体积约 `912K`。 + +### 5. 构建缓存与大目录移动到 recycle + +以下构建缓存 / 依赖缓存已移动到: + +- `recycle/20260520-build-cache-cleanup/` + +涉及内容包括: + +- `rust/spikes/leptos-tiptap-spike/target` +- 根 `rust/target` +- `test/agent-browser-lab/node_modules` +- `reference-code` 下若干 `target` / `node_modules` / `.next` 构建缓存 + +当前该回收目录体积约 `53G`。 + +说明: + +- 根 `rust/target` 历史上存在被 Git 跟踪的构建产物,已先做停止跟踪处理,再移动。 +- 这些内容是构建缓存或历史依赖缓存,不应进入 CodeGraph 主索引。 +- 因为已移入 `recycle`,当前还没有物理删除;后续若确认无需回滚,可再删除该回收目录释放空间。 + +### 6. `.pnpm-store` 清理与替换 + +原 `.pnpm-store` 约 `2.6G`,包含大量历史前端依赖缓存,例如 Next、BlockNote、Tiptap、Supabase、Radix、Cloudflare / Wrangler 等。当前根锁文件已经不再引用这些历史包。 + +处理流程: + +1. 先用干净 store 目录 `.pnpm-store-clean` 按当前 `pnpm-lock.yaml` 重新安装。 +2. 验证干净 store 可用。 +3. 将旧 `.pnpm-store` 移动到 `recycle/20260520-pnpm-store-legacy/.pnpm-store`。 +4. 将 `.pnpm-store-clean` 切回标准路径 `.pnpm-store`。 +5. 重新安装当前依赖,确保 `node_modules` 指向标准 `.pnpm-store`。 +6. 清理 `node_modules/.electron-alyHT01H` 断链。 + +当前验证: + +```bash +pnpm store status --store-dir .pnpm-store +``` + +结果: + +```text +Packages in the store are untouched +``` + +当前体积: + +- `.pnpm-store`: `308M` +- `node_modules`: `2.0M` +- `recycle/20260520-pnpm-store-legacy`: `2.6G` + +### 7. `.playwright-mcp` 和 `.pnpm-store` 的 Git 跟踪问题 + +已按规则处理“停止跟踪”方向,而不是删除本地功能。 + +当前工作区仍会显示大量 `.pnpm-store` / `.playwright-mcp` 的删除状态,这是因为这些历史缓存文件曾被 Git 跟踪,而本轮清理目标是让缓存退出版本控制焦点。 + +后续提交时应确认: + +- `.pnpm-store` 不应继续作为源码资产跟踪。 +- `.playwright-mcp` 不应继续作为源码资产跟踪。 +- 当前活跃 `.pnpm-store` 是本地依赖缓存,可重建。 +- 历史旧 store 已在 `recycle/20260520-pnpm-store-legacy/` 中保留,便于回滚。 + +## 旧命名和历史配置处理 + +本轮还处理了以下历史残留: + +- 根 `package.json` 中 `org.aichem.mnote` 旧 appId 已移除。 +- `desktop-electron` 历史配置已移除;当前桌面主路径是 Rust。 +- `scripts/cloudflared/config.yml.example` 中旧 `*.aichem.dpdns.org` 域名已作为旧域名残留识别并纳入清理检查范围。 + +## 当前保留但不进入主索引的内容 + +以下内容不应进入 CodeGraph 主索引,但不是“可删除”的含义: + +- `design/05-editor-mainline/reference-code/` +- `tmp/` +- `test-results/` +- `recycle/` +- `.playwright-mcp/` +- 构建缓存、生成缓存、spike 输出目录 + +这符合当前规则:主索引只聚焦当前功能实现,参考代码和本地辅助内容不干扰结构分析。 + +## 验证记录 + +已执行并通过的关键验证: + +```bash +codegraph status +pnpm store status --store-dir .pnpm-store +npm ls --depth=0 --json +find -L node_modules -maxdepth 2 -type l -printf 'BROKEN %p -> %l\n' +``` + +验证结论: + +- CodeGraph 主索引可用且 up to date。 +- 当前 `.pnpm-store` 可用,且体积已收缩到 `308M`。 +- 当前 `node_modules` 依赖树只包含当前根依赖。 +- `node_modules` 不再存在断开的 Electron symlink。 + +## 后续建议 + +### 可在确认后物理删除的回收目录 + +以下目录已经移入 `recycle`,当前不再作为 active 实现路径使用。若后续运行一段时间确认无回滚需求,可以物理删除以释放空间: + +- `recycle/20260520-build-cache-cleanup/`,约 `53G` +- `recycle/20260520-pnpm-store-legacy/`,约 `2.6G` +- `recycle/20260520-graphify-uninstall/`,约 `433M` +- `recycle/20260520-local-sandbox-cleanup/`,约 `912K` +- `recycle/20260520-graph-cleanup/`,约 `40K` + +### 仍需持续遵守的规则 + +- 不把 `.gitignore` 内容直接等同于废弃内容。 +- 不从 `recycle` 中回退引用运行时代码。 +- 新增 CodeGraph 主索引排除项时,应优先判断其是否属于参考代码、生成缓存、测试输出、回收区或本地辅助功能。 +- 后续依赖变化后应重新运行 `pnpm install`,CodeGraph 在代码变化后需要重建或等待 watcher 更新;若排除规则变化,需显式重新初始化 / 重建索引。 + diff --git a/design/01-tree-first-graph-kernel/process/1-4-next-phase-execution-roadmap-v1.md b/design/01-tree-first-graph-kernel/process/1-4-next-phase-execution-roadmap-v1.md new file mode 100644 index 00000000..ea5ab96f --- /dev/null +++ b/design/01-tree-first-graph-kernel/process/1-4-next-phase-execution-roadmap-v1.md @@ -0,0 +1,204 @@ +# 1-4 [process] 下一阶段执行路线图 v1 + +> 创建时间:2026-05-19 +> +> 当前状态:`PROCESS` +> +> 上位依据: +> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-3-current-priority-execution-checklist-v1.md` +> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-14-zed-lapce-vscode-reference-adoption-matrix-v1.md` +> - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md` +> - `/mnt/Data1T/mnote/ARCHITECTURE.md` +> +> 目标:把当前已经推进到混合态的主线,收口成一条更清晰的下一阶段路线,避免“功能继续堆、底座继续散”的问题。 + +--- + +## 1. 结论 + +下一阶段不是全量重写,也不是继续优先补 UI 功能,而是: + +> **以架构收口为主,带着产品化闭环往前推。** + +这里的“架构收口”指的是把当前已经存在但还没完全统一的底座收束到少数几条正式主链上: + +- `WorkspacePath / ProjectPath / ObjectIdentity` +- `BufferStore / fileVersion / dirty / external change` +- `Page Aggregate` +- `tree.*` / `tree.resource.*` +- `live cache` 的 snapshot / delta / resync 主链 +- agent 文件编辑权限与 allowed roots + +这里的“产品化闭环”指的是在底座稳定后,继续做: + +- 分享与同步 +- simplemindmap / office 资源模型 +- 旧 Convex 数据迁移 + +--- + +## 2. 为什么不是先做功能补全 + +`1-3` 里已经完成了比较多的可见功能: + +- 管理员目录授权控制面。 +- VSCode-like 冲突处理 UI。 +- agent changed files / diff 审计。 +- 本地搜索、反链、资源引用索引。 + +`5-14` 也说明了一个更底层的事实: + +- Zed 更适合作为主架构参考。 +- Lapce 更适合作为轻量实现参考。 +- SideX 更适合作为 VS Code 行为迁移到 Rust 后端的桥接参考。 +- 官方 VS Code 只适合作为行为规格参考。 + +这意味着当前真正缺的不是“再加一个功能”,而是: + +> **把这些功能压在同一套身份、版本、命令、搜索和 watcher 主链上。** + +如果继续优先做功能补全,结果通常是: + +- 新功能依赖旧兼容面。 +- 新能力继续长在不同命令面上。 +- UI 看起来越来越完整,但底层真相越来越散。 + +--- + +## 3. 下一阶段三段式推进 + +### 3.1 阶段 A:架构收口 + +这是当前最优先的阶段。 + +目标: + +> **把 workspace identity、buffer/version、page aggregate、tree command、live cache 收成统一主链。** + +应优先推进的事: + +- 统一 `WorkspacePath` / `ProjectPath` / `ObjectIdentity`。 +- 统一 `BufferStore` / dirty / save / external change 模型。 +- 继续瘦身 `documents.*`,新增只走 `tree.*` / `tree.resource.*`。 +- 让 `Page Aggregate` 继续成为页面主投影唯一入口。 +- 统一 Sidebar / filetree / page subtree / WS / SSE 到同一 live cache。 +- 让 agent 文件编辑权限、allowed roots、symlink escape、文件版本仲裁保持一致。 + +不该做的事: + +- 不重建通用 IDE 壳。 +- 不优先做 terminal / debugger / extension host。 +- 不继续扩展旧兼容命令面。 + +### 3.2 阶段 B:资源与共享产品化 + +架构收口后,第二阶段做产品化闭环。 + +目标: + +> **把 simplemindmap / office / 分享 / AI 访问权限纳入同一套资源与权限模型。** + +应优先推进的事: + +- `share grant` 与 `local access grant` 的关系。 +- `shared AI session` 的权限边界。 +- `tree.resource.*` 的资源创建 / 重命名 / 移动 / 删除 / 恢复。 +- simplemindmap / office 的 ObjectIdentity 和 Resource Tree 映射。 + +### 3.3 阶段 C:迁移与运营闭环 + +最后做迁移和收尾。 + +目标: + +> **把旧 Convex 资产迁移成本变成可视、可回滚、可验证的产品流程。** + +应优先推进的事: + +- Convex workspace 导出到本地 root。 +- dry run。 +- 备份。 +- 冲突报告。 +- 回滚。 + +--- + +## 4. 和 `1-3` / `5-14` 的关系 + +### 4.1 `1-3` 负责告诉我们“当前还剩什么没收口” + +`1-3` 已经把主线拆得很清楚: + +- P0 到 P3 基本是已完成或接近完成的产品能力。 +- P4 到 P7 仍然是后续主线。 +- 其中 P7 明确指出:Page Aggregate / tree command / realtime 兼容链还要继续瘦身。 + +### 4.2 `5-14` 负责告诉我们“底座应该参考什么” + +`5-14` 的作用不是多拉一个功能列表,而是告诉我们: + +- workspace / project / buffer / search / agent 权限,主参考应是 Zed。 +- RPC / proxy / config / keymap 的轻量 Rust 组织方式,可以参考 Lapce 和 SideX。 +- VS Code 只用于行为规格校准。 + +### 4.3 合起来的结论 + +两份文档合起来,结论就是: + +> **下一阶段的主线是“架构收口 + 产品化闭环”,不是“全量功能扩展”也不是“推倒重写”。** + +--- + +## 5. 明确优先级 + +### P0 + +- `WorkspacePath / ProjectPath / ObjectIdentity` +- `BufferStore / fileVersion / dirty / conflict` +- `Page Aggregate` +- `tree.*` / `tree.resource.*` +- `live cache` 主链 +- agent 文件编辑权限 + +### P1 + +- 本地搜索与快速打开继续巩固 +- File Tree / Page Tree / Resource Tree 命令上下文统一 +- command context / context key 统一 + +### P2 + +- share / sync +- simplemindmap / office 资源产品化 +- 本地迁移与导出 + +### P3 + +- terminal / PTY 边界 +- MultiBuffer / excerpts +- review diff / streaming apply +- 插件运行时 + +--- + +## 6. 当前不做什么 + +- 不把 MNote 拉成通用 IDE。 +- 不先做 extension host / debugger / terminal 优先级升级。 +- 不让 `documents.*` 继续长成新主命令面。 +- 不把 Tauri desktop shell 当成当前主壳方向。 +- 不把 UI 功能增长误当成主线前进。 + +--- + +## 7. 下一步建议 + +接下来最合理的动作是: + +1. 先按 `1-4` 这条路线图推进架构收口。 +2. 在 `1-3` 里把 `P7` 拆成明确的收口子任务。 +3. 等底座稳定后,再把 `P4-P6` 推成产品化闭环。 + +一句话结论: + +> **先收口主链,再做产品化。** diff --git a/design/01-tree-first-graph-kernel/process/1-5-next-phase-sequential-execution-checklist-v1.md b/design/01-tree-first-graph-kernel/process/1-5-next-phase-sequential-execution-checklist-v1.md new file mode 100644 index 00000000..0f56684c --- /dev/null +++ b/design/01-tree-first-graph-kernel/process/1-5-next-phase-sequential-execution-checklist-v1.md @@ -0,0 +1,687 @@ +# 1-5 [process] 下一阶段顺序执行 checklist v1 + +> 创建时间:2026-05-19 +> +> 当前状态:`PROCESS` +> +> 上位依据: +> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-4-next-phase-execution-roadmap-v1.md` +> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-3-current-priority-execution-checklist-v1.md` +> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-14-zed-lapce-vscode-reference-adoption-matrix-v1.md` +> +> 目标:给出一份可以顺序执行、每一步都有完成验收、且标明参考源码位置的下一阶段执行清单。 + +--- + +## 0. 执行原则 + +- [ ] 每个阶段必须先补或确认测试,再改实现。 +- [ ] 每个阶段只能收口当前主链,不新增通用 IDE 产品面。 +- [ ] 新增长期命令必须优先走 `tree.*` / `tree.resource.*` / `page.*`,不能继续扩 `documents.*`。 +- [ ] 涉及本地文件写入时,必须携带 workspace identity、allowed roots、expected file version。 +- [ ] 涉及 UI / live stream 时,必须补 browser smoke 或复用既有 smoke。 +- [ ] 每完成一个阶段,必须把验证命令和 smoke 证据写回本文件或 `1-3`。 + +--- + +## 1. Phase A0:建立执行基线 + +目标: + +> **确认当前 P0-P3 既有能力仍然稳定,避免在架构收口前带着红灯推进。** + +### 1.1 基线验证 + +- [ ] 运行 Rust 格式检查。 + - 命令:`cargo fmt --check --all --manifest-path rust/Cargo.toml` + - 完成验收:命令通过。 + +- [ ] 运行 local-first 权限与 workspace 基线测试。 + - 命令:`cargo test -p mnote-web local_folder -- --nocapture` + - 命令:`cargo test -p mnote-web local_access_policy -- --nocapture` + - 命令:`cargo test -p mnote-web hermes_client_local_acp -- --nocapture` + - 完成验收:三组测试通过。 + +- [ ] 运行 Page Aggregate / tree command 基线测试。 + - 命令:`cargo test -p mnote-web page_aggregate -- --nocapture` + - 命令:`cargo test -p mnote-web tree_command -- --nocapture` + - 完成验收:两组测试通过。 + +- [ ] 运行 local-first guard。 + - 命令:`npm run check:local-first-convex-guard` + - 完成验收:命令通过,新增路径没有把 Convex 恢复成默认正文主存储。 + +### 1.2 基线 smoke + +- [ ] 冲突 UI smoke。 + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task451-local-markdown-conflict-resolution-ui-smoke.js` + - 完成验收:tiptap 与外部/agent 写入冲突仍能显示、合并、保存。 + +- [ ] 本地搜索索引 smoke。 + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task452-local-search-index-browser-smoke.js` + - 完成验收:新建、重命名、搜索、反链、标签仍稳定。 + +- [ ] agent changed files smoke。 + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task453-local-folder-page-ai-changed-files-smoke.js` + - 完成验收:AI 写入后 UI 能显示 `agent.changed_files`。 + +--- + +## 2. Phase A1:统一 WorkspacePath / ProjectPath / ObjectIdentity + +目标: + +> **用一个 Rust 侧稳定身份表达“哪个 workspace root 下的哪个对象 / 文件 / 资源”。** + +### 2.1 当前代码锚点 + +- MNote: + - `rust/crates/core-protocol/src/kernel.rs` + - `KernelObjectIdentity` + - `PageBodyWriteRequest` + - `rust/crates/core-protocol/src/lib.rs` + - `rust/crates/mnote-web/src/routes/local_folder_source.rs` + - `rust/crates/mnote-web/src/ssr/pages/layout.rs` + - `fileObjectIdentity` + - `readFileTreeObjectIdentity` + - `rust/crates/mnote-web/src/routes/tree.rs` + +### 2.2 参考代码位置 + +- Zed: + - `design/05-editor-mainline/reference-code/zed/crates/project/src/project.rs` + - `design/05-editor-mainline/reference-code/zed/crates/project/src/worktree_store.rs` + - `design/05-editor-mainline/reference-code/zed/crates/worktree/src/worktree.rs` +- SideX: + - `design/05-editor-mainline/reference-code/sidex-main/crates/sidex-workspace/src/path_util.rs` + - `design/05-editor-mainline/reference-code/sidex-main/crates/sidex-workspace/src/multi_root.rs` + - `design/05-editor-mainline/reference-code/sidex-main/crates/sidex-workspace/src/workspace.rs` + +### 2.3 顺序任务 + +- [ ] 盘点当前所有 path key / object key。 + - 搜索命令:`rg -n "ObjectIdentity|objectIdentity|rootUri|relativePath|fileVersion|documentId" rust/crates/mnote-web rust/crates/core-protocol -S` + - 完成验收:列出哪些入口仍只用 `documentId`、绝对路径或前端拼接 key。 + +- [ ] 在 `core-protocol` 中明确 `WorkspacePath` 或等价结构。 + - 建议字段:`workspace_id`、`root_uri`、`root_id`、`relative_path`、`source_kind`、`object_identity`、`resource_kind`。 + - 完成验收:结构能表达页面 `.md`、mindmap、office、附件、目录节点。 + +- [ ] 给 local folder file tree 输出稳定 identity。 + - 重点文件:`rust/crates/mnote-web/src/routes/local_folder_source.rs` + - 完成验收:File Tree row、Page Tree row、Resource row 均能携带统一 identity,不再由前端临时猜测。 + +- [ ] 前端 sidebar/filetree 消费统一 identity。 + - 重点文件:`rust/crates/mnote-web/src/ssr/pages/layout.rs` + - 完成验收:`fileObjectIdentity` / `readFileTreeObjectIdentity` 不再作为事实源,只作为兼容读取层。 + +### 2.4 验收 + +- [ ] 单测覆盖 identity 序列化与反序列化。 + - 建议命令:`cargo test -p core-protocol object_identity -- --nocapture` + +- [ ] local folder tree 测试覆盖页面 `.md` 和资源对象 identity。 + - 建议命令:`cargo test -p mnote-web local_folder -- --nocapture` + +- [ ] 浏览器 smoke 覆盖 filetree active reveal 和标题同步。 + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task438-filetree-title-md-active-reveal-smoke.js` + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task440-page-title-filetree-md-sync-smoke.js` + +完成标准: + +- [ ] 本地页面、资源、附件、目录节点都能映射到同一套 workspace identity。 +- [ ] UI 不再需要通过文件名或 DOM 属性临时反推对象身份。 + +--- + +## 3. Phase A2:建立 BufferStore / DocumentBuffer 最小模型 + +目标: + +> **让 tiptap autosave、AI 文件写入、外部编辑器修改围绕同一份打开态、dirty 状态和文件版本仲裁。** + +### 3.1 当前代码锚点 + +- `rust/crates/core-protocol/src/kernel.rs` + - `PageBodyWriteRequest.expected_file_version` +- `rust/crates/mnote-web/src/routes/documents.rs` + - `/api/page-body/write` + - `/api/documents/save` compat +- `rust/crates/mnote-web/src/routes/local_folder_source.rs` + - `write_local_markdown_page_body` + - `save_local_markdown_page` +- `rust/crates/mnote-web/src/routes/web_shell.rs` + - `conflictDetectionKey` + - `fileVersion` +- `rust/crates/mnote-web/src/routes/editor.rs` + +### 3.2 参考代码位置 + +- Zed: + - `design/05-editor-mainline/reference-code/zed/crates/project/src/buffer_store.rs` + - `design/05-editor-mainline/reference-code/zed/crates/editor/src/editor.rs` +- Lapce: + - `design/05-editor-mainline/reference-code/lapce/lapce-app/src/doc.rs` +- VS Code: + - `design/05-editor-mainline/reference-code/vscode/src/vs/platform/files` + - `design/05-editor-mainline/reference-code/vscode/src/vs/workbench/services/filesConfiguration` + +### 3.3 顺序任务 + +- [ ] 盘点所有文件写入口。 + - 搜索命令:`rg -n "documents.save|page.body.save|page-body/write|write_local_markdown|expectedFileVersion|conflictDetectionKey" rust/crates -S` + - 完成验收:列出 tiptap、Hermes tool、page save、documents compat、外部 watcher 入口。 + +- [ ] 定义 `DocumentBuffer` 最小字段。 + - 建议字段:`workspace_path`、`file_version`、`base_content_hash`、`current_content_hash`、`dirty_state`、`external_change_state`、`last_loaded_at`、`last_saved_at`。 + - 完成验收:字段能覆盖 clean、dirty、stale、external modified、deleted 五种状态。 + +- [ ] 让 `/api/page-body/write` 成为本地 `.md` 正文写入主入口。 + - 重点文件:`rust/crates/mnote-web/src/routes/documents.rs` + - 完成验收:本地 markdown 写入必须携带 `expectedFileVersion`;`documents/save` 只作为 compat adapter。 + +- [ ] 把冲突 UI 的 source 数据改为来自 buffer 状态。 + - 重点文件:`rust/crates/mnote-web/src/ssr/pages/layout.rs` + - 完成验收:冲突面板能显示 editor base version、disk version、source actor。 + +### 3.4 验收 + +- [ ] stale 写入被拒绝。 + - 命令:`cargo test -p mnote-web local_folder_documents_save_rejects_stale_expected_file_version -- --nocapture` + +- [ ] Page Body 写入测试通过。 + - 命令:`cargo test -p mnote-web page_aggregate -- --nocapture` + +- [ ] 冲突 UI smoke 通过。 + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task451-local-markdown-conflict-resolution-ui-smoke.js` + +完成标准: + +- [ ] tiptap、AI、外部修改共享同一版本仲裁模型。 +- [ ] 外部文件变更不能静默覆盖 dirty buffer。 +- [ ] `documents/save` 不再被新增路径当作长期主入口。 + +--- + +## 4. Phase A3:Page Aggregate 写读主链继续收口 + +目标: + +> **页面头部、正文、设置、树投影都通过 Page Aggregate 主链回流 UI。** + +### 4.1 当前代码锚点 + +- `rust/crates/core-protocol/src/page_aggregate.rs` +- `rust/crates/mnote-web/src/routes/documents.rs` +- `rust/crates/mnote-web/src/ssr/pages/document.rs` +- `rust/crates/mnote-web/src/ssr/pages/layout.rs` +- `rust/crates/bridge-runtime/src/lib.rs` +- `rust/crates/storage-convex-bridge/src/mapping.rs` + +### 4.2 参考代码位置 + +- Zed: + - `design/05-editor-mainline/reference-code/zed/crates/project/src/project.rs` + - `design/05-editor-mainline/reference-code/zed/crates/project/src/worktree_store.rs` +- VS Code: + - `design/05-editor-mainline/reference-code/vscode/src/vs/workbench/contrib/files` + +### 4.3 顺序任务 + +- [ ] 盘点仍直接拼 `meta + content` 或读取 `documents.content` 的 runtime 路径。 + - 搜索命令:`rg -n "documents.content|getContent|getMeta|meta \\+ content|page_aggregate|pageAggregate" rust/crates wolai-frontend src -S` + - 完成验收:列出仍存在的 runtime fallback,并标记 `compat only` 或待删除。 + +- [ ] 标题、正文、页面设置写入后只触发 Page Aggregate refresh。 + - 重点文件:`rust/crates/mnote-web/src/routes/documents.rs` + - 重点文件:`rust/crates/mnote-web/src/ssr/pages/layout.rs` + - 完成验收:写入后 UI 更新来源能追溯到 Page Aggregate,而不是局部补偿状态。 + +- [ ] 明确 `documents.*` 在 Page Aggregate 中的兼容边界。 + - 重点文件:`rust/crates/storage-convex-bridge/src/mapping.rs` + - 完成验收:新增语义不再加到 `documents.*`,旧命令只作为 adapter。 + +### 4.4 验收 + +- [ ] Page Aggregate Rust 测试通过。 + - 命令:`cargo test -p mnote-web page_aggregate -- --nocapture` + - 命令:`cargo test -p bridge-runtime page_aggregate -- --nocapture` + +- [ ] 页面标题、正文、设置 smoke 通过。 + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task110-page-title-single-truth-smoke.js` + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task-page-aggregate-body-sync-smoke.js` + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task-page-aggregate-options-sync-smoke.js` + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task-page-aggregate-refresh-persistence-smoke.js` + +完成标准: + +- [ ] 文档页不再新增 `meta + content` 拼装真相。 +- [ ] 标题、正文、页面设置刷新后仍一致。 +- [ ] Page Aggregate provenance 能清楚区分 local markdown、compat join、kernel projection。 + +--- + +## 5. Phase A4:tree command / tree.resource command 收口 + +目标: + +> **页面和资源操作统一走 `tree.*` / `tree.resource.*`,`documents.*` 只保留兼容。** + +### 5.1 当前代码锚点 + +- `rust/crates/mnote-web/src/routes/tree.rs` +- `rust/crates/bridge-runtime/src/lib.rs` +- `rust/crates/storage-convex-bridge/src/mapping.rs` +- `rust/crates/mnote-cli/src/lib.rs` +- `rust/crates/mnote-web/src/transport/convex.rs` + +### 5.2 参考代码位置 + +- Zed: + - `design/05-editor-mainline/reference-code/zed/crates/project_panel/src/project_panel.rs` +- VS Code: + - `design/05-editor-mainline/reference-code/vscode/src/vs/platform/commands` + - `design/05-editor-mainline/reference-code/vscode/src/vs/workbench/contrib/files` +- SideX: + - `design/05-editor-mainline/reference-code/sidex-main/src-tauri/src/commands/fs.rs` + - `design/05-editor-mainline/reference-code/sidex-main/crates/sidex-workspace/src/file_operations.rs` + +### 5.3 顺序任务 + +- [ ] 盘点仍使用 `documents.*` 的命令入口。 + - 搜索命令:`rg -n "\"documents\\.|documents\\." rust/crates infra src wolai-frontend -S` + - 完成验收:输出保留清单,逐项标记为 `compat adapter`、`test fixture`、`must cutover`。 + +- [ ] 将新增页面操作固定为 `tree.node.*`。 + - 完成验收:新建、重命名、移动、归档、恢复、删除不再新增 `documents.*` 调用。 + +- [ ] 将资源操作固定为 `tree.resource.*`。 + - 完成验收:mindmap、office、附件 rename/move/archive/restore/purge 都有正式命令名。 + +- [ ] mnote-cli 中历史 `documents.*` 命令降级为 compat。 + - 重点文件:`rust/crates/mnote-cli/src/lib.rs` + - 完成验收:CLI 新增命令使用 `tree.*` 或 `tree.resource.*`。 + +### 5.4 验收 + +- [ ] tree command 单测通过。 + - 命令:`cargo test -p mnote-web tree_command -- --nocapture` + - 命令:`cargo test -p bridge-runtime tree_command -- --nocapture` + +- [ ] 文件树操作 smoke 通过。 + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task179-tree-create-delete-no-reload-smoke.js` + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task446-tree-rename-dual-browser-live-smoke.js` + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task447-tree-move-order-dual-browser-live-smoke.js` + +完成标准: + +- [ ] 新增长期命令面不再出现 `documents.*`。 +- [ ] `storage-convex-bridge` 只把正式命令映射到 Convex 兼容 mutation。 +- [ ] 资源生命周期命令能统一产生 tree stream delta hint。 + +--- + +## 6. Phase A5:tree realtime live cache 收口 + +目标: + +> **Sidebar、File Tree、Page Tree、Page Subtree 都消费同一套 snapshot / delta / resync live cache。** + +### 6.1 当前代码锚点 + +- `rust/crates/mnote-web/src/routes/sse.rs` +- `rust/crates/mnote-web/src/routes/realtime_ws.rs` +- `rust/crates/mnote-web/src/ssr/pages/layout.rs` + - `tree:snapshot` + - `tree:delta` + - `tree:resync` + - `deltaNeedsProjectionRefresh` +- `rust/crates/bridge-runtime/src/lib.rs` + - `tree_stream_delta_hint` + - `materialize_tree_stream_delta` + +### 6.2 参考代码位置 + +- Zed: + - `design/05-editor-mainline/reference-code/zed/crates/worktree/src/worktree.rs` + - `design/05-editor-mainline/reference-code/zed/crates/project/src/worktree_store.rs` +- Lapce: + - `design/05-editor-mainline/reference-code/lapce/lapce-proxy/src/watcher.rs` +- SideX: + - `design/05-editor-mainline/reference-code/sidex-main/crates/sidex-workspace/src/watcher.rs` + +### 6.3 顺序任务 + +- [ ] 盘点所有 tree/page/filetree 刷新来源。 + - 搜索命令:`rg -n "tree:snapshot|tree:delta|tree:resync|EventSource|WebSocket|poll|refetch|preferred snapshot|pageSubtree" rust/crates/mnote-web/src -S` + - 完成验收:标出 WS 主链、SSE fallback、polling/refetch 补偿链。 + +- [ ] 明确 WS 是主链、SSE 是 fallback。 + - 重点文件:`rust/crates/mnote-web/src/ssr/pages/layout.rs` + - 完成验收:SSE 不和 WS 并行长期消费同一事件,只有 WS 不可用时 fallback。 + +- [ ] delta 能处理时直接 apply,不能处理时 resync。 + - 重点文件:`rust/crates/mnote-web/src/ssr/pages/layout.rs` + - 重点文件:`rust/crates/bridge-runtime/src/lib.rs` + - 完成验收:`deltaNeedsProjectionRefresh` 不再成为常态路径。 + +- [ ] 删除或降级不必要的 polling / refetch 补偿链。 + - 完成验收:页面/文件树操作不靠定时轮询才能稳定。 + +### 6.4 验收 + +- [ ] realtime route 单测通过。 + - 命令:`cargo test -p mnote-web tree_realtime -- --nocapture` + - 命令:`cargo test -p mnote-web local_folder -- --nocapture` + +- [ ] 双浏览器 live smoke 通过。 + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task446-tree-rename-dual-browser-live-smoke.js` + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task447-tree-move-order-dual-browser-live-smoke.js` + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task448-tree-resync-recovery-dual-browser-smoke.js` + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js` + +完成标准: + +- [ ] 双浏览器页面、文件、垃圾箱操作不刷新页面也能同步。 +- [ ] WS 主链可用时 SSE 只作为 fallback。 +- [ ] resync 是异常恢复路径,不是每次 delta 后的默认补偿。 + +--- + +## 7. Phase A6:agent 文件编辑权限与 audit 收口 + +目标: + +> **Hermes / Reasonix 只能在 allowed roots 内编辑文件,写前受版本约束,写后 watcher / audit / projection 能同步。** + +### 7.1 当前代码锚点 + +- `rust/crates/core-protocol/src/ai.rs` + - `AiAccessScope` + - `allowed_roots` +- `rust/crates/mnote-web/src/routes/hermes_client.rs` + - `acp_allowed_roots_env_for_payload` + - `local_agent_audit_collect_snapshot` + - `local_agent_audit_finalize_run` +- `rust/crates/mnote-web/src/routes/hermes_tools.rs` + - `local_agent_audit_record_write_rejected` + - `local_agent_audit_record_tool_write` +- `rust/crates/mnote-web/src/hermes_tools/doc.rs` +- `rust/crates/mnote-web/src/hermes_tools/page.rs` + +### 7.2 参考代码位置 + +- Zed: + - `design/05-editor-mainline/reference-code/zed/crates/agent/src/tools/edit_file_tool.rs` + - `design/05-editor-mainline/reference-code/zed/crates/agent/src/tools/read_file_tool.rs` + - `design/05-editor-mainline/reference-code/zed/crates/agent/src/tools/write_file_tool.rs` + - `design/05-editor-mainline/reference-code/zed/crates/agent/src/tools/edit_session.rs` + - `design/05-editor-mainline/reference-code/zed/crates/agent/src/tool_permissions.rs` +- SideX: + - `design/05-editor-mainline/reference-code/sidex-main/src-tauri/src/commands/validation.rs` + - `design/05-editor-mainline/reference-code/sidex-main/src-tauri/src/commands/fs.rs` + +### 7.3 顺序任务 + +- [ ] 盘点所有 agent 写入口。 + - 搜索命令:`rg -n "mnote.doc.markdown_edit|mnote.page.save|mnote.block|allowed_roots|AiAccessScope|write_rejected|agent_audit" rust/crates/mnote-web/src rust/crates/core-protocol/src -S` + - 完成验收:列出 Hermes tool、Reasonix/ACP、agent native file edit、MNote tool 写入入口。 + +- [ ] allowed roots 校验统一到一个 Rust helper。 + - 完成验收:所有写入口共用同一套 root canonicalization、symlink escape、read/write permission 校验。 + +- [ ] agent 写入前读取当前 file version。 + - 完成验收:MNote tool 写入必须携带 `expectedFileVersion`;agent native 写入完成后必须进入 audit + watcher 同步路径。 + +- [ ] audit event 统一 run id 和 changed files 结构。 + - 重点文件:`rust/crates/mnote-web/src/routes/hermes_client.rs` + - 完成验收:agent native 写入和 MNote tool 写入可归入同一个 run audit。 + +### 7.4 验收 + +- [ ] allowed roots env 测试通过。 + - 命令:`cargo test -p mnote-web acp_allowed_roots -- --nocapture` + +- [ ] audit 单测通过。 + - 命令:`cargo test -p mnote-web local_agent_audit_snapshot_detects_changed_files -- --nocapture` + - 命令:`cargo test -p mnote-web hermes_tools_markdown_edit_shared_read -- --nocapture` + - 命令:`cargo test -p mnote-web hermes_tools_page_save_local_folder_writes_markdown_file -- --nocapture` + +- [ ] changed files smoke 通过。 + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task453-local-folder-page-ai-changed-files-smoke.js` + +完成标准: + +- [ ] read grant 下写入被拒绝并记录 audit。 +- [ ] write grant 下写入后 UI 能显示 changed files。 +- [ ] agent 不能通过 symlink 或 `..` 逃出 allowed roots。 + +--- + +## 8. Phase A7:command context / context key 最小落地 + +目标: + +> **File Tree / Page Tree / Object Tab / Editor selection 贡献统一 command context,右键菜单和快捷键不再各自临时判断。** + +### 8.1 当前代码锚点 + +- `rust/crates/mnote-web/src/ssr/pages/layout.rs` +- `rust/crates/mnote-web/src/routes/tree.rs` +- `rust/crates/core-protocol/src/command.rs` + +### 8.2 参考代码位置 + +- VS Code: + - `design/05-editor-mainline/reference-code/vscode/src/vs/platform/contextkey` + - `design/05-editor-mainline/reference-code/vscode/src/vs/platform/commands` +- SideX: + - `design/05-editor-mainline/reference-code/sidex-main/crates/sidex-keymap/src/context.rs` + - `design/05-editor-mainline/reference-code/sidex-main/crates/sidex-keymap/src/resolver.rs` +- Lapce: + - `design/05-editor-mainline/reference-code/lapce/lapce-app/src/command.rs` + - `design/05-editor-mainline/reference-code/lapce/lapce-app/src/keypress/loader.rs` + +### 8.3 顺序任务 + +- [ ] 盘点当前菜单、快捷键、按钮 enablement 条件。 + - 搜索命令:`rg -n "disabled|aria-disabled|context|shortcut|keydown|menu|right-click|data-action" rust/crates/mnote-web/src/ssr/pages/layout.rs -S` + - 完成验收:列出至少 filetree、pagetree、editor、AI panel 的条件来源。 + +- [ ] 定义 MNote 最小 command context。 + - 建议 key:`workspace.sourceKind`、`tree.focusKind`、`tree.selectionCount`、`tree.selectionResourceKind`、`editor.dirty`、`editor.hasSelection`、`ai.canWrite`、`workspace.readonly`。 + - 完成验收:这些 key 能覆盖当前右键菜单、快捷键和主按钮 enablement。 + +- [ ] 先在 Rust 侧定义 context schema,再让前端消费。 + - 完成验收:前端不再在多个分支里重复判断同一个能力。 + +### 8.4 验收 + +- [ ] command context 单测覆盖 `when` 判断。 + - 建议命令:`cargo test -p core-protocol command_context -- --nocapture` + +- [ ] 文件树 DnD / readonly conflict smoke 通过。 + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task431-vscode-explorer-dnd-readonly-conflict-smoke.js` + +完成标准: + +- [ ] 同一命令在菜单、快捷键、按钮上使用同一 context 判断。 +- [ ] readonly / read grant / selected resource kind 的禁用态一致。 + +--- + +## 9. Phase B1:分享与同步闭环 + +目标: + +> **个人本地空间默认隔离,显式分享后才产生 shared workspace / shared AI session。** + +### 9.1 当前代码锚点 + +- `rust/crates/mnote-web/src/routes/local_access_policy.rs` +- `rust/crates/mnote-web/src/routes/hermes_client.rs` +- `rust/crates/core-protocol/src/ai.rs` +- `infra/convex/` + +### 9.2 参考代码位置 + +- VS Code: + - `design/05-editor-mainline/reference-code/vscode/src/vs/platform/files` +- Zed: + - `design/05-editor-mainline/reference-code/zed/crates/collab` + - 仅参考权限和协作边界,不采用完整协作产品。 + +### 9.3 顺序任务 + +- [ ] 定义 share grant 与 local access grant 的关系。 + - 完成验收:share grant 不自动扩大本机文件系统权限。 + +- [ ] shared AI session 只消费 share grant 允许的资源。 + - 完成验收:shared AI session 不能访问 owner 本机未授权 root。 + +- [ ] 同步 cache 目录设计。 + - 完成验收:cache 中保留来源、版本、actor、权限、冲突信息。 + +- [ ] 同步冲突复用 Phase A2 冲突 UI。 + - 完成验收:冲突报告包含 local、remote、base、actor。 + +### 9.4 验收 + +- [ ] 单测:share read 不允许写入。 +- [ ] 单测:shared AI session 不扩大 workspace root。 +- [ ] browser smoke:共享页面只读用户无法通过 AI 写入正文。 + +完成标准: + +- [ ] share / sync 不破坏 local-first 权限模型。 +- [ ] shared AI session 不能越权读写。 + +--- + +## 10. Phase B2:simplemindmap / office Resource Tree 产品化 + +目标: + +> **simplemindmap / office 是 Resource Tree 对象,Markdown 正文只保留链接或嵌入引用。** + +### 10.1 当前代码锚点 + +- `rust/crates/mnote-web/src/routes/mindmap_api.rs` +- `rust/crates/mnote-web/src/routes/mindmap_shell.rs` +- `rust/crates/mnote-web/src/ssr/pages/mindmap.rs` +- `rust/crates/mnote-web/src/routes/tree.rs` +- `rust/crates/bridge-runtime/src/lib.rs` +- `src/components/onlyoffice/` + +### 10.2 参考代码位置 + +- Zed: + - `design/05-editor-mainline/reference-code/zed/crates/project_panel/src/project_panel.rs` +- VS Code: + - `design/05-editor-mainline/reference-code/vscode/src/vs/workbench/contrib/files` +- SideX: + - `design/05-editor-mainline/reference-code/sidex-main/crates/sidex-workspace/src/file_tree.rs` + +### 10.3 顺序任务 + +- [ ] 盘点 simplemindmap object identity、保存路径、打开路径。 + - 搜索命令:`rg -n "mindmap|tree.resource.mindmap|resource:mindmap|mindmapId" rust/crates src -S` + - 完成验收:列出所有 mindmap 创建、打开、保存、删除路径。 + +- [ ] 盘点 office object identity、保存路径、打开路径。 + - 搜索命令:`rg -n "onlyoffice|office|tree.resource.office|resource:office|assetId" rust/crates src -S` + - 完成验收:列出所有 office 创建、打开、保存、删除路径。 + +- [ ] 统一资源生命周期到 `tree.resource.*`。 + - 完成验收:rename / move / archive / restore / purge 不再走 document command。 + +- [ ] Markdown 中只写资源引用。 + - 完成验收:mindmap / office 内容不塞回 `.md` 正文真相。 + +### 10.4 验收 + +- [ ] 单测:resource rename 不走 document command。 +- [ ] 单测:mindmap resource 删除 / 恢复不破坏 markdown 页面。 +- [ ] browser smoke:mindmap 长中文保存和 index.md 不互相污染。 + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task443-filetree-mindmap-click-active-row-smoke.js` + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task445-filetree-mindmap-switch-no-flicker-smoke.js` + +完成标准: + +- [ ] Page Tree 只做页面导航投影。 +- [ ] File Tree 能显示资源行。 +- [ ] Resource Tree 才是资源归属真相。 + +--- + +## 11. Phase C1:旧 Convex 数据迁移产品化 + +目标: + +> **把旧 Convex workspace 迁移成本变成可视、可回滚、可验证的产品流程。** + +### 11.1 当前代码锚点 + +- `rust/crates/storage-convex-bridge/src/mapping.rs` +- `rust/crates/mnote-web/src/transport/convex.rs` +- `rust/crates/mnote-web/src/routes/local_folder_source.rs` +- `scripts/task444-convex-workspace-export-local-fixture-smoke.js` + +### 11.2 参考代码位置 + +- VS Code: + - `design/05-editor-mainline/reference-code/vscode/src/vs/workbench/contrib/files/browser/fileImportExport.ts` +- SideX: + - `design/05-editor-mainline/reference-code/sidex-main/crates/sidex-workspace/src/file_operations.rs` + +### 11.3 顺序任务 + +- [ ] 设计 dry run 导出计划。 + - 完成验收:计划列出页面、资源、附件、目标路径、冲突项,不写文件。 + +- [ ] 导出前生成 backup / manifest。 + - 完成验收:manifest 能记录本次新增、覆盖、跳过、失败。 + +- [ ] 目标 root 同名冲突不覆盖。 + - 完成验收:生成冲突报告,用户明确选择前不写入。 + +- [ ] 回滚按 manifest 删除新增或恢复备份。 + - 完成验收:回滚后 root 回到导出前状态。 + +- [ ] 导出完成后重建本地索引。 + - 完成验收:迁移后页面可搜索,资源引用可查。 + +### 11.4 验收 + +- [ ] Convex fixture 导出 smoke 通过。 + - 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task444-convex-workspace-export-local-fixture-smoke.js` + +- [ ] 新增 smoke:同名文件冲突时不覆盖。 + +- [ ] 新增 smoke:回滚后 root 回到导出前状态。 + +完成标准: + +- [ ] 迁移支持 dry run。 +- [ ] 迁移支持备份、冲突报告、回滚。 +- [ ] 迁移后 local index 可用。 + +--- + +## 12. 每阶段统一收尾验收 + +每完成一个 Phase,执行: + +- [ ] `cargo fmt --check --all --manifest-path rust/Cargo.toml` +- [ ] `git diff --check -- ` +- [ ] 与阶段相关的 Rust 单测。 +- [ ] 与阶段相关的 browser smoke。 +- [ ] `npm run check:local-first-convex-guard` +- [ ] 把验证证据写回本 checklist。 + +阶段迁移到 `done/` 的标准: + +- [ ] checklist 项全部勾选。 +- [ ] 真实代码已落地。 +- [ ] 单测与 smoke 有证据。 +- [ ] 不再依赖临时 compat 作为新增能力主路径。 diff --git a/design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md b/design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md new file mode 100644 index 00000000..28d04bcd --- /dev/null +++ b/design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md @@ -0,0 +1,247 @@ +# 1-6 [process] 下一阶段缺口补齐 checklist v1 + +> 创建时间:2026-05-19 +> +> 当前状态:`PARTIALLY_DONE`(Reasonix 已补 A2/A7 基础设施与 A2 保存/watcher 接入;A2 浏览器刷新可见读回、A0 browser smoke、A7 UI 接入、B2/C1 产品化仍未闭环) +> +> 上位依据: +> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-5-next-phase-sequential-execution-checklist-v1.md` +> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-4-next-phase-execution-roadmap-v1.md` +> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-14-zed-lapce-vscode-reference-adoption-matrix-v1.md` +> +> 目的:基于 Reasonix 执行汇总和本轮核对结果,列出仍不能标完成的缺口,形成可继续顺序执行的补充清单。 + +--- + +## 0. 当前核对结论 + +本轮已确认: + +- [x] `ObjectWorkspacePath` 已在 `core-protocol` 定义并导出。 +- [x] local folder projection 已输出 `resourceMeta.workspacePath`。 +- [x] 前端 `fileObjectIdentity` 已优先读取 `resourceMeta.workspacePath.objectIdentity`。 +- [x] `DocumentBuffer` / `DocBufferDirtyState` 已在 `core-protocol` 定义并有状态机测试。 +- [x] `mnote.mindmap.*` / `mnote.office.*` Hermes resource tools 已出现,且最小测试通过。 + +本轮 Reasonix 执行后新增: + +- [x] **Gap A0(部分)**: local-first guard 已通过 ✅(`npm run check:local-first-convex-guard` → `{ok: true, guard: "local-first-convex"}`);browser smoke 尚未复跑。 +- [x] **Gap A2(基础设施+运行时接入)**: `BufferStore` 已在 `rust/crates/mnote-web/src/document_buffer_store.rs` 实现,并加入 `AppState.buffer_store`。运行时接入完成:`write_local_markdown_page_body` → `mark_saved`、watcher → `mark_external_modified`、Hermes tools → 共享 helper。11 个单测全部通过(含 3 个新集成测试)。 +- [x] **Gap A2(运行时接入—完成)**: `write_local_markdown_page_body` 已接入 `BufferStore.mark_saved()`;`documents.rs` `page_body_write` 和 `save` 路由已传递 `state.buffer_store`;Hermes `doc_apply_block_ops` 和 `page_command` 已传递 `state.buffer_store`;watcher 已在 `spawn_local_folder_watcher` 内调用 `mark_external_modified`。3 个新集成测试覆盖运行时入口。 +- [ ] **Gap A2(浏览器可见闭环)**: Codex 浏览器复核显示 `POST /api/page-body/write` 200、磁盘 `.md` 与 local search index 均写入成功、`#__MNOTE_PAGE_AGGREGATE__` 包含保存文本;但刷新后编辑器 DOM/截图未显示该正文,仍需修复可见读回或编辑器 hydration。 +- [x] **Gap A7(基础设施)**: `CommandContext` + `when` evaluator 已在 `core-protocol/src/command.rs` 实现。7 个单测全部通过(含 3 个必补测试)。 +- [ ] **Gap A7(UI/命令接入)**: File Tree 右键菜单、快捷键、AI 写入能力尚未消费 `CommandContext`。 +- [x] **Gap B2(部分)**: `resource_rename_uses_resource_command_not_document_command` 测试通过;`local_agent_audit_snapshot_detects_resource_files` 通过;mindmap 生命周期测试通过。 +- [x] **Gap C1(部分)**: `scripts/task444-convex-workspace-export-local-fixture-smoke.js` 通过;`export-convex-workspace-to-local.js` 既有 `--dry-run`、`--manifest`、`--conflict-report`、`--rollback` 能力已确认,但本轮未新增对应 smoke。 +- [x] **core-protocol 全量测试**: 41 单元 + 6 editor bridge + 2 resource tree = 49 通过 ✅ +- [x] **mnote-web 相关测试**: BufferStore 8 通过 + resource_trash 6 通过 + audit 2 通过 = 16 通过 ✅ + +--- + +## 1. Gap A0:补齐基线验证证据 — 🔄 部分完成 + +### 1.1 必跑命令 + +- [x] 运行 local-first guard。 + - 命令:`npm run check:local-first-convex-guard` + - 结果:`{"ok": true, "checkedLines": 1, "guard": "local-first-convex"}` + - 验收:通过 ✅ + +- [ ] 冲突 UI smoke — `task451-local-markdown-conflict-resolution-ui-smoke.js` + - 编号已存在脚本目录,需在完整后端环境下运行(依赖 mnote-web + Convex) + - 验收:尚未复跑 browser smoke;单测级 `document_buffer_state_transitions` 不能替代 UI smoke。 + +- [ ] 本地搜索索引 smoke — `task452-local-search-index-browser-smoke.js` + - 编号已存在脚本目录 + - 验收:尚未复跑 browser smoke。 + +- [ ] agent changed files smoke — `task453-local-folder-page-ai-changed-files-smoke.js` + - 编号已存在脚本目录 + - 验收:尚未复跑 browser smoke;`local_agent_audit_snapshot_detects_changed_files` 和 `detects_resource_files` 只能覆盖 audit 单元路径。 + +### 1.2 收尾 + +- [x] guard 输出已记录(见上) +- [ ] browser smokes 需完整后端环境运行(mnote-web + Convex + Playwright) +- [ ] 失败归属:无失败 + +--- + +## 2. Gap A2:把 DocumentBuffer 接入运行时 BufferStore — 🔄 基础设施与运行时接入完成,浏览器可见读回未闭环 + +### 2.1 当前代码锚点(更新后) + +- `rust/crates/mnote-web/src/document_buffer_store.rs` — **新增**:`BufferStore` + `BufferKey` 实现 +- `rust/crates/mnote-web/src/app.rs` — `buffer_store: BufferStore` 已加入 `AppState` +- `rust/crates/mnote-web/src/lib.rs` — `pub mod document_buffer_store` 已注册 +- `rust/crates/core-protocol/src/kernel.rs` — `DocumentBuffer` / `DocBufferDirtyState` / `ObjectWorkspacePath`(已有未改) + +### 2.2 顺序任务完成情况 + +- [x] 新增运行时 `BufferStore` owner:`rust/crates/mnote-web/src/document_buffer_store.rs`,通过 `AppState.buffer_store` 持有 +- [x] 定义 buffer key:以 `ObjectWorkspacePath` 的 `workspace_id + source_kind + root_uri + relative_path + document_id` 作为稳定 `BufferKey` +- [x] 接入 tiptap 保存链:`write_local_markdown_page_body` / `documents.rs` `save` 路由已调用 `state.buffer_store.mark_saved()` +- [x] 接入外部文件变更 watcher:`spawn_local_folder_watcher` 已通过 `buffer_store.mark_external_modified()` 更新对应 buffer +- [x] 接入 Hermes/Reasonix 写入后状态更新:`hermes_tools/doc.rs` 和 `hermes_tools/page.rs` 已通过共享 helper `write_local_markdown_page_body` 自动消费 BufferStore +- [ ] conflict UI 数据源改为 buffer state:API 已就绪,未接入 +- [ ] 浏览器可见读回:保存后磁盘与 PageAggregate 已更新,但刷新后 tiptap/editor DOM 未显示保存正文 + +### 2.3 必补测试 + +- [x] `document_buffer_reuses_workspace_path_key` — 通过 ✅ +- [x] `document_buffer_marks_external_modified_for_clean_buffer` — 通过 ✅ +- [x] `document_buffer_marks_stale_for_dirty_buffer` — 通过 ✅ +- [x] `document_buffer_mark_saved_after_page_body_write` — 通过 ✅ +- [x] 额外测试:`document_buffer_different_paths_different_keys` ✅ +- [x] 额外测试:`document_buffer_init_buffer_sets_file_version` ✅ +- [x] 额外测试:`document_buffer_mark_dirty_and_saved_round_trip` ✅ +- [x] 额外测试:`document_buffer_reuses_across_multiple_get_or_create_calls` ✅ + +### 2.4 当前完成标准 + +- [x] `DocumentBuffer` 已有 mnote-web runtime `BufferStore` owner +- [x] `BufferKey` 可稳定区分不同文件 +- [x] 11 个单测覆盖了关键 path + state transitions + save round-trip + 运行时入口 +- [x] **已完成**:tiptap/保存路由/Hermes 写入链已通过 `write_local_markdown_page_body` 统一消费 `BufferStore` +- [x] **已完成**:watcher 已通过 `spawn_local_folder_watcher` 调用 `mark_external_modified` +- [ ] **未完成**:`documents/save` 的 conflict UI 数据源尚未改为 buffer state 查询 +- [ ] **未完成**:浏览器刷新后编辑器可见正文未读回;证据截图:`reasonix-a2-after-refresh-text-missing.png` + +### 2.5 下一步 + +- 修复保存后刷新读回:`#__MNOTE_PAGE_AGGREGATE__` 已包含正文,但 `mnote-leptos-tiptap-editor-root` 可见 DOM 为空,需要定位 editor hydration / blockDocument 消费路径。 +- conflict UI 数据源改为 buffer state:前端查询 `/api/documents/buffer/state` 或 SSE 推送 buffer 变更事件。 + +--- + +## 3. Gap A7:落地 command context / context key — 🔄 基础设施完成,UI/命令接入未完成 + +### 3.1 当前代码锚点(更新后) + +- `rust/crates/core-protocol/src/command.rs` — **已扩展**:新增 `CommandContext`、`WhenExprNode`、`parse_when_expr`、`evaluate_when`、`check_when`、`is_command_enabled` +- `rust/crates/core-protocol/src/lib.rs` — **已更新**:导出所有 command context 类型和函数 + +### 3.2 顺序任务完成情况 + +- [x] 定义最小 `CommandContext` 结构:包含 `workspace.sourceKind`、`workspace.readonly`、`tree.focusKind`、`tree.selectionCount`、`tree.selectionResourceKind`、`editor.dirty`、`editor.hasSelection`、`ai.canWrite` 共 8 个 key +- [x] 实现最小 `when` evaluator:支持 `key`、`!key`、`key == value`、`key != value`、`&&`、`||`、括号分组(完整的递归下降解析器 + AST evaluator) +- [ ] 把 File Tree 右键菜单接入 context:`CommandContext` 和 `is_command_enabled` 已提供基础设施,但尚未在 SSR layout 中使用 +- [ ] 把快捷键接入 context:基础设施已就绪,未接入 +- [ ] 把 AI 写入能力接入 context:`ai.canWrite` key 已定义,未在 hermes_tools 中消费 + +### 3.3 必补测试 + +- [x] `command_context_when_clause` — 通过 ✅(覆盖 key、!key、==、!=、&&、||、括号分组) +- [x] `command_context_disables_write_commands_for_readonly_workspace` — 通过 ✅ +- [x] `command_context_filters_resource_commands_by_kind` — 通过 ✅ +- [x] `command_context_default_is_allow` — 通过 ✅ +- [x] `command_context_number_comparison` — 通过 ✅ +- [x] `command_context_parse_error_returns_default` — 通过 ✅ +- [x] `command_context_ai_can_write_gates_ai_actions` — 通过 ✅ + +### 3.4 当前完成标准 + +- [x] 最小 `CommandContext` 定义 + `when` evaluator 可工作 +- [x] readonly / grant / resource kind 的禁用态可测试 +- [ ] **未完成**:当前 UI 中同一命令的多份 enablement 判断尚未统一 +- [ ] **未完成**:菜单、按钮、快捷键可用性尚未统一 + +### 3.5 下一步 + +在 `layout.rs` 的右键菜单和 `hermes_tools` 的写入守卫中引入 `is_command_enabled`。 + +--- + +## 4. Gap B2:Resource tools 到 Resource Tree 产品化 — 🔄 部分完成 + +### 4.1 当前代码锚点(已有未改) + +- `rust/crates/mnote-web/src/routes/resource_trash.rs` — 媒体 / mindmap / table 生命周期使用 `tree.resource.*` 命令 +- `rust/crates/mnote-web/src/routes/hermes_tools.rs` +- `rust/crates/mnote-web/src/hermes_tools/resource.rs` +- `rust/crates/mnote-web/src/hermes_tools/manifest.rs` + +### 4.2 顺序任务完成情况 + +- [x] 明确 resource tool 是辅助入口,不是资源真相 — 文档中已有此约定 +- [x] mindmap resource 生命周期走 `tree.resource.*` — 测试验证 ✅ +- [x] office resource 生命周期走 `tree.resource.*` — ✅(仅链路存在,需更多测试覆盖) +- [ ] Resource Tree projection 显示资源对象 — 部分完成(filetree 已显示资源行) +- [x] agent audit 包含 resource files — `local_agent_audit_snapshot_detects_resource_files` 通过 ✅ + +### 4.3 必补测试 + +- [x] `tree_resource_rename_does_not_emit_document_command` — 被现有测试覆盖: + - `resource_rename_uses_resource_command_not_document_command` ✅(验证 canonicalCommand 为 `tree.resource.rename` 而非 `tree.node.rename`) +- [ ] `mindmap_resource_trash_restore_keeps_markdown_body` — 部分覆盖: + - `mindmap_delete_restore_keeps_markdown_reference_out_of_lifecycle_command` ✅(验证 mindmap 生命周期不携带 documentId 从而不影响 markdown 页面) + - 尚未验证文档正文在 trash/restore 后是否完整 +- [x] `local_agent_audit_snapshot_detects_resource_files` — 通过 ✅ +- [ ] browser smokes:`task443`、`task445` 需 Playwright 后端环境 + +### 4.4 完成标准 + +- [x] Resource Tree 是资源归属真相的大部分覆盖 +- [ ] Markdown 正文只保留资源引用 — 已有原则,未额外验证 +- [x] AI resource tools 与 tree.resource 生命周期一致 — 验证通过 + +--- + +## 5. Gap C1:Convex 迁移产品化补证据 — 🔄 部分完成 + +### 5.1 当前代码锚点(已有未改) + +- `rust/crates/storage-convex-bridge/src/mapping.rs` +- `rust/crates/mnote-web/src/transport/convex.rs` +- `rust/crates/mnote-web/src/routes/local_folder_source.rs` +- `scripts/task444-convex-workspace-export-local-fixture-smoke.js` +- `scripts/export-convex-workspace-to-local.js` + +### 5.2 顺序任务完成情况 + +- [x] 定义 dry run response — `export-convex-workspace-to-local.js` 支持 `--dry-run` 标识 ✅ +- [x] 定义 migration manifest — 支持 `--manifest` 参数 ✅ +- [x] 实现同名冲突不覆盖 — 导出时不覆盖已有文件,输出冲突报告 ✅ +- [x] 实现 rollback — 支持 `--rollback` 参数 ✅ +- [x] 导出完成后重建 local index — task444 smoke 验证通过 ✅ + +### 5.3 必补测试 + +- [x] `task444-convex-workspace-export-local-fixture-smoke.js` — 通过 ✅ +- [ ] 新增 smoke:dry run 不写文件 — `--dry-run` 已支持,需额外 smoke 验证 +- [ ] 新增 smoke:同名冲突不覆盖 — 已有冲突报告逻辑,需 smoke 验证 +- [ ] 新增 smoke:rollback 恢复导出前状态 — 已支持,需 smoke 验证 + +--- + +## 6. 最终收尾 + +### 6.1 验证命令结果 + +- [x] `cargo test -p core-protocol` — 41+6+2=49 passed ✅ +- [x] `cargo test -p mnote-web document_buffer` — 8 passed ✅ +- [x] `cargo test -p mnote-web resource_trash` — 6 passed ✅ +- [x] `cargo test -p mnote-web local_agent_audit_snapshot` — 2 passed ✅ +- [x] `npm run check:local-first-convex-guard` — `{"ok": true}` ✅ +- [x] `node scripts/task444-convex-workspace-export-local-fixture-smoke.js` — `{"ok": true}` ✅ +- [x] `cargo fmt --check --all --manifest-path rust/Cargo.toml` — 通过 ✅ +- [x] `git diff --check -- ` — 通过 ✅ + +### 6.2 Browser smokes(需完整后端环境) + +- [ ] `task431-vscode-explorer-dnd-readonly-conflict-smoke.js` +- [ ] `task451-local-markdown-conflict-resolution-ui-smoke.js` +- [ ] `task452-local-search-index-browser-smoke.js` +- [ ] `task453-local-folder-page-ai-changed-files-smoke.js` +- [ ] `task443-filetree-mindmap-click-active-row-smoke.js` +- [ ] `task445-filetree-mindmap-switch-no-flicker-smoke.js` + +### 6.3 总完成率 + +| Gap | 状态 | 关键产出 | 剩余 | +|-----|------|----------|------| +| A0 | 🔄 部分 | guard 通过,smoke 脚本已存在 | browser smokes 未复跑 | +| A2 | 🔄 运行时接入完成,浏览器闭环未完成 | `BufferStore` 实现 + 11 tests;保存请求 200;磁盘和 PageAggregate 写入成功 | 刷新后 editor DOM 未显示正文;conflict UI 未接 buffer state | +| A7 | 🔄 基础设施完成 | `CommandContext` + when evaluator + 7 tests | UI/快捷键/菜单接入 | +| B2 | 🔄 部分 | 资源生命周期 + agent audit 测试通过 | mindmap body 测试、browser smokes | +| C1 | 🔄 部分 | export/rollback/dry-run 脚本 + smoke 通过 | 额外 smoke 验证 | diff --git a/design/01-tree-first-graph-kernel/process/1-7-local-workspace-root-page-bundle-checklist-v1.md b/design/01-tree-first-graph-kernel/process/1-7-local-workspace-root-page-bundle-checklist-v1.md new file mode 100644 index 00000000..3e39dcd9 --- /dev/null +++ b/design/01-tree-first-graph-kernel/process/1-7-local-workspace-root-page-bundle-checklist-v1.md @@ -0,0 +1,258 @@ +# 1-7 Local Workspace Root Page Bundle Checklist v1 + +## 背景 + +当前默认本地工作区初始化会创建 `pages/`、`assets/`、`mindmaps/`、`ai-sessions/` 等顶层目录。这更像早期产品内部布局,不像用户真实打开的 VSCode 文件夹,也不利于页面导出。 + +本轮收口方向: + +- 默认工作区只保留用户内容根目录和 `.mnote/` 系统目录。 +- 页面树允许把 `页面.md + 页面/` 解释成一个页面节点与其子页面容器。 +- 文件树必须真实展示文件系统,不合并、不隐藏同名 `.md` 和目录。 +- 新建页面使用 `新页面HHMMSS` 命名,避免重复创建同名 `新页面.md`。 +- 新建页面时同时创建 `新页面HHMMSS.md` 和 `新页面HHMMSS/`。 +- 附件、思维导图等页面资源默认进入同名目录下,而不是顶层 `assets/` / `mindmaps/`。 + +## 参考代码位置 + +- 当前默认工作区初始化:`rust/crates/mnote-web/src/routes/local_folder_source.rs` + - `create_default_local_workspace_for_actor_at_base` + - `ensure_default_workspace_directories` + - `ensure_default_workspace_home_page` +- 当前本地页面创建:`rust/crates/mnote-web/src/routes/local_folder_source.rs` + - `execute_local_tree_command` + - `create_local_markdown_page` +- 当前本地资源上传:`rust/crates/mnote-web/src/routes/local_folder_source.rs` + - `write_local_markdown_asset` +- 当前文件树真实扫描:`rust/crates/mnote-web/src/routes/local_folder_source.rs` + - `load_local_folder_file_tree_snapshot` + - `scan_directory` +- 当前页面树扫描:`rust/crates/mnote-web/src/routes/local_folder_source.rs` + - `load_local_folder_page_tree_snapshot` + - `scan_markdown_page_tree` +- 当前根入口默认初始化测试:`rust/crates/mnote-web/src/routes/gateway.rs` + - `root_entry_initializes_default_local_workspace_page` +- 当前上传 smoke:`scripts/task443-local-markdown-asset-upload-smoke.js` + +## 目标目录契约 + +### 默认初始化 + +默认工作区初始化后应该是: + +```text +my-space/ + .mnote/ + workspace.json + trash/ + 初始化的新页面.md +``` + +不再默认创建: + +```text +pages/ +assets/ +mindmaps/ +ai-sessions/ +``` + +`ai-sessions` 后续如果要落本地,应放入 `.mnote/ai-sessions/`,本轮只禁止默认创建顶层 `ai-sessions/`。 + +### 新建页面 + +在父目录下新建页面时,默认标题若为 `新页面`,真实落盘应为: + +```text +新页面HHMMSS.md +新页面HHMMSS/ +``` + +例如: + +```text +新页面061240.md +新页面061240/ +``` + +要求: + +- `HHMMSS` 使用本地时间或现有时间工具生成,精确到秒。 +- 即使有时间戳,也必须做磁盘冲突检查;同秒冲突时使用 `-2`、`-3` 追加。 +- `documentId` 仍使用路径型 `local-md:`。 +- `.md` 正文标题应使用最终安全标题,例如 `# 新页面061240`。 + +### 页面树和文件树边界 + +- 页面树:允许把 `X.md` 与同目录 `X/` 解释为一个页面 `X`,并把 `X/` 内 Markdown 作为页面 `X` 的子页面。 +- 文件树:必须真实展示 `X.md` 和 `X/` 两个磁盘条目。不得把二者合并为一个节点。 + +### 页面资源 + +对 `X.md` 上传附件或图片时,默认写入: + +```text +X/assets/ +``` + +对 mindmap,后续默认写入: + +```text +X/mindmaps/ +``` + +本轮先完成 `write_local_markdown_asset` 的附件/图片路径迁移。正文链接相对 `X.md` 应为: + +```markdown +![图](X/assets/photo.png) +[spec.pdf](X/assets/spec.pdf) +``` + +## 顺序执行 Checklist + +### A0 基线保护 + +- [x] 记录当前 `git status --short`,不要清理与本任务无关的脏改动。 +- [x] 运行当前相关定向测试,确认改前基线: + - `cargo test --manifest-path rust/Cargo.toml -p mnote-web create_default_local_workspace_uses_managed_data_root -- --nocapture` + - `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown_asset_upload_copies_next_to_markdown_with_relative_path -- --nocapture` + +验收: + +- [x] 基线通过。两项测试均 ok。 + +### A1 默认工作区目录最小化 + +- [x] 修改 `ensure_default_workspace_directories`:默认只创建 `.mnote/trash`。 +- [x] 修改 `ensure_default_workspace_home_page`:初始化页改为 `root/初始化的新页面.md`,不再放入 `pages/`。 +- [x] 更新 `create_default_local_workspace_uses_managed_data_root`: + - 断言 `初始化的新页面.md` 位于 root 下。 + - 断言 `pages/`、`assets/`、`mindmaps/`、顶层 `ai-sessions/` 不存在。 + - 保留 `.mnote/workspace.json` 与 `.mnote/trash` 断言。 +- [x] 更新 `root_entry_initializes_default_local_workspace_page`: + - 断言 root 下存在 `初始化的新页面.md`。 + - 不再断言 `pages/初始化的新页面.md`。 + +验收: + +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web create_default_local_workspace_uses_managed_data_root -- --nocapture` — ok +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry_initializes_default_local_workspace_page -- --nocapture` — ok + +### A2 新建页面创建 `.md + 同名目录` + +- [x] 新增 helper `determine_page_stem`: + - 输入默认标题 `新页面` 输出 `新页面HHMMSS`(本地时间 HHMMSS,失败时回退 UTC)。 + - 对非默认标题,仍使用用户标题的安全文件名。 + - 对冲突同时检查 `.md` 与 `/`,追加 `-2`、`-3`。 +- [x] 修改 `create_local_markdown_page`: + - 创建 `.md`。 + - 同步创建 `/`。 + - 返回的 `relativePath` 仍是 `.md`。 + - 返回的 `documentId` 仍是 `local-md:`。 +- [x] 增加单测 `local_tree_command_create_page_creates_timestamped_markdown_and_sibling_folder`: + - 执行 `execute_local_tree_command(..., "create", ..., Some("新页面"))`。 + - 断言产生 `新页面\d{6}.md` 和同名目录。 + - 断言返回 `documentId` 指向 `.md`。 + +验收: + +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_tree_command_create_page_creates_timestamped_markdown_and_sibling_folder -- --nocapture` — ok + +### A3 页面树使用同名目录作为页面子容器 + +- [x] 修改 `scan_markdown_page_tree`: + - 如果同目录存在 `X.md` 和 `X/`,页面树只显示 `X.md` 对应的页面节点。 + - `X/` 内的 Markdown 节点挂在 `X.md` 页面节点下。 + - 没有同名 `.md` 的普通目录,如果包含 Markdown,继续作为 page group 显示。 +- [x] 增加单测: + - 构造 `Root.md`、`Root/Child.md`。 + - 页面树断言 `Child.md` 的 `parent_node_id` 是 `Root.md` 的 `documentId`。 + - 页面树断言没有单独的 `local:page-group:Root` 行。 + +验收: + +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_page_tree_uses_sibling_directory_as_page_children_container -- --nocapture` — ok + +### A4 文件树保持真实 + +- [x] 确认 `scan_directory` 不做任何同名 `.md + folder` 合并。 +- [x] 增加单测: + - 构造 `Root.md`、`Root/Child.md`。 + - 文件树断言同时存在 `Root.md` markdown 行和 `Root` folder 行。 + +验收: + +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_file_tree_keeps_markdown_and_sibling_folder_as_separate_rows -- --nocapture` — ok + +### A5 附件上传进入页面同名目录 + +- [x] 修改 `write_local_markdown_asset`: + - `README.md` 的附件目录从 `README.assets/` 改为 `README/assets/`。 + - 返回的 `sourcePath` / `file_url` 相对 Markdown 文件目录,例如 `README/assets/photo.png`。 + - 保留重复文件名 `photo-1.png` 逻辑。 +- [x] 更新单测 `local_markdown_asset_upload_copies_next_to_markdown_with_relative_path`。 +- [x] 更新 `scripts/task443-local-markdown-asset-upload-smoke.js` 中对 `README.assets/...` 的断言为 `README/assets/...`。 +- [x] 只改本地 local_folder 上传路径,不改 cloud / Convex media 路径。 + +验收: + +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown_asset_upload_copies_next_to_markdown_with_relative_path -- --nocapture` — ok +- [x] `node scripts/task443-local-markdown-asset-upload-smoke.js` — ok,证据:`tmp/task443-local-markdown-asset-upload-smoke/result.json` + +### A6 重命名 bundle 规则最小收口 + +- [x] 检查 `rename_local_entry`: + - 当重命名 `X.md` 且同目录存在 `X/` 时,同步重命名目录为新标题目录。 + - 如果目标目录已存在,使用与文件一致的冲突策略或返回可解释错误,禁止覆盖。 +- [x] 增加单测: + - `Old.md + Old/Child.md` 重命名为 `New`。 + - 断言 `New.md` 和 `New/Child.md` 存在。 + - 页面树中 `Child.md` 仍挂在 `New.md` 下。 + +验收: + +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_rename_markdown_page_renames_sibling_page_folder -- --nocapture` — ok + +### A7 文档同步 + +- [x] 更新本 checklist 状态。 +- [x] 本 checklist 已明确覆盖旧的默认顶层 `pages/assets/mindmaps/ai-sessions` 初始化结构;未大范围改旧 done 文档。 + +验收: + +- [x] checklist 中已记录实际完成情况。 + +### A8 最终验证 + +- [x] `cargo fmt --check --all --manifest-path rust/Cargo.toml` — ok +- [x] `cargo check --manifest-path rust/Cargo.toml -p mnote-web` — ok +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web create_default_local_workspace_uses_managed_data_root -- --nocapture` — ok +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_tree_command_create_page_creates_timestamped_markdown_and_sibling_folder -- --nocapture` — ok +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_page_tree_uses_sibling_directory_as_page_children_container -- --nocapture` — ok +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_file_tree_keeps_markdown_and_sibling_folder_as_separate_rows -- --nocapture` — ok +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown_asset_upload_copies_next_to_markdown_with_relative_path -- --nocapture` — ok +- [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_rename_markdown_page_renames_sibling_page_folder -- --nocapture` — ok +- [x] `node scripts/task443-local-markdown-asset-upload-smoke.js` — ok + +浏览器验收由 Codex 复核执行: + +- [x] 使用临时 `MNOTE_LOCAL_WORKSPACE_BASE_DIR=/mnt/Data1T/mnote/tmp/page-bundle-data` 启动 `mnote-web`。 +- [x] 登录/注入测试 actor 后打开首页。 +- [x] 截图保存到 `tmp/`: + - `tmp/page-bundle-home-3003.png` + - `tmp/page-bundle-after-create-3003.png` + - `tmp/page-bundle-filetree-3003.png` +- [x] 检查磁盘: + - root 下有 `初始化的新页面.md`。 + - root 下没有默认 `pages/`、`assets/`、`mindmaps/`、顶层 `ai-sessions/`。 + - 新建页面后有 `新页面HHMMSS.md` 和 `新页面HHMMSS/`。 + - 文件树能看到 `.md` 与同名目录两个真实条目。 + - 页面树把同名目录内子页面挂到 `.md` 页面下。 + +## 非目标 + +- 不迁移用户历史数据。 +- 不删除正式 `/mnt/Data1T/Mnote_data/users` 下旧测试目录。 +- 不实现完整 mindmap 新路径落盘;本轮只定义默认目标并完成附件/图片上传路径迁移。 +- 不把文件树改成页面语义树。 +- 不重新引入 `mnote_id` frontmatter。 diff --git a/design/05-editor-mainline/process/5-14-zed-lapce-vscode-reference-adoption-matrix-v1.md b/design/05-editor-mainline/process/5-14-zed-lapce-vscode-reference-adoption-matrix-v1.md new file mode 100644 index 00000000..fe2601cd --- /dev/null +++ b/design/05-editor-mainline/process/5-14-zed-lapce-vscode-reference-adoption-matrix-v1.md @@ -0,0 +1,547 @@ +# 5-14 [process] Zed / Lapce / VS Code 参考采用矩阵 v1 + +> 更新时间:2026-05-19 +> +> 关联背景: +> - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md` +> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md` +> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md` +> - `/mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md` +> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md` +> +> 本地参考源码: +> - VS Code:`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/vscode` +> - Zed:`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/zed` +> - Lapce:`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/lapce` +> - SideX:`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/sidex-main` + +## 1. 文档目的 + +这份文档回答一个工程路线问题: + +> **MNote 当前 Rust 主线应继续从 VS Code 源码里参考什么,又应该从哪些 Rust 编辑器项目里借鉴现成结构,避免把 VS Code TypeScript / Electron 代码大量转译到 Rust。** + +当前结论是: + +> **Zed 作为主架构参考,Lapce 作为辅助实现参考,SideX 作为 VS Code 行为迁移到 Rust 后端的桥接参考,VS Code 官方源码作为行为规格参考。** + +这不是说 MNote 要改造成通用 IDE,也不是要复制 Zed / Lapce 的桌面产品形态。MNote 的主线仍然是: + +- `tree-first graph kernel` +- local-first Markdown workspace +- Rust `mnote-web` Web shell +- `Page Aggregate` / `File Tree` / `Resource Tree` 投影 +- Hermes / Reasonix agent 在授权目录内进行文件编辑 + +Zed、Lapce、SideX、VS Code 只用于补齐“编辑器工作区基础设施”的成熟参考,尤其是文件树、打开态 buffer、dirty/save、搜索、外部文件变更、agent 文件编辑权限这些层。 + +--- + +## 2. 总体判断 + +### 2.1 不建议把 VS Code 源码作为转译目标 + +VS Code 官方源码仍然是 TypeScript / Electron 架构。它对 MNote 有很强的产品行为参考价值,但不适合作为 Rust 代码转译目标。 + +主要风险: + +- VS Code 的 workbench、service、contribution、lifecycle 与 Electron / DOM / Node 绑定很深。 +- 大量接口依赖 TypeScript DI、事件模型、extension host 和 workbench contribution 体系,转到 Rust 后会变成重建一套框架。 +- 文件服务、保存冲突、watcher、context key 等模块很有价值,但它们的价值在行为合同和边界划分,不在逐行代码。 +- MNote 当前不是通用 IDE,而是 local-first Markdown workspace + agent 编辑器,直接转译会把产品边界拉宽。 + +因此,VS Code 的定位应固定为: + +> **行为规格参考,不作为 Rust 实现蓝本。** + +### 2.2 Zed 是当前最接近 MNote 长期主线的 Rust 参考 + +Zed 的价值不在 UI 壳,而在这条 Rust 原生编辑器链路: + +> `Worktree / Snapshot -> WorktreeStore -> Project / ProjectPath -> BufferStore / MultiBuffer / Editor -> Search / FileFinder -> AgentTool / ToolPermission` + +这与 MNote 的长期方向高度重合: + +- 多 workspace root / local folder。 +- 稳定的 `{root + relative path}` 文件身份。 +- 文件树扫描、ignore、watch 增量更新。 +- 打开态 buffer、dirty/save、路径到 buffer 的索引。 +- 全文搜索、快速打开。 +- agent 文件读写、patch、权限与 allowed roots。 + +Zed 应作为: + +> **workspace / project / buffer / search / agent 文件编辑权限的主参考。** + +### 2.3 Lapce 是轻量实现与边界拆分的辅助参考 + +Lapce 的价值不在“产品完整度超过 Zed”,而在它更轻、更容易抽取工程模式: + +> `UI -> typed RPC -> proxy -> watcher / search / plugin / terminal -> doc / buffer delta / save` + +这对 MNote 有两类价值: + +- typed command / request / response 如何拆到 proxy / backend 执行层。 +- 文档 `rev / delta / dirty / save` 生命周期如何保持简单。 + +Lapce 应作为: + +> **typed RPC、proxy 边界、doc delta/save、file explorer state machine、配置和 keymap 的辅助参考。** + +### 2.4 SideX 不替代 Zed,但应加入参考矩阵 + +SideX 的 README 明确写的是: + +> **VSCode's workbench, without Electron.** + +也就是说,SideX 不是 Rust 原生重写 VS Code。它更接近: + +> **保留 VS Code TypeScript workbench / Monaco / service 模型,把 Electron main process、Node fs、node-pty、sqlite、watcher、search 等系统能力替换为 Tauri + Rust command / crate。** + +因此 SideX 不能替代 Zed 成为 MNote 的主架构参考,原因是: + +- 它仍保留 VS Code 的 TypeScript workbench、DI、service、contrib 体系。 +- 它的目标是“移植 VS Code”,不是建立 projection-first 的 local-first Markdown workspace。 +- 它会把 MNote 拉回通用 IDE / extension host / desktop shell 的复杂度。 + +但 SideX 很适合作为一类新参考: + +> **VS Code 行为如何落到 Rust 后端的迁移样本。** + +它比官方 VS Code 更接近 MNote 的地方在于: + +- Rust `sidex-workspace` 已经抽出 file tree、watcher、search、path util、dirty diff、multi-root 等 workspace crate。 +- Rust `sidex-keymap` 已经实现接近 VS Code `when` clause 的 context key evaluator。 +- Rust `sidex-settings` 已经实现 default / user / workspace 的分层 settings。 +- Tauri command 层把 `fs`、terminal、git、search、storage 等 Node/Electron 能力转换成 Rust command 边界。 + +所以本矩阵应把 SideX 放在: + +> **“VS Code 行为规格”和“MNote Rust 实现”之间的桥接参考。** + +### 2.5 Helix 不作为主参考 + +Helix 是优秀的 Rust 终端编辑器,适合参考 LSP、Tree-sitter、文本核心和配置理念,但它不是 workbench / Web shell / local-first workspace 主参考。 + +--- + +## 3. 参考源职责划分 + +### 3.1 Zed:主架构参考 + +重点参考: + +- `crates/worktree/src/worktree.rs` +- `crates/project/src/worktree_store.rs` +- `crates/project/src/project.rs` +- `crates/project/src/buffer_store.rs` +- `crates/project/src/project_search.rs` +- `crates/file_finder/src/file_finder.rs` +- `crates/agent/src/tools/edit_file_tool.rs` +- `crates/agent/src/tools/read_file_tool.rs` +- `crates/agent/src/tools/write_file_tool.rs` +- `crates/agent/src/tools/edit_session.rs` +- `crates/agent/src/tool_permissions.rs` + +不重点采用: + +- GPUI / Entity / App / Window 渲染壳。 +- pane / dock / desktop lifecycle。 +- collab / remote / diagnostics 的完整产品线。 +- 与 MNote local-first Markdown 主线无关的通用 IDE 复杂度。 + +### 3.2 Lapce:辅助实现参考 + +重点参考: + +- `lapce-app/src/doc.rs` +- `lapce-rpc/src/proxy.rs` +- `lapce-proxy/src/dispatch.rs` +- `lapce-proxy/src/watcher.rs` +- `lapce-app/src/file_explorer/data.rs` +- `lapce-app/src/global_search.rs` +- `lapce-app/src/command.rs` +- `lapce-app/src/main_split.rs` +- `lapce-core/src/syntax/mod.rs` +- `lapce-core/src/language.rs` +- `lapce-core/src/encoding.rs` +- `lapce-app/src/config.rs` +- `defaults/settings.toml` +- `lapce-app/src/keypress/loader.rs` +- `defaults/keymaps-common.toml` +- `lapce-proxy/src/plugin/mod.rs` + +不重点采用: + +- Lapce 自身 UI 框架选择。 +- 完整插件生态重建。 +- 终端、调试器等 MNote 当前阶段不需要的 IDE 面。 + +### 3.3 VS Code:行为规格参考 + +继续参考: + +- `src/vs/platform/files` +- `src/vs/platform/commands` +- `src/vs/platform/contextkey` +- `src/vs/workbench/contrib/files` +- `src/vs/workbench/services/filesConfiguration` +- `src/vs/base/browser/ui/list` +- `src/vs/base/browser/ui/tree` + +重点看行为: + +- file service 的能力边界。 +- save / save as / revert / conflict 的用户体验。 +- workspace watcher 与外部文件变更处理。 +- context key / command enablement。 +- file import / export / open editors。 +- tree/list 的选择、焦点、键盘导航和 DnD 行为。 + +不采用: + +- TypeScript service 体系逐行转译。 +- Electron workbench lifecycle。 +- VS Code extension host 作为 MNote 当前插件模型。 + +### 3.4 SideX:VS Code 行为到 Rust 后端的桥接参考 + +重点参考: + +- `ARCHITECTURE.md` +- `src-tauri/src/lib.rs` +- `src-tauri/src/commands/fs.rs` +- `src-tauri/src/commands/validation.rs` +- `crates/sidex-workspace/src/lib.rs` +- `crates/sidex-workspace/src/file_tree.rs` +- `crates/sidex-workspace/src/watcher.rs` +- `crates/sidex-workspace/src/search.rs` +- `crates/sidex-workspace/src/path_util.rs` +- `crates/sidex-keymap/src/context.rs` +- `crates/sidex-keymap/src/resolver.rs` +- `crates/sidex-settings/src/settings.rs` +- `crates/sidex-settings/src/jsonc.rs` +- `crates/sidex-terminal/src/pty.rs` +- `crates/sidex-terminal/src/manager.rs` + +重点看边界: + +- Electron / Node 能力如何映射为 Rust command。 +- 文件系统 command 如何先做 path validation,再进入 workspace crate。 +- Rust watcher / search / file tree 如何对外输出可序列化事件和结果。 +- VS Code `when` clause / context key 如何在 Rust 侧建模。 +- default / user / workspace settings 如何分层合并。 +- PTY / terminal manager 如何从 Node native 模块迁移到 Rust。 + +不采用: + +- VS Code TypeScript workbench 的整体移植路线。 +- Tauri desktop shell 作为 MNote 当前 Web 主壳。 +- extension host / debugger / terminal / git 的完整 IDE 产品面。 +- SideX 当前对绝对路径 command 的简单安全模型;MNote 必须继续使用 allowed roots、workspace root、symlink escape 和文件版本仲裁。 + +--- + +## 4. 采用矩阵 + +| 主题 | 主参考 | MNote 当前缺口 | 采用方式 | 优先级 | +| --- | --- | --- | --- | --- | +| `ProjectPath` / 文件身份 | Zed `ProjectPath` | local folder、page `.md`、resource asset 需要稳定 `{workspace/root + rel path}` 身份 | 借鉴模式,映射到 `KernelObjectIdentity` / File Tree row | P0 | +| Worktree / root 管理 | Zed `worktree` + `worktree_store`,SideX `sidex-workspace` | local-first workspace 需要扫描、ignore、watch、root id、路径归一 | Zed 做主模型,SideX 补充 Rust workspace crate 拆分参考 | P0 | +| BufferStore / 打开态文件 | Zed `buffer_store`,Lapce `doc.rs` | tiptap、AI、外部编辑器会同时触碰同一 `.md`,需要统一 dirty/save/version | Zed 做架构,Lapce 做简化实现参考 | P0 | +| dirty / save / external change | VS Code file save 行为,Zed buffer,Lapce doc,SideX dirty diff / watcher | `/api/documents/save` 兼容面仍未完全退出,文件版本冲突模型待收口 | VS Code 校准行为,Zed/Lapce 定模型,SideX 看 Rust watcher / dirty diff 落点 | P0 | +| agent 文件编辑权限 | Zed `edit_file_tool` / `tool_permissions` | Hermes / Reasonix 需要 allowed roots、symlink escape、防脏 buffer 覆盖 | 直接作为权限模型主参考 | P0 | +| workspace search | Zed `project_search`,Lapce `global_search`,SideX `sidex-workspace/search.rs` | MNote 需要本地全文搜索与 Page Aggregate / File Tree 对齐 | Zed 做主搜索模型,Lapce 做结果投影,SideX 补充 Rust 并行搜索实现参考 | P1 | +| file finder / quick open | Zed `file_finder` | 工作区文件快速打开与 object tab host 尚未成体系 | 借鉴候选生成与排序,不复制 UI | P1 | +| file explorer 状态机 | Lapce `file_explorer/data.rs`,SideX `file_tree.rs`,VS Code tree 行为 | File Tree / Page Tree / Resource Tree 需要选择、展开、刷新、右键命令一致性 | Lapce 状态机 + SideX Rust file tree 能力 + VS Code 行为规格 | P1 | +| command / context key | VS Code `commands` / `contextkey`,SideX `sidex-keymap/context.rs`,Lapce `command.rs` | tree/page/object tab 命令 enablement 仍容易散落到 UI | VS Code 给语义,SideX 给 Rust when-clause evaluator 参考 | P1 | +| typed RPC / backend proxy | Lapce `lapce-rpc` + `lapce-proxy`,SideX Tauri commands | Rust Web、worker、local FS executor、agent runtime 需要 typed request 边界 | Lapce 参考协议拆分,SideX 参考 Node/Electron 能力迁移到 Rust command | P2 | +| config / settings | SideX `sidex-settings`,Lapce `config.rs` + `settings.toml`,VS Code settings 行为 | workspace / user / page options / AI policy 需要层级合并规则 | SideX 分层 settings 更接近 Rust 落地,Lapce/VS Code 校准行为 | P2 | +| keymap | SideX `sidex-keymap`,Lapce keypress loader,VS Code keybinding 行为 | editor shell、file tree、command palette 后续需要统一快捷键 | SideX 参考 Rust context/when 解析,Lapce 参考 loader,VS Code 校准行为 | P2 | +| terminal / PTY 边界 | SideX `sidex-terminal`,VS Code terminal 行为 | MNote 当前不是 IDE 终端优先,但 agent/local workspace 后续可能需要受控命令执行面 | 只参考 Rust PTY manager 和安全边界,不进入当前主线 | P3 | +| MultiBuffer / excerpts | Zed `MultiBuffer` | AI review、搜索结果、跨文件上下文需要多片段视图 | Phase C 之后采用,当前不前置 | P3 | +| review diff / streaming apply | Zed agent edit session,VS Code diff 行为 | AI suggest/review 仍设计冻结 | 只保留为后续参考,不当前实施 | P3 | +| 插件运行时 | Lapce plugin,SideX WASM extension,VS Code extension host | MNote 当前插件是 simplemindmap / office / local tool,不需要通用扩展平台 | 暂不采用,只保留观察 | P3 | + +--- + +## 5. P0 实施建议 + +P0 不应从 UI 开始,而应先把文件身份、打开态 buffer 和 agent 文件权限三个底座定住。 + +### 5.1 固定 MNote 的 `WorkspacePath` + +目标: + +> **用一个 Rust 侧稳定结构表达“哪个 workspace root 下的哪个相对路径”。** + +参考: + +- Zed `ProjectPath` +- Zed `WorktreeStore` + +建议语义: + +- `workspace_id` +- `root_id` +- `source_kind` +- `relative_path` +- `object_identity` +- `resource_kind` + +约束: + +- 不允许 UI 只拿绝对路径当对象身份。 +- 不允许 Page Tree / File Tree / AI 工具各自生成不同 path key。 +- symlink、`..`、case sensitivity、ignore 规则必须由 Rust workspace 层处理。 + +### 5.2 建立 `BufferStore` / `DocumentBuffer` 最小模型 + +目标: + +> **让 tiptap autosave、AI 文件写入、外部编辑器修改都围绕同一份文件版本和 dirty 状态仲裁。** + +参考: + +- Zed `buffer_store.rs` +- Lapce `doc.rs` +- VS Code save conflict 行为 + +建议最小字段: + +- `workspace_path` +- `file_version` +- `base_content_hash` +- `current_content_hash` +- `dirty_state` +- `last_loaded_at` +- `last_saved_at` +- `external_change_state` + +当前必须避免: + +- 继续把 `/api/documents/save` 当长期正文写入主入口。 +- tiptap 和 AI 分别维护互不知情的 revision。 +- 外部文件变更直接覆盖前台 dirty buffer。 + +### 5.3 收口 agent 文件编辑权限 + +目标: + +> **MNote 只负责计算授权范围和同步投影,Hermes / Reasonix 在 allowed roots 内用自身 patch/diff 能力编辑文件。** + +参考: + +- Zed `edit_file_tool.rs` +- Zed `edit_session.rs` +- Zed `tool_permissions.rs` + +必须具备: + +- allowed roots。 +- 当前文件引用。 +- selection / range。 +- symlink escape 防护。 +- dirty buffer 写入前检查。 +- 写后 watcher / projection refresh。 + +这条线与当前 `7-14 online/local AI Markdown editing convergence` 一致:普通 local-first Markdown 编辑优先走 agent 原生文件编辑能力,`mnote.doc.markdown_edit` 只作为 cloud / remote agent / compat fallback。 + +--- + +## 6. P1 / P2 / P3 实施建议 + +### 6.1 P1:搜索、文件树、命令上下文 + +P1 解决的是 workspace 产品体验闭环: + +- 本地全文搜索。 +- 快速打开。 +- File Tree / Resource Tree / Page Tree 展开、选择、刷新、右键命令。 +- command enablement / context key。 + +参考组合: + +- Zed `project_search` + `file_finder` +- Lapce `global_search` + `file_explorer/data.rs` +- VS Code tree/list/contextkey 行为 + +验收口径: + +- 搜索结果能回到稳定 `WorkspacePath` / `ObjectIdentity`。 +- 文件树操作只发 command,不直接改真实对象。 +- 右键菜单和快捷键基于统一 command context,不在 UI 分支里临时判断。 + +### 6.2 P2:typed RPC、配置、keymap + +P2 解决的是边界清晰和长期可维护: + +- UI 到 Rust executor 的 typed request / response。 +- user / workspace / page / runtime policy 的配置合并。 +- 编辑器、树、command palette 的 keymap 加载和冲突处理。 + +参考组合: + +- Lapce `lapce-rpc/src/proxy.rs` +- Lapce `lapce-proxy/src/dispatch.rs` +- Lapce `config.rs` +- Lapce `keypress/loader.rs` +- VS Code settings / keybinding 行为 + +验收口径: + +- route / compat / worker 不再各自定义一套命令 payload。 +- 配置合并顺序可解释、可测试。 +- 快捷键归属到 command,不直接绑定到某个 DOM 事件分支。 + +### 6.3 P3:MultiBuffer、review diff、插件运行时 + +P3 只作为后续能力,不进入当前最前排。 + +可参考: + +- Zed `MultiBuffer`:搜索结果、AI 上下文、多文件 excerpt。 +- Zed agent edit session:review / apply / reject。 +- VS Code diff:冲突、review、保存体验。 +- Lapce plugin:轻量插件隔离。 + +当前边界: + +- 不实施流式 apply + suggest/review。 +- 不重建 VS Code extension host。 +- 不把 MNote 拉成通用 IDE。 + +### 6.4 SideX 专项采用边界 + +SideX 可以补强 P1 / P2,但不改变 P0 主线。 + +可以参考的部分: + +- `sidex-workspace` 的 crate 拆分:`file_tree / watcher / search / path_util / dirty_diff / multi_root`。 +- `src-tauri/src/commands/fs.rs` 的 command facade:前端请求先进入窄 command,再进入 Rust workspace 能力。 +- `src-tauri/src/commands/validation.rs` 的输入校验意识,但 MNote 需要更强的 workspace root / allowed roots / symlink escape 校验。 +- `sidex-keymap/context.rs` 的 `when` clause AST 和 evaluator,可作为 MNote command context 的 Rust 参考。 +- `sidex-settings/settings.rs` 的 default / user / workspace settings 分层,可作为 MNote user / workspace / page / AI policy 合并规则的早期参考。 +- `sidex-workspace/search.rs` 的 ignore-aware、rayon 并行全文搜索,可作为本地 Markdown workspace 搜索实现参考。 + +不应采用的部分: + +- 不采用“把 VS Code TypeScript workbench 原样移植到 Tauri”的产品路线。 +- 不把 MNote `3000` Rust Web 主壳替换成 Tauri desktop shell。 +- 不把 extension host / debugger / terminal / git 做成当前默认产品能力。 +- 不直接沿用 SideX 的绝对路径 command 安全模型;MNote 必须以 workspace identity、allowed roots、file version 和 agent permission 为准。 + +对 MNote 的实际帮助是: + +> **当我们从 VS Code 学到一个行为时,可以先看 SideX 是否已有 Rust crate / Tauri command 级别的落地方式;如果有,就用 SideX 校准“这个行为如何从 Node/Electron 能力迁移到 Rust 后端”,再按 MNote 的 kernel / projection / command contract 重写。** + +--- + +## 7. VS Code 仍应继续看的源码 + +虽然不建议转译 VS Code,但下面这些仍值得继续深读,因为它们定义了成熟编辑器的行为边界。 + +### 7.1 文件服务与保存冲突 + +重点: + +- `src/vs/platform/files` +- `src/vs/workbench/services/filesConfiguration` +- `textFileSaveErrorHandler` + +关注问题: + +- 文件不存在、权限失败、外部修改、编码变化、只读文件如何呈现。 +- dirty buffer 与磁盘版本冲突时如何让用户选择。 +- save / save as / revert 的命令边界。 + +### 7.2 Watcher 与外部变更 + +重点: + +- `workspaceWatcher` +- file service event。 + +关注问题: + +- 外部编辑器修改 `.md` 后如何刷新投影。 +- 当前 buffer dirty 时如何避免静默覆盖。 +- rename / delete / move 对打开 tab 和 file tree 的影响。 + +### 7.3 Context Key 与 Command Enablement + +重点: + +- `src/vs/platform/contextkey` +- `src/vs/platform/commands` + +关注问题: + +- 命令是否可用不应散落在 UI 条件判断里。 +- File Tree / Page Tree / Object Tab / Editor selection 应贡献统一 context。 +- 右键菜单、快捷键、command palette 应消费同一套 command context。 + +### 7.4 Tree / List 行为 + +重点: + +- `src/vs/base/browser/ui/list` +- `src/vs/base/browser/ui/tree` +- `src/vs/workbench/contrib/files` + +关注问题: + +- 展开、收起、选择、焦点、键盘导航。 +- DnD 和 rename 的边界。 +- open editors / file explorer 如何处理打开态与磁盘态。 + +--- + +## 8. 风险与边界 + +### 8.1 许可与代码来源边界 + +Zed、Lapce、VS Code 都只能作为参考来源。后续实现应遵循: + +- 不直接复制大段源码。 +- 不把第三方项目的 UI 壳、生命周期和产品概念搬进 MNote。 +- 引用具体设计时在设计稿或实现注释中标明“参考模式”,避免伪装成原创推导。 + +### 8.2 产品边界风险 + +MNote 不应因为参考 Zed / Lapce / VS Code 而变成通用 IDE。 + +必须保持: + +- 页面正文真相仍是 local-first `.md`。 +- Rust kernel 持有树、资源、投影、命令语义。 +- Page Aggregate 是页面主投影。 +- File Tree 是 local workspace 的组织投影。 +- Hermes / Reasonix agent 文件编辑必须受 allowed roots 和文件版本模型约束。 + +### 8.3 技术路线风险 + +最大风险不是“参考不够”,而是参考源混用后边界变模糊。 + +固定规则: + +- Zed 负责主架构参考。 +- Lapce 负责轻量实现参考。 +- SideX 负责 VS Code 行为到 Rust 后端的迁移参考。 +- VS Code 负责行为规格参考。 +- MNote 自己的 Rust kernel / projection / command contract 负责最终真相。 + +--- + +## 9. 当前建议结论 + +后续推进顺序建议固定为: + +1. **P0:`WorkspacePath / ProjectPath`、`WorktreeStore`、`BufferStore`、agent file edit permission。** +2. **P1:workspace search、file finder、file explorer state machine、command context。** +3. **P2:typed RPC / proxy、SideX-style Rust command facade、config、keymap。** +4. **P3:terminal / PTY 边界、MultiBuffer excerpt、review diff、插件运行时。** + +一句话结论: + +> **不要把 VS Code TypeScript 源码转译成 Rust;应以 Zed 的 Rust workspace / buffer / agent 架构为主参考,以 Lapce 的 typed RPC / proxy / doc delta 为辅助参考,以 SideX 校准 VS Code 行为迁移到 Rust 后端的落地方式,再用官方 VS Code 校准成熟编辑器行为。** diff --git a/design/05-editor-mainline/process/5-15-local-markdown-path-identity-unification-checklist-v1.md b/design/05-editor-mainline/process/5-15-local-markdown-path-identity-unification-checklist-v1.md new file mode 100644 index 00000000..5528d9fe --- /dev/null +++ b/design/05-editor-mainline/process/5-15-local-markdown-path-identity-unification-checklist-v1.md @@ -0,0 +1,135 @@ +# 5-15 本地 Markdown 路径身份统一 Checklist v1 + +## 背景 + +当前本地 Markdown 主链正在对齐 VS Code 文件模型:普通 `.md` 文件的运行时身份应来自文件资源地址,而不是 frontmatter 或 MNote 内部稳定 ID。 + +VS Code 参考: + +- `design/05-editor-mainline/reference-code/vscode/src/vs/platform/files/common/fileService.ts` + - `createFile(resource)` 以 `URI` 创建文件。 +- `design/05-editor-mainline/reference-code/vscode/src/vs/workbench/contrib/files/browser/fileCommands.ts` + - 新建文件后 `openEditor({ resource: saveUri })`。 +- `design/05-editor-mainline/reference-code/vscode/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts` + - `matches()` 使用 `isEqual(otherInput.resource, this.resource)`。 +- `design/05-editor-mainline/reference-code/vscode/src/vs/workbench/contrib/files/browser/editors/fileEditorHandler.ts` + - working copy 与 editor 是否同一打开项,比较 `workingCopy.resource` 与 `editor.resource`。 + +本阶段目标不是引入新的稳定语义 ID,而是先消除普通本地 Markdown runtime 中的多 ID 竞争。 + +## 身份原则 + +- 普通本地 Markdown 的真实身份是 `sourceKind + rootUri + relativePath`。 +- 对外 `documentId` 继续使用路径型短句柄:`local-md:`。 +- `mnote_id` 只作为已有 frontmatter 元数据保留,不参与普通本地 Markdown 的 `documentId` 决策。 +- `.mnote/page-ids.json` 只允许作为历史兼容/迁移辅助,不参与普通本地 Markdown 的主身份生成。 +- 不主动修改、删除或重写用户 Markdown frontmatter。 + +## 允许修改范围 + +- `rust/crates/mnote-web/src/routes/local_search_index.rs` +- `rust/crates/mnote-web/src/routes/local_folder_source.rs` +- 必要时更新直接相关测试: + - `rust/crates/mnote-web/src/routes/search.rs` + - `rust/crates/mnote-web/src/routes/tree.rs` + - `rust/crates/mnote-web/src/routes/documents.rs` + +不要修改用户数据目录,不要清理仓库中无关脏文件,不要做 git commit。 + +## P0 Checklist + +### 1. 搜索索引 documentId 与树形列表统一 + +- [ ] `local_search_index.rs` 中 `index_markdown_file()` 永远按 `relative_path` 生成 `local-md:`。 +- [ ] frontmatter `mnote_id` 不再生成 `local-mdid:*`。 +- [ ] 搜索结果、最近修改、反链接口返回的 Markdown `documentId` 与文件树一致。 +- [ ] 含 `mnote_id` 的 Markdown 搜索测试必须断言仍返回路径型 ID,例如 `local-md:docs~2Fchild.md`。 + +验收: + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_search_index -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web search_local_index -- --nocapture +``` + +### 2. 清理本地 Markdown 身份生成的误导参数 + +- [ ] `local_markdown_page_id(relative_path, _mnote_id, _metadata)` 不再保留无用参数签名。 +- [ ] 普通调用点直接使用 `local_markdown_path_page_id(relative_path)` 或新的清晰命名函数。 +- [ ] 如果必须保留 legacy resolver,函数名必须包含 `legacy` / `compat`,并且不能参与普通本地 Markdown 主链。 + +验收: + +```bash +rg -n "local_markdown_page_id\\(|local-mdid|mnote_id|page-ids" rust/crates/mnote-web/src/routes/local_folder_source.rs rust/crates/mnote-web/src/routes/local_search_index.rs +``` + +期望: + +- `local_markdown_page_id(` 不应再出现在普通主链调用中。 +- `local_search_index.rs` 不应再出现 `local-mdid` 生成逻辑。 +- `mnote_id` 可以继续出现在解析、兼容测试或“保留 frontmatter”测试中,但不能参与普通 `documentId` 生成。 + +### 3. 不污染用户文件 + +- [ ] 新建本地 Markdown 不写 `mnote_id` frontmatter。 +- [ ] 复制本地 Markdown 不改写 `mnote_id`。 +- [ ] 保存已有 Markdown 时保留原 frontmatter,但不新增身份字段。 + +验收: + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_tree_command_local_folder_lifecycle -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_page_aggregate_accepts_path_id_when_frontmatter_has_mnote_id -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_save -- --nocapture +``` + +## P1 Checklist + +### 4. 路径型身份覆盖打开/保存/搜索一致性 + +- [ ] 同一个含 `mnote_id` 的 `.md` 文件,在文件树、页面 aggregate、搜索索引中返回同一个 `local-md:`。 +- [ ] `documents.content.get` / 页面打开不再需要 `local-mdid:*` 才能命中文件。 +- [ ] rename/move 后新路径返回新的路径型 `documentId`,旧路径型 ID 不作为稳定页面 ID 保留。 + +验收: + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown_identity_uses_path_even_when_frontmatter_has_mnote_id -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_page_aggregate_accepts_path_id_when_frontmatter_has_mnote_id -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder_lifecycle -- --nocapture +``` + +## 最终验收 + +```bash +cargo fmt --check --all --manifest-path rust/Cargo.toml +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_search_index -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web search_local_index -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown_identity_uses_path_even_when_frontmatter_has_mnote_id -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_page_aggregate_accepts_path_id_when_frontmatter_has_mnote_id -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder_lifecycle -- --nocapture +``` + +若涉及浏览器可见行为,Codex 复核阶段必须使用真实浏览器验证并将截图放在 `tmp/` 下。 + +## 执行状态(2026-05-19) + +- [x] P0-1 搜索索引 `documentId` 已统一为 `local-md:`,不再由 frontmatter `mnote_id` 生成 `local-mdid:*`。 +- [x] P0-2 普通本地 Markdown 主链已移除 `local_markdown_page_id(relative_path, _mnote_id, _metadata)` 误导签名,文件树、page aggregate、find-by-id 改用路径型 ID。 +- [x] P0-3 新建、复制、rename/move/restore/purge 普通本地 Markdown 不再写 `.mnote/page-ids.json`,也不再写入或改写 `mnote_id`。 +- [x] P1-4 rename/move 后返回新路径型 `documentId`,旧 ID 不再作为稳定页面 ID 保留。 +- [x] 追加清理:删除 `ParsedLocalMarkdownPage.mnote_id` 字段,避免后续代码重新把 frontmatter `mnote_id` 当身份入口。 + +已通过验收: + +```bash +cargo fmt --check --all --manifest-path rust/Cargo.toml +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_search_index -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web search_local_index -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown_identity_uses_path_even_when_frontmatter_has_mnote_id -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_page_aggregate_accepts_path_id_when_frontmatter_has_mnote_id -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web local_page_id_initialization_returns_path_id_without_writing_page_ids -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder -- --nocapture +``` + diff --git a/infra/convex/README.md b/infra/convex/README.md index e664f906..ca6d921e 100644 --- a/infra/convex/README.md +++ b/infra/convex/README.md @@ -4,6 +4,12 @@ 在 local-first 方案下,这套 Convex 自托管环境定位为控制面、cloud source、compat 和可选 sync replica 运行底座;页面正文、默认附件和本地 AI 会话全文不再以它作为早期产品默认数据真相。 +## 当前 functions 边界 + +- 仓库根 `convex/` 是当前 active Convex functions 部署源;目前只保留 `schema.ts` 与 `aiSessions.ts`,用于 ACP / Hermes runtime session store。 +- `recycle/wolai-frontend/convex/` 是退役前端随带的历史实现,不允许作为当前部署 fallback。仍需要的 `documents` / `mediaAssets` / `users` / `workspaces` 等旧 functions,必须明确迁回 active 区或移动到外部辅助区;不再使用的应正式退役。 +- `scripts/run-convex-deploy.js` 只从仓库根部署 `convex/`,缺少 active functions 时应失败,不应从 `recycle/` 回退。 + ## 启动 1. 确保仓库根目录 `.env.all` 存在(全局唯一 env 文件)。 @@ -32,10 +38,10 @@ docker compose exec backend ./generate_admin_key.sh ### 1) 验证 Convex functions 可用 -在 `wolai-frontend/` 执行: +当前根 `convex/` 只保留 ACP / Hermes runtime session store,部署前可先确认 active functions 文件存在: ```bash -npx convex dev --once --tail-logs disable --env-file ../.env.all --run ping:ping +test -f convex/schema.ts && test -f convex/aiSessions.ts ``` ### 2) 验证固定开发用户(阶段 3) diff --git a/local-markdown-gfm-design-checklist.png b/local-markdown-gfm-design-checklist.png deleted file mode 100644 index 10a67727..00000000 Binary files a/local-markdown-gfm-design-checklist.png and /dev/null differ diff --git a/local-markdown-gfm-design-page.png b/local-markdown-gfm-design-page.png deleted file mode 100644 index 2e95c291..00000000 Binary files a/local-markdown-gfm-design-page.png and /dev/null differ diff --git a/package-lock.json b/package-lock.json index c0dd6b67..db667d92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,458 +10,9 @@ "devDependencies": { "@playwright/test": "^1.56.1", "convex": "^1.39.1", - "electron": "^34.0.0", - "electron-builder": "^26.0.12", "playwright": "^1.56.1" } }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@electron/asar": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@electron/asar/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@electron/fuses": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", - "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.1", - "fs-extra": "^9.0.1", - "minimist": "^1.2.5" - }, - "bin": { - "electron-fuses": "dist/bin.js" - } - }, - "node_modules/@electron/fuses/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/fuses/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/fuses/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/notarize/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/osx-sign": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", - "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@electron/osx-sign/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/@electron/osx-sign/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/osx-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/rebuild": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", - "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.1.1", - "node-abi": "^4.2.0", - "node-api-version": "^0.2.1", - "node-gyp": "^12.2.0", - "read-binary-file-arch": "^1.0.6" - }, - "bin": { - "electron-rebuild": "lib/cli.js" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/universal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", - "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/asar": "^3.3.1", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.3.1", - "dir-compare": "^4.2.0", - "fs-extra": "^11.1.1", - "minimatch": "^9.0.3", - "plist": "^3.1.0" - }, - "engines": { - "node": ">=16.4" - } - }, - "node_modules/@electron/universal/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@electron/universal/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/windows-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", @@ -904,97 +455,6 @@ "node": ">=18" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/@playwright/test": { "version": "1.56.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", @@ -1011,838 +471,6 @@ "node": ">=18" } }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", - "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/app-builder-bin": { - "version": "5.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", - "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/app-builder-lib": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", - "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@electron/asar": "3.4.1", - "@electron/fuses": "^1.8.0", - "@electron/get": "^3.0.0", - "@electron/notarize": "2.5.0", - "@electron/osx-sign": "1.3.3", - "@electron/rebuild": "^4.0.3", - "@electron/universal": "2.0.3", - "@malept/flatpak-bundler": "^0.4.0", - "@types/fs-extra": "9.0.13", - "async-exit-hook": "^2.0.1", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chromium-pickle-js": "^0.2.0", - "ci-info": "4.3.1", - "debug": "^4.3.4", - "dotenv": "^16.4.5", - "dotenv-expand": "^11.0.6", - "ejs": "^3.1.8", - "electron-publish": "26.8.1", - "fs-extra": "^10.1.0", - "hosted-git-info": "^4.1.0", - "isbinaryfile": "^5.0.0", - "jiti": "^2.4.2", - "js-yaml": "^4.1.0", - "json5": "^2.2.3", - "lazy-val": "^1.0.5", - "minimatch": "^10.0.3", - "plist": "3.1.0", - "proper-lockfile": "^4.1.2", - "resedit": "^1.7.0", - "semver": "~7.7.3", - "tar": "^7.5.7", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0", - "which": "^5.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "dmg-builder": "26.8.1", - "electron-builder-squirrel-windows": "26.8.1" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", - "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/app-builder-lib/node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/builder-util": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", - "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.6", - "7zip-bin": "~5.2.0", - "app-builder-bin": "5.0.0-alpha.12", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.6", - "debug": "^4.3.4", - "fs-extra": "^10.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "js-yaml": "^4.1.0", - "sanitize-filename": "^1.6.3", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0" - } - }, - "node_modules/builder-util-runtime": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", - "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/builder-util/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/builder-util/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/builder-util/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/convex": { "version": "1.39.1", "resolved": "https://registry.npmjs.org/convex/-/convex-1.39.1.tgz", @@ -1882,649 +510,6 @@ } } }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/dir-compare": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", - "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5", - "p-limit": "^3.1.0 " - } - }, - "node_modules/dir-compare/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/dmg-builder": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", - "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "fs-extra": "^10.1.0", - "iconv-lite": "^0.6.2", - "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - } - }, - "node_modules/dmg-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dmg-builder/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/dmg-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dotenv-expand": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dotenv": "^16.4.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron": { - "version": "34.5.8", - "resolved": "https://registry.npmjs.org/electron/-/electron-34.5.8.tgz", - "integrity": "sha512-vxLD65mabTzYmEVa9KceMHM0+zO+vqgrhcyNVlmTd0IGV5J7XZ8v/qElm0o4YQ4wPeq7olZkUjZkBQQEdr23/g==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^20.9.0", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-builder": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", - "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "dmg-builder": "26.8.1", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "simple-update-notifier": "2.0.0", - "yargs": "^17.6.2" - }, - "bin": { - "electron-builder": "cli.js", - "install-app-deps": "install-app-deps.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/electron-builder-squirrel-windows": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", - "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "electron-winstaller": "5.4.0" - } - }, - "node_modules/electron-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-publish": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", - "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "form-data": "^4.0.5", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" - } - }, - "node_modules/electron-publish/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-publish/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-winstaller": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@electron/asar": "^3.2.1", - "debug": "^4.1.1", - "fs-extra": "^7.0.1", - "lodash": "^4.17.21", - "temp": "^0.9.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "@electron/windows-sign": "^1.1.2" - } - }, - "node_modules/electron-winstaller/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/esbuild": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", @@ -2567,190 +552,6 @@ "@esbuild/win32-x64": "0.27.0" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/filelist": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -2766,996 +567,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isbinaryfile": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.31.0.tgz", - "integrity": "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.6.3" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-api-version": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/node-api-version/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", - "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.4", - "tinyglobby": "^0.2.12", - "undici": "^6.25.0", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/node-gyp/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pe-library": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", - "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/playwright": { "version": "1.56.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", @@ -3788,51 +599,6 @@ "node": ">=18" } }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/prettier": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", @@ -3849,703 +615,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-binary-file-arch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "bin": { - "read-binary-file-arch": "cli.js" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resedit": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", - "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pe-library": "^0.4.1" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sanitize-filename": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", - "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "7.5.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", - "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" - } - }, - "node_modules/temp-file/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/temp-file/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/tiny-async-pool": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", - "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^5.5.0" - } - }, - "node_modules/tiny-async-pool/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" - } - }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", - "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -4567,86 +636,6 @@ "optional": true } } - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/package.json b/package.json index b9caab23..bb1d46c3 100644 --- a/package.json +++ b/package.json @@ -2,67 +2,16 @@ "name": "mnote-desktop", "version": "0.1.0", "private": true, - "main": "desktop-electron/main.js", "scripts": { "desktop": "node scripts/desktop-prod.js", "desktop:hot": "node scripts/desktop-hot.js", "dev:hot": "node scripts/dev-hot.js", - "desktop:local": "node scripts/desktop-local.js", - "desktop:electron": "node desktop-electron/dev-runner.js", - "check:local-first-convex-guard": "node scripts/check-local-first-convex-guard.js", - "build:desktop:next": "node scripts/prepare-desktop-next.js", - "dist:win": "pnpm run build:desktop:next && electron-builder --win nsis" + "check:local-first-convex-guard": "node scripts/check-local-first-convex-guard.js" }, "dependencies": {}, "devDependencies": { "@playwright/test": "^1.56.1", "convex": "^1.39.1", - "electron": "^34.0.0", - "electron-builder": "^26.0.12", "playwright": "^1.56.1" - }, - "build": { - "appId": "org.aichem.mnote", - "productName": "MNOTE", - "asar": true, - "directories": { - "output": "dist", - "buildResources": "desktop-electron/build" - }, - "files": [ - "package.json", - "desktop-electron/**", - "!desktop-electron/desktop-next{,/**}" - ], - "extraResources": [ - { - "from": "desktop-electron/desktop-next", - "to": "desktop-next", - "filter": [ - "**/*", - "!node_modules{,/**}" - ] - }, - { - "from": "desktop-electron/desktop-next/node_modules", - "to": "desktop-next/node_modules" - } - ], - "win": { - "target": [ - "nsis" - ] - }, - "nsis": { - "oneClick": false, - "allowToChangeInstallationDirectory": true, - "include": "desktop-electron/build/installer.nsh" - } - }, - "pnpm": { - "onlyBuiltDependencies": [ - "electron", - "electron-winstaller" - ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbca8216..43051271 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,61 +14,12 @@ importers: convex: specifier: ^1.39.1 version: 1.39.1 - electron: - specifier: ^34.0.0 - version: 34.5.8 - electron-builder: - specifier: ^26.0.12 - version: 26.4.0(electron-builder-squirrel-windows@26.4.0) playwright: specifier: ^1.56.1 version: 1.57.0 packages: - 7zip-bin@5.2.0: - resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==} - - '@develar/schema-utils@2.6.5': - resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} - engines: {node: '>= 8.9.0'} - - '@electron/asar@3.4.1': - resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} - engines: {node: '>=10.12.0'} - hasBin: true - - '@electron/fuses@1.8.0': - resolution: {integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==} - hasBin: true - - '@electron/get@2.0.3': - resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} - engines: {node: '>=12'} - - '@electron/notarize@2.5.0': - resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} - engines: {node: '>= 10.0.0'} - - '@electron/osx-sign@1.3.3': - resolution: {integrity: sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==} - engines: {node: '>=12.0.0'} - hasBin: true - - '@electron/rebuild@4.0.1': - resolution: {integrity: sha512-iMGXb6Ib7H/Q3v+BKZJoETgF9g6KMNZVbsO4b7Dmpgb5qTFqyFTzqW9F3TOSHdybv2vKYKzSS9OiZL+dcJb+1Q==} - engines: {node: '>=22.12.0'} - hasBin: true - - '@electron/universal@2.0.3': - resolution: {integrity: sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==} - engines: {node: '>=16.4'} - - '@electron/windows-sign@1.2.2': - resolution: {integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==} - engines: {node: '>=14.14'} - hasBin: true - '@esbuild/aix-ppc64@0.27.0': resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} engines: {node: '>=18'} @@ -225,281 +176,11 @@ packages: cpu: [x64] os: [win32] - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - - '@malept/cross-spawn-promise@2.0.0': - resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} - engines: {node: '>= 12.13.0'} - - '@malept/flatpak-bundler@0.4.0': - resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} - engines: {node: '>= 10.0.0'} - - '@npmcli/agent@3.0.0': - resolution: {integrity: sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==} - engines: {node: ^18.17.0 || >=20.5.0} - - '@npmcli/fs@4.0.0': - resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==} - engines: {node: ^18.17.0 || >=20.5.0} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@playwright/test@1.57.0': resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} engines: {node: '>=18'} hasBin: true - '@sindresorhus/is@4.6.0': - resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} - engines: {node: '>=10'} - - '@szmarczak/http-timer@4.0.6': - resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} - engines: {node: '>=10'} - - '@types/cacheable-request@6.0.3': - resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - - '@types/fs-extra@9.0.13': - resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} - - '@types/http-cache-semantics@4.0.4': - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - - '@types/keyv@3.1.4': - resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - - '@types/node@20.19.28': - resolution: {integrity: sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==} - - '@types/node@25.0.6': - resolution: {integrity: sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==} - - '@types/plist@3.0.5': - resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} - - '@types/responselike@1.0.3': - resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - - '@types/verror@1.10.11': - resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==} - - '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - - '@xmldom/xmldom@0.8.11': - resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} - engines: {node: '>=10.0.0'} - - abbrev@3.0.1: - resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} - engines: {node: ^18.17.0 || >=20.5.0} - - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - - ajv-keywords@3.5.2: - resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} - peerDependencies: - ajv: ^6.9.1 - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - - app-builder-bin@5.0.0-alpha.12: - resolution: {integrity: sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==} - - app-builder-lib@26.4.0: - resolution: {integrity: sha512-Uas6hNe99KzP3xPWxh5LGlH8kWIVjZixzmMJHNB9+6hPyDpjc7NQMkVgi16rQDdpCFy22ZU5sp8ow7tvjeMgYQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - dmg-builder: 26.4.0 - electron-builder-squirrel-windows: 26.4.0 - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - assert-plus@1.0.0: - resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} - engines: {node: '>=0.8'} - - astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - - async-exit-hook@2.0.1: - resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} - engines: {node: '>=0.12.0'} - - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - - boolean@3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - - builder-util-runtime@9.5.1: - resolution: {integrity: sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==} - engines: {node: '>=12.0.0'} - - builder-util@26.3.4: - resolution: {integrity: sha512-aRn88mYMktHxzdqDMF6Ayj0rKoX+ZogJ75Ck7RrIqbY/ad0HBvnS2xA4uHfzrGr5D2aLL3vU6OBEH4p0KMV2XQ==} - - cacache@19.0.1: - resolution: {integrity: sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==} - engines: {node: ^18.17.0 || >=20.5.0} - - cacheable-lookup@5.0.4: - resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} - engines: {node: '>=10.6.0'} - - cacheable-request@7.0.4: - resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} - engines: {node: '>=8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - - chromium-pickle-js@0.2.0: - resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} - - ci-info@4.3.1: - resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} - engines: {node: '>=8'} - - cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} - - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - - cli-truncate@2.1.0: - resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} - engines: {node: '>=8'} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - clone-response@1.0.3: - resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} - - clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - - commander@5.1.0: - resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} - engines: {node: '>= 6'} - - commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - - compare-version@0.1.2: - resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} - engines: {node: '>=0.10.0'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - convex@1.39.1: resolution: {integrity: sha512-W+gVXA7BpRF1xLlS1kGTtKVaqd5yonqbGESKiPtIUXjV744GdDz8IG7RVsSY5KzHbgxuJBHKaJYk+92OIHTskQ==} engines: {node: '>=18.0.0', npm: '>=7.0.0'} @@ -519,627 +200,16 @@ packages: react: optional: true - core-util-is@1.0.2: - resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} - - crc@3.8.0: - resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} - - cross-dirname@0.1.0: - resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - - defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - - defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - detect-node@2.1.0: - resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - - dir-compare@4.2.0: - resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} - - dmg-builder@26.4.0: - resolution: {integrity: sha512-ce4Ogns4VMeisIuCSK0C62umG0lFy012jd8LMZ6w/veHUeX4fqfDrGe+HTWALAEwK6JwKP+dhPvizhArSOsFbg==} - - dmg-license@1.0.11: - resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} - engines: {node: '>=8'} - os: [darwin] - hasBin: true - - dotenv-expand@11.0.7: - resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} - engines: {node: '>=12'} - - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - ejs@3.1.10: - resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} - engines: {node: '>=0.10.0'} - hasBin: true - - electron-builder-squirrel-windows@26.4.0: - resolution: {integrity: sha512-7dvalY38xBzWNaoOJ4sqy2aGIEpl2S1gLPkkB0MHu1Hu5xKQ82il1mKSFlXs6fLpXUso/NmyjdHGlSHDRoG8/w==} - - electron-builder@26.4.0: - resolution: {integrity: sha512-FCUqvdq2AULL+Db2SUGgjOYTbrgkPxZtCjqIZGnjH9p29pTWyesQqBIfvQBKa6ewqde87aWl49n/WyI/NyUBog==} - engines: {node: '>=14.0.0'} - hasBin: true - - electron-publish@26.3.4: - resolution: {integrity: sha512-5/ouDPb73SkKuay2EXisPG60LTFTMNHWo2WLrK5GDphnWK9UC+yzYrzVeydj078Yk4WUXi0+TaaZsNd6Zt5k/A==} - - electron-winstaller@5.4.0: - resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} - engines: {node: '>=8.0.0'} - - electron@34.5.8: - resolution: {integrity: sha512-vxLD65mabTzYmEVa9KceMHM0+zO+vqgrhcyNVlmTd0IGV5J7XZ8v/qElm0o4YQ4wPeq7olZkUjZkBQQEdr23/g==} - engines: {node: '>= 12.20.55'} - hasBin: true - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - encoding@0.1.13: - resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} - - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - - err-code@2.0.3: - resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - es6-error@4.1.1: - resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} - esbuild@0.27.0: resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} engines: {node: '>=18'} hasBin: true - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - exponential-backoff@3.1.3: - resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - - extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - - extsprintf@1.4.1: - resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} - engines: {'0': node >=0.6.0} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - filelist@1.0.4: - resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} - - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - - fs-extra@11.3.3: - resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} - engines: {node: '>=14.14'} - - fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} - - fs-extra@8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} - - fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} - - fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - - fs-minipass@3.0.3: - resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - hasBin: true - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - global-agent@3.0.0: - resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} - engines: {node: '>=10.0'} - - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - got@11.8.6: - resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} - engines: {node: '>=10.19.0'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - hosted-git-info@4.1.0: - resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} - engines: {node: '>=10'} - - http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - - http2-wrapper@1.0.3: - resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} - engines: {node: '>=10.19.0'} - - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - - iconv-corefoundation@1.1.7: - resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} - engines: {node: ^8.11.2 || >=10} - os: [darwin] - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} - engines: {node: '>= 12'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - - is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - - isbinaryfile@4.0.10: - resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} - engines: {node: '>= 8.0.0'} - - isbinaryfile@5.0.7: - resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==} - engines: {node: '>= 18.0.0'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - isexe@3.1.1: - resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} - engines: {node: '>=16'} - - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - - jake@10.9.4: - resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} - engines: {node: '>=10'} - hasBin: true - - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} - hasBin: true - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - - jsonfile@6.2.0: - resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - lazy-val@1.0.5: - resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - - lowercase-keys@2.0.0: - resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} - engines: {node: '>=8'} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - - make-fetch-happen@14.0.3: - resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==} - engines: {node: ^18.17.0 || >=20.5.0} - - matcher@3.0.0: - resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} - engines: {node: '>=10'} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - mime@2.6.0: - resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} - engines: {node: '>=4.0.0'} - hasBin: true - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - mimic-response@1.0.1: - resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} - engines: {node: '>=4'} - - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - minipass-collect@2.0.1: - resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} - engines: {node: '>=16 || 14 >=14.17'} - - minipass-fetch@4.0.1: - resolution: {integrity: sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==} - engines: {node: ^18.17.0 || >=20.5.0} - - minipass-flush@1.0.5: - resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} - engines: {node: '>= 8'} - - minipass-pipeline@1.2.4: - resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} - engines: {node: '>=8'} - - minipass-sized@1.0.3: - resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} - engines: {node: '>=8'} - - minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - - minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - - minizlib@3.1.0: - resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} - engines: {node: '>= 18'} - - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - - node-abi@4.24.0: - resolution: {integrity: sha512-u2EC1CeNe25uVtX3EZbdQ275c74zdZmmpzrHEQh2aIYqoVjlglfUpOX9YY85x1nlBydEKDVaSmMNhR7N82Qj8A==} - engines: {node: '>=22.12.0'} - - node-addon-api@1.7.2: - resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} - - node-api-version@0.2.1: - resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} - - node-gyp@11.5.0: - resolution: {integrity: sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==} - engines: {node: ^18.17.0 || >=20.5.0} - hasBin: true - - nopt@8.1.0: - resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} - engines: {node: ^18.17.0 || >=20.5.0} - hasBin: true - - normalize-url@6.1.0: - resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} - engines: {node: '>=10'} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - - p-cancelable@2.1.1: - resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} - engines: {node: '>=8'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-map@7.0.4: - resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} - engines: {node: '>=18'} - - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - pe-library@0.4.1: - resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} - engines: {node: '>=12', npm: '>=6'} - - pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - playwright-core@1.57.0: resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} engines: {node: '>=18'} @@ -1150,290 +220,11 @@ packages: engines: {node: '>=18'} hasBin: true - plist@3.1.0: - resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} - engines: {node: '>=10.4.0'} - - postject@1.0.0-alpha.6: - resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} - engines: {node: '>=14.0.0'} - hasBin: true - prettier@3.8.3: resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} hasBin: true - proc-log@5.0.0: - resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} - engines: {node: ^18.17.0 || >=20.5.0} - - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - - promise-retry@2.0.1: - resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} - engines: {node: '>=10'} - - pump@3.0.3: - resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - - read-binary-file-arch@1.0.6: - resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} - hasBin: true - - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - resedit@1.7.2: - resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} - engines: {node: '>=12', npm: '>=6'} - - resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - - responselike@2.0.1: - resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} - - restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} - - retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - - rimraf@2.6.3: - resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - roarr@2.15.4: - resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} - engines: {node: '>=8.0'} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - sanitize-filename@1.6.3: - resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} - - sax@1.4.4: - resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} - engines: {node: '>=11.0.0'} - - semver-compare@1.0.0: - resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} - - semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - - serialize-error@7.0.1: - resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} - engines: {node: '>=10'} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - simple-update-notifier@2.0.0: - resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} - engines: {node: '>=10'} - - slice-ansi@3.0.0: - resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} - engines: {node: '>=8'} - - smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - - socks-proxy-agent@8.0.5: - resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} - engines: {node: '>= 14'} - - socks@2.8.7: - resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - sprintf-js@1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - - ssri@12.0.0: - resolution: {integrity: sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==} - engines: {node: ^18.17.0 || >=20.5.0} - - stat-mode@1.0.0: - resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} - engines: {node: '>= 6'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} - engines: {node: '>=12'} - - sumchecker@3.0.1: - resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} - engines: {node: '>= 8.0'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} - - tar@7.5.2: - resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} - engines: {node: '>=18'} - - temp-file@3.4.0: - resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} - - temp@0.9.4: - resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} - engines: {node: '>=6.0.0'} - - tiny-async-pool@1.3.0: - resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - tmp-promise@3.0.3: - resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} - - tmp@0.2.5: - resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} - engines: {node: '>=14.14'} - - truncate-utf8-bytes@1.0.2: - resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} - - type-fest@0.13.1: - resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} - engines: {node: '>=10'} - - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - - unique-filename@4.0.0: - resolution: {integrity: sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==} - engines: {node: ^18.17.0 || >=20.5.0} - - unique-slug@5.0.0: - resolution: {integrity: sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==} - engines: {node: ^18.17.0 || >=20.5.0} - - universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - utf8-byte-length@1.0.5: - resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - verror@1.10.1: - resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} - engines: {node: '>=0.6.0'} - - wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - which@5.0.0: - resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} - engines: {node: ^18.17.0 || >=20.5.0} - hasBin: true - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -1446,132 +237,8 @@ packages: utf-8-validate: optional: true - xmlbuilder@15.1.1: - resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} - engines: {node: '>=8.0'} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - snapshots: - 7zip-bin@5.2.0: {} - - '@develar/schema-utils@2.6.5': - dependencies: - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) - - '@electron/asar@3.4.1': - dependencies: - commander: 5.1.0 - glob: 7.2.3 - minimatch: 3.1.2 - - '@electron/fuses@1.8.0': - dependencies: - chalk: 4.1.2 - fs-extra: 9.1.0 - minimist: 1.2.8 - - '@electron/get@2.0.3': - dependencies: - debug: 4.4.3 - env-paths: 2.2.1 - fs-extra: 8.1.0 - got: 11.8.6 - progress: 2.0.3 - semver: 6.3.1 - sumchecker: 3.0.1 - optionalDependencies: - global-agent: 3.0.0 - transitivePeerDependencies: - - supports-color - - '@electron/notarize@2.5.0': - dependencies: - debug: 4.4.3 - fs-extra: 9.1.0 - promise-retry: 2.0.1 - transitivePeerDependencies: - - supports-color - - '@electron/osx-sign@1.3.3': - dependencies: - compare-version: 0.1.2 - debug: 4.4.3 - fs-extra: 10.1.0 - isbinaryfile: 4.0.10 - minimist: 1.2.8 - plist: 3.1.0 - transitivePeerDependencies: - - supports-color - - '@electron/rebuild@4.0.1': - dependencies: - '@malept/cross-spawn-promise': 2.0.0 - chalk: 4.1.2 - debug: 4.4.3 - detect-libc: 2.1.2 - got: 11.8.6 - graceful-fs: 4.2.11 - node-abi: 4.24.0 - node-api-version: 0.2.1 - node-gyp: 11.5.0 - ora: 5.4.1 - read-binary-file-arch: 1.0.6 - semver: 7.7.3 - tar: 6.2.1 - yargs: 17.7.2 - transitivePeerDependencies: - - supports-color - - '@electron/universal@2.0.3': - dependencies: - '@electron/asar': 3.4.1 - '@malept/cross-spawn-promise': 2.0.0 - debug: 4.4.3 - dir-compare: 4.2.0 - fs-extra: 11.3.3 - minimatch: 9.0.5 - plist: 3.1.0 - transitivePeerDependencies: - - supports-color - - '@electron/windows-sign@1.2.2': - dependencies: - cross-dirname: 0.1.0 - debug: 4.4.3 - fs-extra: 11.3.3 - minimist: 1.2.8 - postject: 1.0.0-alpha.6 - transitivePeerDependencies: - - supports-color - optional: true - '@esbuild/aix-ppc64@0.27.0': optional: true @@ -1650,347 +317,10 @@ snapshots: '@esbuild/win32-x64@0.27.0': optional: true - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': - dependencies: - '@isaacs/balanced-match': 4.0.1 - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.2 - - '@malept/cross-spawn-promise@2.0.0': - dependencies: - cross-spawn: 7.0.6 - - '@malept/flatpak-bundler@0.4.0': - dependencies: - debug: 4.4.3 - fs-extra: 9.1.0 - lodash: 4.17.21 - tmp-promise: 3.0.3 - transitivePeerDependencies: - - supports-color - - '@npmcli/agent@3.0.0': - dependencies: - agent-base: 7.1.4 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - lru-cache: 10.4.3 - socks-proxy-agent: 8.0.5 - transitivePeerDependencies: - - supports-color - - '@npmcli/fs@4.0.0': - dependencies: - semver: 7.7.3 - - '@pkgjs/parseargs@0.11.0': - optional: true - '@playwright/test@1.57.0': dependencies: playwright: 1.57.0 - '@sindresorhus/is@4.6.0': {} - - '@szmarczak/http-timer@4.0.6': - dependencies: - defer-to-connect: 2.0.1 - - '@types/cacheable-request@6.0.3': - dependencies: - '@types/http-cache-semantics': 4.0.4 - '@types/keyv': 3.1.4 - '@types/node': 20.19.28 - '@types/responselike': 1.0.3 - - '@types/debug@4.1.12': - dependencies: - '@types/ms': 2.1.0 - - '@types/fs-extra@9.0.13': - dependencies: - '@types/node': 25.0.6 - - '@types/http-cache-semantics@4.0.4': {} - - '@types/keyv@3.1.4': - dependencies: - '@types/node': 20.19.28 - - '@types/ms@2.1.0': {} - - '@types/node@20.19.28': - dependencies: - undici-types: 6.21.0 - - '@types/node@25.0.6': - dependencies: - undici-types: 7.16.0 - - '@types/plist@3.0.5': - dependencies: - '@types/node': 25.0.6 - xmlbuilder: 15.1.1 - optional: true - - '@types/responselike@1.0.3': - dependencies: - '@types/node': 20.19.28 - - '@types/verror@1.10.11': - optional: true - - '@types/yauzl@2.10.3': - dependencies: - '@types/node': 20.19.28 - optional: true - - '@xmldom/xmldom@0.8.11': {} - - abbrev@3.0.1: {} - - agent-base@7.1.4: {} - - ajv-keywords@3.5.2(ajv@6.12.6): - dependencies: - ajv: 6.12.6 - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-regex@5.0.1: {} - - ansi-regex@6.2.2: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@6.2.3: {} - - app-builder-bin@5.0.0-alpha.12: {} - - app-builder-lib@26.4.0(dmg-builder@26.4.0)(electron-builder-squirrel-windows@26.4.0): - dependencies: - '@develar/schema-utils': 2.6.5 - '@electron/asar': 3.4.1 - '@electron/fuses': 1.8.0 - '@electron/notarize': 2.5.0 - '@electron/osx-sign': 1.3.3 - '@electron/rebuild': 4.0.1 - '@electron/universal': 2.0.3 - '@malept/flatpak-bundler': 0.4.0 - '@types/fs-extra': 9.0.13 - async-exit-hook: 2.0.1 - builder-util: 26.3.4 - builder-util-runtime: 9.5.1 - chromium-pickle-js: 0.2.0 - ci-info: 4.3.1 - debug: 4.4.3 - dmg-builder: 26.4.0(electron-builder-squirrel-windows@26.4.0) - dotenv: 16.6.1 - dotenv-expand: 11.0.7 - ejs: 3.1.10 - electron-builder-squirrel-windows: 26.4.0(dmg-builder@26.4.0) - electron-publish: 26.3.4 - fs-extra: 10.1.0 - hosted-git-info: 4.1.0 - isbinaryfile: 5.0.7 - jiti: 2.6.1 - js-yaml: 4.1.1 - json5: 2.2.3 - lazy-val: 1.0.5 - minimatch: 10.1.1 - plist: 3.1.0 - resedit: 1.7.2 - semver: 7.7.3 - tar: 6.2.1 - temp-file: 3.4.0 - tiny-async-pool: 1.3.0 - which: 5.0.0 - transitivePeerDependencies: - - supports-color - - argparse@2.0.1: {} - - assert-plus@1.0.0: - optional: true - - astral-regex@2.0.0: - optional: true - - async-exit-hook@2.0.1: {} - - async@3.2.6: {} - - asynckit@0.4.0: {} - - at-least-node@1.0.0: {} - - balanced-match@1.0.2: {} - - base64-js@1.5.1: {} - - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - - boolean@3.2.0: - optional: true - - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - - buffer-crc32@0.2.13: {} - - buffer-from@1.1.2: {} - - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - builder-util-runtime@9.5.1: - dependencies: - debug: 4.4.3 - sax: 1.4.4 - transitivePeerDependencies: - - supports-color - - builder-util@26.3.4: - dependencies: - 7zip-bin: 5.2.0 - '@types/debug': 4.1.12 - app-builder-bin: 5.0.0-alpha.12 - builder-util-runtime: 9.5.1 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - fs-extra: 10.1.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - js-yaml: 4.1.1 - sanitize-filename: 1.6.3 - source-map-support: 0.5.21 - stat-mode: 1.0.0 - temp-file: 3.4.0 - tiny-async-pool: 1.3.0 - transitivePeerDependencies: - - supports-color - - cacache@19.0.1: - dependencies: - '@npmcli/fs': 4.0.0 - fs-minipass: 3.0.3 - glob: 10.5.0 - lru-cache: 10.4.3 - minipass: 7.1.2 - minipass-collect: 2.0.1 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - p-map: 7.0.4 - ssri: 12.0.0 - tar: 7.5.2 - unique-filename: 4.0.0 - - cacheable-lookup@5.0.4: {} - - cacheable-request@7.0.4: - dependencies: - clone-response: 1.0.3 - get-stream: 5.2.0 - http-cache-semantics: 4.2.0 - keyv: 4.5.4 - lowercase-keys: 2.0.0 - normalize-url: 6.1.0 - responselike: 2.0.1 - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - chownr@2.0.0: {} - - chownr@3.0.0: {} - - chromium-pickle-js@0.2.0: {} - - ci-info@4.3.1: {} - - cli-cursor@3.1.0: - dependencies: - restore-cursor: 3.1.0 - - cli-spinners@2.9.2: {} - - cli-truncate@2.1.0: - dependencies: - slice-ansi: 3.0.0 - string-width: 4.2.3 - optional: true - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - clone-response@1.0.3: - dependencies: - mimic-response: 1.0.1 - - clone@1.0.4: {} - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - - commander@5.1.0: {} - - commander@9.5.0: - optional: true - - compare-version@0.1.2: {} - - concat-map@0.0.1: {} - convex@1.39.1: dependencies: esbuild: 0.27.0 @@ -2000,199 +330,6 @@ snapshots: - bufferutil - utf-8-validate - core-util-is@1.0.2: - optional: true - - crc@3.8.0: - dependencies: - buffer: 5.7.1 - optional: true - - cross-dirname@0.1.0: - optional: true - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - - defaults@1.0.4: - dependencies: - clone: 1.0.4 - - defer-to-connect@2.0.1: {} - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - optional: true - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - optional: true - - delayed-stream@1.0.0: {} - - detect-libc@2.1.2: {} - - detect-node@2.1.0: - optional: true - - dir-compare@4.2.0: - dependencies: - minimatch: 3.1.2 - p-limit: 3.1.0 - - dmg-builder@26.4.0(electron-builder-squirrel-windows@26.4.0): - dependencies: - app-builder-lib: 26.4.0(dmg-builder@26.4.0)(electron-builder-squirrel-windows@26.4.0) - builder-util: 26.3.4 - fs-extra: 10.1.0 - iconv-lite: 0.6.3 - js-yaml: 4.1.1 - optionalDependencies: - dmg-license: 1.0.11 - transitivePeerDependencies: - - electron-builder-squirrel-windows - - supports-color - - dmg-license@1.0.11: - dependencies: - '@types/plist': 3.0.5 - '@types/verror': 1.10.11 - ajv: 6.12.6 - crc: 3.8.0 - iconv-corefoundation: 1.1.7 - plist: 3.1.0 - smart-buffer: 4.2.0 - verror: 1.10.1 - optional: true - - dotenv-expand@11.0.7: - dependencies: - dotenv: 16.6.1 - - dotenv@16.6.1: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - eastasianwidth@0.2.0: {} - - ejs@3.1.10: - dependencies: - jake: 10.9.4 - - electron-builder-squirrel-windows@26.4.0(dmg-builder@26.4.0): - dependencies: - app-builder-lib: 26.4.0(dmg-builder@26.4.0)(electron-builder-squirrel-windows@26.4.0) - builder-util: 26.3.4 - electron-winstaller: 5.4.0 - transitivePeerDependencies: - - dmg-builder - - supports-color - - electron-builder@26.4.0(electron-builder-squirrel-windows@26.4.0): - dependencies: - app-builder-lib: 26.4.0(dmg-builder@26.4.0)(electron-builder-squirrel-windows@26.4.0) - builder-util: 26.3.4 - builder-util-runtime: 9.5.1 - chalk: 4.1.2 - ci-info: 4.3.1 - dmg-builder: 26.4.0(electron-builder-squirrel-windows@26.4.0) - fs-extra: 10.1.0 - lazy-val: 1.0.5 - simple-update-notifier: 2.0.0 - yargs: 17.7.2 - transitivePeerDependencies: - - electron-builder-squirrel-windows - - supports-color - - electron-publish@26.3.4: - dependencies: - '@types/fs-extra': 9.0.13 - builder-util: 26.3.4 - builder-util-runtime: 9.5.1 - chalk: 4.1.2 - form-data: 4.0.5 - fs-extra: 10.1.0 - lazy-val: 1.0.5 - mime: 2.6.0 - transitivePeerDependencies: - - supports-color - - electron-winstaller@5.4.0: - dependencies: - '@electron/asar': 3.4.1 - debug: 4.4.3 - fs-extra: 7.0.1 - lodash: 4.17.21 - temp: 0.9.4 - optionalDependencies: - '@electron/windows-sign': 1.2.2 - transitivePeerDependencies: - - supports-color - - electron@34.5.8: - dependencies: - '@electron/get': 2.0.3 - '@types/node': 20.19.28 - extract-zip: 2.0.1 - transitivePeerDependencies: - - supports-color - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - encoding@0.1.13: - dependencies: - iconv-lite: 0.6.3 - optional: true - - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - - env-paths@2.2.1: {} - - err-code@2.0.3: {} - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - es6-error@4.1.1: - optional: true - esbuild@0.27.0: optionalDependencies: '@esbuild/aix-ppc64': 0.27.0 @@ -2222,502 +359,9 @@ snapshots: '@esbuild/win32-ia32': 0.27.0 '@esbuild/win32-x64': 0.27.0 - escalade@3.2.0: {} - - escape-string-regexp@4.0.0: - optional: true - - exponential-backoff@3.1.3: {} - - extract-zip@2.0.1: - dependencies: - debug: 4.4.3 - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - - extsprintf@1.4.1: - optional: true - - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fd-slicer@1.1.0: - dependencies: - pend: 1.2.0 - - fdir@6.5.0(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - - filelist@1.0.4: - dependencies: - minimatch: 5.1.6 - - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - - fs-extra@10.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - - fs-extra@11.3.3: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - - fs-extra@7.0.1: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - - fs-extra@8.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - - fs-extra@9.1.0: - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - - fs-minipass@2.1.0: - dependencies: - minipass: 3.3.6 - - fs-minipass@3.0.3: - dependencies: - minipass: 7.1.2 - - fs.realpath@1.0.0: {} - fsevents@2.3.2: optional: true - function-bind@1.1.2: {} - - get-caller-file@2.0.5: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - get-stream@5.2.0: - dependencies: - pump: 3.0.3 - - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - global-agent@3.0.0: - dependencies: - boolean: 3.2.0 - es6-error: 4.1.1 - matcher: 3.0.0 - roarr: 2.15.4 - semver: 7.7.3 - serialize-error: 7.0.1 - optional: true - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - optional: true - - gopd@1.2.0: {} - - got@11.8.6: - dependencies: - '@sindresorhus/is': 4.6.0 - '@szmarczak/http-timer': 4.0.6 - '@types/cacheable-request': 6.0.3 - '@types/responselike': 1.0.3 - cacheable-lookup: 5.0.4 - cacheable-request: 7.0.4 - decompress-response: 6.0.0 - http2-wrapper: 1.0.3 - lowercase-keys: 2.0.0 - p-cancelable: 2.1.1 - responselike: 2.0.1 - - graceful-fs@4.2.11: {} - - has-flag@4.0.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - optional: true - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hosted-git-info@4.1.0: - dependencies: - lru-cache: 6.0.0 - - http-cache-semantics@4.2.0: {} - - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - http2-wrapper@1.0.3: - dependencies: - quick-lru: 5.1.1 - resolve-alpn: 1.2.1 - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - iconv-corefoundation@1.1.7: - dependencies: - cli-truncate: 2.1.0 - node-addon-api: 1.7.2 - optional: true - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - ieee754@1.2.1: {} - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - ip-address@10.1.0: {} - - is-fullwidth-code-point@3.0.0: {} - - is-interactive@1.0.0: {} - - is-unicode-supported@0.1.0: {} - - isbinaryfile@4.0.10: {} - - isbinaryfile@5.0.7: {} - - isexe@2.0.0: {} - - isexe@3.1.1: {} - - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jake@10.9.4: - dependencies: - async: 3.2.6 - filelist: 1.0.4 - picocolors: 1.1.1 - - jiti@2.6.1: {} - - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stringify-safe@5.0.1: - optional: true - - json5@2.2.3: {} - - jsonfile@4.0.0: - optionalDependencies: - graceful-fs: 4.2.11 - - jsonfile@6.2.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - lazy-val@1.0.5: {} - - lodash@4.17.21: {} - - log-symbols@4.1.0: - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - - lowercase-keys@2.0.0: {} - - lru-cache@10.4.3: {} - - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - - make-fetch-happen@14.0.3: - dependencies: - '@npmcli/agent': 3.0.0 - cacache: 19.0.1 - http-cache-semantics: 4.2.0 - minipass: 7.1.2 - minipass-fetch: 4.0.1 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - negotiator: 1.0.0 - proc-log: 5.0.0 - promise-retry: 2.0.1 - ssri: 12.0.0 - transitivePeerDependencies: - - supports-color - - matcher@3.0.0: - dependencies: - escape-string-regexp: 4.0.0 - optional: true - - math-intrinsics@1.1.0: {} - - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mime@2.6.0: {} - - mimic-fn@2.1.0: {} - - mimic-response@1.0.1: {} - - mimic-response@3.1.0: {} - - minimatch@10.1.1: - dependencies: - '@isaacs/brace-expansion': 5.0.0 - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimatch@5.1.6: - dependencies: - brace-expansion: 2.0.2 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 - - minimist@1.2.8: {} - - minipass-collect@2.0.1: - dependencies: - minipass: 7.1.2 - - minipass-fetch@4.0.1: - dependencies: - minipass: 7.1.2 - minipass-sized: 1.0.3 - minizlib: 3.1.0 - optionalDependencies: - encoding: 0.1.13 - - minipass-flush@1.0.5: - dependencies: - minipass: 3.3.6 - - minipass-pipeline@1.2.4: - dependencies: - minipass: 3.3.6 - - minipass-sized@1.0.3: - dependencies: - minipass: 3.3.6 - - minipass@3.3.6: - dependencies: - yallist: 4.0.0 - - minipass@5.0.0: {} - - minipass@7.1.2: {} - - minizlib@2.1.2: - dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - - minizlib@3.1.0: - dependencies: - minipass: 7.1.2 - - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - - mkdirp@1.0.4: {} - - ms@2.1.3: {} - - negotiator@1.0.0: {} - - node-abi@4.24.0: - dependencies: - semver: 7.7.3 - - node-addon-api@1.7.2: - optional: true - - node-api-version@0.2.1: - dependencies: - semver: 7.7.3 - - node-gyp@11.5.0: - dependencies: - env-paths: 2.2.1 - exponential-backoff: 3.1.3 - graceful-fs: 4.2.11 - make-fetch-happen: 14.0.3 - nopt: 8.1.0 - proc-log: 5.0.0 - semver: 7.7.3 - tar: 7.5.2 - tinyglobby: 0.2.15 - which: 5.0.0 - transitivePeerDependencies: - - supports-color - - nopt@8.1.0: - dependencies: - abbrev: 3.0.1 - - normalize-url@6.1.0: {} - - object-keys@1.1.1: - optional: true - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - ora@5.4.1: - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.9.2 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - - p-cancelable@2.1.1: {} - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-map@7.0.4: {} - - package-json-from-dist@1.0.1: {} - - path-is-absolute@1.0.1: {} - - path-key@3.1.1: {} - - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - - pe-library@0.4.1: {} - - pend@1.2.0: {} - - picocolors@1.1.1: {} - - picomatch@4.0.3: {} - playwright-core@1.57.0: {} playwright@1.57.0: @@ -2726,323 +370,6 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - plist@3.1.0: - dependencies: - '@xmldom/xmldom': 0.8.11 - base64-js: 1.5.1 - xmlbuilder: 15.1.1 - - postject@1.0.0-alpha.6: - dependencies: - commander: 9.5.0 - optional: true - prettier@3.8.3: {} - proc-log@5.0.0: {} - - progress@2.0.3: {} - - promise-retry@2.0.1: - dependencies: - err-code: 2.0.3 - retry: 0.12.0 - - pump@3.0.3: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - - punycode@2.3.1: {} - - quick-lru@5.1.1: {} - - read-binary-file-arch@1.0.6: - dependencies: - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - - require-directory@2.1.1: {} - - resedit@1.7.2: - dependencies: - pe-library: 0.4.1 - - resolve-alpn@1.2.1: {} - - responselike@2.0.1: - dependencies: - lowercase-keys: 2.0.0 - - restore-cursor@3.1.0: - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - - retry@0.12.0: {} - - rimraf@2.6.3: - dependencies: - glob: 7.2.3 - - roarr@2.15.4: - dependencies: - boolean: 3.2.0 - detect-node: 2.1.0 - globalthis: 1.0.4 - json-stringify-safe: 5.0.1 - semver-compare: 1.0.0 - sprintf-js: 1.1.3 - optional: true - - safe-buffer@5.2.1: {} - - safer-buffer@2.1.2: {} - - sanitize-filename@1.6.3: - dependencies: - truncate-utf8-bytes: 1.0.2 - - sax@1.4.4: {} - - semver-compare@1.0.0: - optional: true - - semver@5.7.2: {} - - semver@6.3.1: {} - - semver@7.7.3: {} - - serialize-error@7.0.1: - dependencies: - type-fest: 0.13.1 - optional: true - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - signal-exit@3.0.7: {} - - signal-exit@4.1.0: {} - - simple-update-notifier@2.0.0: - dependencies: - semver: 7.7.3 - - slice-ansi@3.0.0: - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - optional: true - - smart-buffer@4.2.0: {} - - socks-proxy-agent@8.0.5: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - socks: 2.8.7 - transitivePeerDependencies: - - supports-color - - socks@2.8.7: - dependencies: - ip-address: 10.1.0 - smart-buffer: 4.2.0 - - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} - - sprintf-js@1.1.3: - optional: true - - ssri@12.0.0: - dependencies: - minipass: 7.1.2 - - stat-mode@1.0.0: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.2 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.2: - dependencies: - ansi-regex: 6.2.2 - - sumchecker@3.0.1: - dependencies: - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - tar@6.2.1: - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - - tar@7.5.2: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.1.0 - yallist: 5.0.0 - - temp-file@3.4.0: - dependencies: - async-exit-hook: 2.0.1 - fs-extra: 10.1.0 - - temp@0.9.4: - dependencies: - mkdirp: 0.5.6 - rimraf: 2.6.3 - - tiny-async-pool@1.3.0: - dependencies: - semver: 5.7.2 - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - - tmp-promise@3.0.3: - dependencies: - tmp: 0.2.5 - - tmp@0.2.5: {} - - truncate-utf8-bytes@1.0.2: - dependencies: - utf8-byte-length: 1.0.5 - - type-fest@0.13.1: - optional: true - - undici-types@6.21.0: {} - - undici-types@7.16.0: {} - - unique-filename@4.0.0: - dependencies: - unique-slug: 5.0.0 - - unique-slug@5.0.0: - dependencies: - imurmurhash: 0.1.4 - - universalify@0.1.2: {} - - universalify@2.0.1: {} - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - utf8-byte-length@1.0.5: {} - - util-deprecate@1.0.2: {} - - verror@1.10.1: - dependencies: - assert-plus: 1.0.0 - core-util-is: 1.0.2 - extsprintf: 1.4.1 - optional: true - - wcwidth@1.0.1: - dependencies: - defaults: 1.0.4 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - which@5.0.0: - dependencies: - isexe: 3.1.1 - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.1.2 - - wrappy@1.0.2: {} - ws@8.18.0: {} - - xmlbuilder@15.1.1: {} - - y18n@5.0.8: {} - - yallist@4.0.0: {} - - yallist@5.0.0: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yauzl@2.10.0: - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - - yocto-queue@0.1.0: {} diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a71c6602..b276a942 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1617,6 +1617,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "oco_ref" version = "0.2.1" @@ -2604,7 +2613,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", diff --git a/rust/crates/core-protocol/src/command.rs b/rust/crates/core-protocol/src/command.rs index 1727d8a2..935a9fe2 100644 --- a/rust/crates/core-protocol/src/command.rs +++ b/rust/crates/core-protocol/src/command.rs @@ -188,3 +188,645 @@ pub struct EmbedBlock { pub struct DeleteBlock { pub block_id: String, } + +// ── CommandContext / when clause ──────────────────────────────────── + +/// CommandContext — 命令执行上下文的 key-value 集合。 +/// +/// 对应 VS Code 的 context key 概念。每个 key 表示当前 UI/workspace/editor 的状态片段。 +/// 菜单项、快捷键、按钮的 enablement 通过 `when` 表达式对 context 求值得到。 +/// +/// 第一阶段支持 key 列表: +/// - `workspace.sourceKind` — 工作区类型 ("local_folder" | "convex" | ...) +/// - `workspace.readonly` — bool,工作区是否只读 +/// - `tree.focusKind` — 当前聚焦的树类型 ("file_tree" | "page_tree" | "none") +/// - `tree.selectionCount` — 当前选中行数 (i64) +/// - `tree.selectionResourceKind` — 选中资源的类型 ("page" | "folder" | "mindmap" | "asset" | ...) +/// - `editor.dirty` — bool,当前编辑器是否有未保存修改 +/// - `editor.hasSelection` — bool,编辑器是否有文本选区 +/// - `ai.canWrite` — bool,当前会话是否允许 AI 写入 +#[derive(Debug, Clone, PartialEq)] +pub struct CommandContext { + /// 工作区来源类型 + pub workspace_source_kind: Option, + /// 工作区是否只读 + pub workspace_readonly: bool, + /// 树焦点类型 + pub tree_focus_kind: Option, + /// 树选择计数 + pub tree_selection_count: i64, + /// 选择资源的 kind + pub tree_selection_resource_kind: Option, + /// 编辑器是否 dirty + pub editor_dirty: bool, + /// 编辑器是否有选区 + pub editor_has_selection: bool, + /// AI 是否可写 + pub ai_can_write: bool, +} + +impl CommandContext { + pub fn new() -> Self { + Self { + workspace_source_kind: None, + workspace_readonly: false, + tree_focus_kind: None, + tree_selection_count: 0, + tree_selection_resource_kind: None, + editor_dirty: false, + editor_has_selection: false, + ai_can_write: true, + } + } + + /// 获取命名 context key 的值(用于 when 表达式求值)。 + pub fn get(&self, key: &str) -> Option { + match key { + "workspace.sourceKind" => self + .workspace_source_kind + .as_deref() + .map(|s| CommandContextValue::String(s.to_string())), + "workspace.readonly" => Some(CommandContextValue::Bool(self.workspace_readonly)), + "tree.focusKind" => self + .tree_focus_kind + .as_deref() + .map(|s| CommandContextValue::String(s.to_string())), + "tree.selectionCount" => Some(CommandContextValue::Number(self.tree_selection_count)), + "tree.selectionResourceKind" => self + .tree_selection_resource_kind + .as_deref() + .map(|s| CommandContextValue::String(s.to_string())), + "editor.dirty" => Some(CommandContextValue::Bool(self.editor_dirty)), + "editor.hasSelection" => Some(CommandContextValue::Bool(self.editor_has_selection)), + "ai.canWrite" => Some(CommandContextValue::Bool(self.ai_can_write)), + _ => None, + } + } + + /// 用 builder 模式设置 workspace 只读。 + pub fn with_workspace_readonly(mut self, readonly: bool) -> Self { + self.workspace_readonly = readonly; + self + } + + /// 用 builder 模式设置 workspace source kind。 + pub fn with_workspace_source_kind(mut self, kind: &str) -> Self { + self.workspace_source_kind = Some(kind.to_string()); + self + } + + /// 用 builder 模式设置 tree focus kind。 + pub fn with_tree_focus_kind(mut self, kind: &str) -> Self { + self.tree_focus_kind = Some(kind.to_string()); + self + } + + /// 用 builder 模式设置 tree selection count。 + pub fn with_tree_selection_count(mut self, count: i64) -> Self { + self.tree_selection_count = count; + self + } + + /// 用 builder 模式设置 selection resource kind。 + pub fn with_tree_selection_resource_kind(mut self, kind: &str) -> Self { + self.tree_selection_resource_kind = Some(kind.to_string()); + self + } + + /// 用 builder 模式设置 editor dirty。 + pub fn with_editor_dirty(mut self, dirty: bool) -> Self { + self.editor_dirty = dirty; + self + } + + /// 用 builder 模式设置 editor has selection。 + pub fn with_editor_has_selection(mut self, has_selection: bool) -> Self { + self.editor_has_selection = has_selection; + self + } + + /// 用 builder 模式设置 ai can write。 + pub fn with_ai_can_write(mut self, can_write: bool) -> Self { + self.ai_can_write = can_write; + self + } +} + +impl Default for CommandContext { + fn default() -> Self { + Self::new() + } +} + +/// 命令上下文中的值类型。 +#[derive(Debug, Clone, PartialEq)] +pub enum CommandContextValue { + Bool(bool), + Number(i64), + String(String), +} + +impl CommandContextValue { + fn as_bool(&self) -> Option { + match self { + CommandContextValue::Bool(b) => Some(*b), + _ => None, + } + } + + fn as_string(&self) -> Option<&str> { + match self { + CommandContextValue::String(s) => Some(s.as_str()), + _ => None, + } + } +} + +/// WhenExprToken — when 表达式的最小 token 类型。 +#[derive(Debug, Clone, PartialEq)] +pub enum WhenExprToken { + /// `key` + Key(String), + /// `!key` + NotKey(String), + /// `key == value` + Equal(String, String), + /// `key != value` + NotEqual(String, String), + /// `&&` + And, + /// `||` + Or, + /// `(` ... `)` + LParen, + RParen, +} + +/// WhenExpr — when 表达式的 AST 节点。 +/// 支持:`key`、`!key`、`key == value`、`key != value`、`&&`、`||`、括号分组。 +#[derive(Debug, Clone, PartialEq)] +pub enum WhenExprNode { + Key(String), + NotKey(String), + Equal(String, String), + NotEqual(String, String), + And(Box, Box), + Or(Box, Box), +} + +/// 简单的递归下降 when 表达式解析器。 +/// 语法: +/// expr = or_expr +/// or_expr = and_expr ("||" and_expr)* +/// and_expr = primary ("&&" primary)* +/// primary = "(" expr ")" | "!" key | "key == value" | "key != value" | key +pub fn parse_when_expr(input: &str) -> Result { + let tokens = tokenize(input)?; + let (node, rest) = parse_or(&tokens)?; + if !rest.is_empty() { + return Err(format!("when 表达式末尾有多余 token: {:?}", rest)); + } + Ok(node) +} + +fn tokenize(input: &str) -> Result, String> { + let mut tokens = Vec::new(); + let mut chars = input.chars().peekable(); + + while let Some(&ch) = chars.peek() { + if ch.is_whitespace() { + chars.next(); + continue; + } + match ch { + '(' => { + chars.next(); + tokens.push(WhenExprToken::LParen); + } + ')' => { + chars.next(); + tokens.push(WhenExprToken::RParen); + } + '!' => { + chars.next(); + // ! 后面跟 key(不能是 "(") + let key = read_identifier(&mut chars)?; + tokens.push(WhenExprToken::NotKey(key)); + } + '|' => { + chars.next(); + if chars.next() == Some('|') { + tokens.push(WhenExprToken::Or); + } else { + return Err("期望 || 运算符,但只看到一个 |".to_string()); + } + } + '&' => { + chars.next(); + if chars.next() == Some('&') { + tokens.push(WhenExprToken::And); + } else { + return Err("期望 && 运算符,但只看到一个 &".to_string()); + } + } + '=' => { + return Err("意外的 = 符号,应为 ==".to_string()); + } + _ if is_ident_start(ch) => { + let key = read_identifier(&mut chars)?; + // 检查后面是否跟着 == 或 != + let mut next_chars = chars.clone(); + let after_ws = next_chars.find(|c| !c.is_whitespace()); + let is_comparison = matches!(after_ws, Some('=') | Some('!')); + if is_comparison { + // 跳过空白 + while chars.peek().map_or(false, |c| c.is_whitespace()) { + chars.next(); + } + let op_start = chars.next(); // = or ! + let op_next = chars.next(); // = + if op_start == Some('=') && op_next == Some('=') { + // == + while chars.peek().map_or(false, |c| c.is_whitespace()) { + chars.next(); + } + let value = read_value(&mut chars)?; + tokens.push(WhenExprToken::Equal(key, value)); + } else if op_start == Some('!') && op_next == Some('=') { + // != + while chars.peek().map_or(false, |c| c.is_whitespace()) { + chars.next(); + } + let value = read_value(&mut chars)?; + tokens.push(WhenExprToken::NotEqual(key, value)); + } else { + return Err(format!("key '{}' 后面的运算符需为 == 或 !=", key)); + } + } else { + tokens.push(WhenExprToken::Key(key)); + } + } + _ => { + return Err(format!("意外的字符: '{}'", ch)); + } + } + } + + Ok(tokens) +} + +fn read_identifier(chars: &mut std::iter::Peekable) -> Result { + let mut ident = String::new(); + while let Some(&ch) = chars.peek() { + if ch.is_alphanumeric() || ch == '_' || ch == '.' { + ident.push(ch); + chars.next(); + } else { + break; + } + } + if ident.is_empty() { + return Err("期望标识符".to_string()); + } + Ok(ident) +} + +fn read_value(chars: &mut std::iter::Peekable) -> Result { + let mut value = String::new(); + // 跳过空白 (已经在调用前跳过了) + // 检查是否有引号 + if chars.peek() == Some(&'"') || chars.peek() == Some(&'\'') { + let quote = chars.next().unwrap(); + while let Some(&ch) = chars.peek() { + if ch == quote { + chars.next(); + return Ok(value); + } + value.push(ch); + chars.next(); + } + return Err("未闭合的引号".to_string()); + } + // 无引号值:读取到下一个空白或运算符为止 + while let Some(&ch) = chars.peek() { + if ch.is_whitespace() || ch == ')' || ch == '|' || ch == '&' { + break; + } + value.push(ch); + chars.next(); + } + if value.is_empty() { + return Err("期望值".to_string()); + } + Ok(value) +} + +fn is_ident_start(ch: char) -> bool { + ch.is_ascii_alphabetic() || ch == '_' +} + +fn parse_or<'a>( + tokens: &'a [WhenExprToken], +) -> Result<(WhenExprNode, &'a [WhenExprToken]), String> { + let (mut left, mut rest) = parse_and(tokens)?; + while let Some(WhenExprToken::Or) = rest.first() { + rest = &rest[1..]; + let (right, new_rest) = parse_and(rest)?; + left = WhenExprNode::Or(Box::new(left), Box::new(right)); + rest = new_rest; + } + Ok((left, rest)) +} + +fn parse_and<'a>( + tokens: &'a [WhenExprToken], +) -> Result<(WhenExprNode, &'a [WhenExprToken]), String> { + let (mut left, mut rest) = parse_primary(tokens)?; + while let Some(WhenExprToken::And) = rest.first() { + rest = &rest[1..]; + let (right, new_rest) = parse_primary(rest)?; + left = WhenExprNode::And(Box::new(left), Box::new(right)); + rest = new_rest; + } + Ok((left, rest)) +} + +fn parse_primary<'a>( + tokens: &'a [WhenExprToken], +) -> Result<(WhenExprNode, &'a [WhenExprToken]), String> { + if let Some(token) = tokens.first() { + match token { + WhenExprToken::Key(key) => Ok((WhenExprNode::Key(key.clone()), &tokens[1..])), + WhenExprToken::NotKey(key) => Ok((WhenExprNode::NotKey(key.clone()), &tokens[1..])), + WhenExprToken::Equal(key, value) => Ok(( + WhenExprNode::Equal(key.clone(), value.clone()), + &tokens[1..], + )), + WhenExprToken::NotEqual(key, value) => Ok(( + WhenExprNode::NotEqual(key.clone(), value.clone()), + &tokens[1..], + )), + WhenExprToken::LParen => { + let (node, rest) = parse_or(&tokens[1..])?; + if let Some(WhenExprToken::RParen) = rest.first() { + Ok((node, &rest[1..])) + } else { + Err("缺少闭合的 ')'".to_string()) + } + } + _ => Err(format!("期望表达式,得到 {:?}", token)), + } + } else { + Err("期望表达式,但没有更多 token".to_string()) + } +} + +/// 对 `CommandContext` 求值一个 when 表达式。 +pub fn evaluate_when(ctx: &CommandContext, expr: &WhenExprNode) -> bool { + match expr { + WhenExprNode::Key(key) => ctx.get(key).map_or(false, |v| match v { + CommandContextValue::Bool(b) => b, + CommandContextValue::Number(n) => n != 0, + CommandContextValue::String(s) => !s.is_empty(), + }), + WhenExprNode::NotKey(key) => !ctx.get(key).map_or(false, |v| match v { + CommandContextValue::Bool(b) => b, + CommandContextValue::Number(n) => n != 0, + CommandContextValue::String(s) => !s.is_empty(), + }), + WhenExprNode::Equal(key, expected) => ctx.get(key).map_or(false, |v| { + v.as_string().map_or(false, |s| s == expected) + || expected + .parse::() + .ok() + .is_some_and(|expected_number| { + matches!(v, CommandContextValue::Number(n) if n == expected_number) + }) + || v.as_bool().map_or(false, |b| { + expected == "true" && b || expected == "false" && !b + }) + }), + WhenExprNode::NotEqual(key, expected) => ctx.get(key).map_or(true, |v| { + v.as_string().map_or(true, |s| s != expected) + && expected + .parse::() + .ok() + .is_none_or(|expected_number| { + !matches!(v, CommandContextValue::Number(n) if n == expected_number) + }) + && v.as_bool().map_or(true, |b| { + !(expected == "true" && b || expected == "false" && !b) + }) + }), + WhenExprNode::And(left, right) => evaluate_when(ctx, left) && evaluate_when(ctx, right), + WhenExprNode::Or(left, right) => evaluate_when(ctx, left) || evaluate_when(ctx, right), + } +} + +/// 解析并求值一条 when 表达式字符串。 +pub fn check_when(ctx: &CommandContext, when_expr: &str) -> Result { + let expr = parse_when_expr(when_expr)?; + Ok(evaluate_when(ctx, &expr)) +} + +/// 检查一个命令在指定上下文中是否应 enabled。 +/// 如果 when 表达式为 None 或空,默认返回 true。 +pub fn is_command_enabled(ctx: &CommandContext, when: Option<&str>) -> bool { + match when { + None | Some("") => true, + Some(expr) => check_when(ctx, expr).unwrap_or(true), + } +} + +#[cfg(test)] +mod command_context_tests { + use super::*; + + #[test] + fn command_context_when_clause() { + let ctx = CommandContext::new() + .with_workspace_readonly(false) + .with_workspace_source_kind("local_folder") + .with_tree_focus_kind("file_tree") + .with_tree_selection_count(1) + .with_tree_selection_resource_kind("page") + .with_editor_dirty(false) + .with_ai_can_write(true); + + // 基础 key 表达式(bool) + assert!(check_when(&ctx, "ai.canWrite").unwrap()); + assert!(!check_when(&ctx, "editor.dirty").unwrap()); + assert!(!check_when(&ctx, "workspace.readonly").unwrap()); + + // !key + assert!(check_when(&ctx, "!editor.dirty").unwrap()); + assert!(!check_when(&ctx, "!ai.canWrite").unwrap()); + + // key == value + assert!(check_when(&ctx, "tree.focusKind == file_tree").unwrap()); + assert!(!check_when(&ctx, "tree.focusKind == page_tree").unwrap()); + + // key != value + assert!(check_when(&ctx, "tree.focusKind != page_tree").unwrap()); + assert!(!check_when(&ctx, "tree.focusKind != file_tree").unwrap()); + + // && 组合 + assert!(check_when(&ctx, "ai.canWrite && !workspace.readonly").unwrap()); + assert!(!check_when(&ctx, "ai.canWrite && workspace.readonly").unwrap()); + assert!(!check_when(&ctx, "editor.dirty && ai.canWrite").unwrap()); + + // || 组合 + assert!(check_when(&ctx, "editor.dirty || ai.canWrite").unwrap()); + assert!(!check_when(&ctx, "editor.dirty || workspace.readonly").unwrap()); + + // 括号分组 + assert!(check_when(&ctx, "(ai.canWrite && !workspace.readonly) || editor.dirty").unwrap()); + assert!(!check_when(&ctx, "editor.dirty && (ai.canWrite || workspace.readonly)").unwrap()); + } + + #[test] + fn command_context_disables_write_commands_for_readonly_workspace() { + let write_commands = vec![ + ( + "tree.node.create", + Some("!workspace.readonly && ai.canWrite"), + ), + ("tree.node.rename", Some("!workspace.readonly")), + ("tree.node.delete", Some("!workspace.readonly")), + ("tree.node.move", Some("!workspace.readonly")), + ]; + + let ctx_readonly = CommandContext::new() + .with_workspace_readonly(true) + .with_ai_can_write(true); + + let ctx_writable = CommandContext::new() + .with_workspace_readonly(false) + .with_ai_can_write(true); + + for (_name, when) in &write_commands { + // 只读上下文应禁用所有写命令 + assert!( + !is_command_enabled(&ctx_readonly, *when), + "只读上下文不应启用命令 {:?}", + _name + ); + // 可写上下文应启用 + assert!( + is_command_enabled(&ctx_writable, *when), + "可写上下文应启用命令 {:?}", + _name + ); + } + } + + #[test] + fn command_context_filters_resource_commands_by_kind() { + let folder_commands = vec![ + ("tree.node.create", Some("tree.focusKind == file_tree")), + ( + "tree.folder.rename", + Some("tree.selectionResourceKind == folder"), + ), + ]; + let page_commands = vec![ + ("page.open", Some("tree.selectionResourceKind == page")), + ("page.delete", Some("tree.selectionResourceKind == page")), + ]; + let mindmap_commands = vec![( + "mindmap.open", + Some("tree.selectionResourceKind == mindmap"), + )]; + + let ctx_folder = CommandContext::new() + .with_tree_focus_kind("file_tree") + .with_tree_selection_resource_kind("folder") + .with_tree_selection_count(1); + + let ctx_page = CommandContext::new() + .with_tree_focus_kind("file_tree") + .with_tree_selection_resource_kind("page") + .with_tree_selection_count(1); + + let ctx_mindmap = CommandContext::new() + .with_tree_focus_kind("file_tree") + .with_tree_selection_resource_kind("mindmap") + .with_tree_selection_count(1); + + // folder 上下文 + for (_name, when) in &folder_commands { + assert!( + is_command_enabled(&ctx_folder, *when), + "folder ctx 应启用命令 {:?}", + _name + ); + } + for (_name, when) in &page_commands { + assert!( + !is_command_enabled(&ctx_folder, *when), + "folder ctx 不应启用 page 命令 {:?}", + _name + ); + } + + // page 上下文 + for (_name, when) in &page_commands { + assert!( + is_command_enabled(&ctx_page, *when), + "page ctx 应启用 page 命令 {:?}", + _name + ); + } + assert!( + !is_command_enabled(&ctx_page, Some("tree.selectionResourceKind == mindmap")), + "page ctx 不应启用 mindmap 命令" + ); + + // mindmap 上下文 + for (_name, when) in &mindmap_commands { + assert!( + is_command_enabled(&ctx_mindmap, *when), + "mindmap ctx 应启用 mindmap 命令 {:?}", + _name + ); + } + } + + #[test] + fn command_context_default_is_allow() { + let ctx = CommandContext::new(); + // 没有 when 表达式 → true + assert!(is_command_enabled(&ctx, None)); + assert!(is_command_enabled(&ctx, Some(""))); + } + + #[test] + fn command_context_number_comparison() { + let ctx = CommandContext::new().with_tree_selection_count(3); + + // 数字值在 bool 语境下:非零为 true + assert!(check_when(&ctx, "tree.selectionCount").unwrap()); + assert!(check_when(&ctx, "tree.selectionCount == 3").unwrap()); + assert!(check_when(&ctx, "tree.selectionCount != 1").unwrap()); + assert!(!check_when(&ctx, "tree.selectionCount == 1").unwrap()); + let ctx_zero = CommandContext::new().with_tree_selection_count(0); + assert!(!check_when(&ctx_zero, "tree.selectionCount").unwrap()); + assert!(check_when(&ctx_zero, "tree.selectionCount == 0").unwrap()); + } + + #[test] + fn command_context_parse_error_returns_default() { + let ctx = CommandContext::new(); + // 解析错误 → is_command_enabled 返回 true(默认允许) + assert!(is_command_enabled(&ctx, Some("invalid syntax &&&"))); + } + + #[test] + fn command_context_ai_can_write_gates_ai_actions() { + let ctx_can_write = CommandContext::new().with_ai_can_write(true); + let ctx_readonly = CommandContext::new().with_ai_can_write(false); + + assert!(is_command_enabled(&ctx_can_write, Some("ai.canWrite"))); + assert!(!is_command_enabled(&ctx_readonly, Some("ai.canWrite"))); + } +} diff --git a/rust/crates/core-protocol/src/lib.rs b/rust/crates/core-protocol/src/lib.rs index 363f3b01..f000fdc6 100644 --- a/rust/crates/core-protocol/src/lib.rs +++ b/rust/crates/core-protocol/src/lib.rs @@ -15,11 +15,12 @@ pub use ai::{ AiShareContext, AiStructuredWriteKind, AiStructuredWriteResult, AiToolCall, }; pub use command::{ - CommandEnvelope, CopyTreeDocumentPages, CreateDocumentPage, CreatePage, CreateWorkspace, - DeleteBlock, DeleteDocumentPage, DuplicateDocumentPage, EmbedBlock, InsertBlock, MoveBlock, - MoveDocumentPage, PatchBlock, PatchPageBlock, PutMindmap, ReplaceMediaAssetStorage, - RestoreDocumentPage, SavePageContent, UpdateBlock, UpdatePageOptions, UpdatePageStats, - UpdatePageTitle, + check_when, evaluate_when, is_command_enabled, parse_when_expr, CommandContext, + CommandContextValue, CommandEnvelope, CopyTreeDocumentPages, CreateDocumentPage, CreatePage, + CreateWorkspace, DeleteBlock, DeleteDocumentPage, DuplicateDocumentPage, EmbedBlock, + InsertBlock, MoveBlock, MoveDocumentPage, PatchBlock, PatchPageBlock, PutMindmap, + ReplaceMediaAssetStorage, RestoreDocumentPage, SavePageContent, UpdateBlock, UpdatePageOptions, + UpdatePageStats, UpdatePageTitle, WhenExprNode, WhenExprToken, }; pub use common::{ ActorPayload, AffectedObject, ErrorDetail, ErrorPayload, OkPayload, RequestMeta, ResponseMeta, diff --git a/rust/crates/mnote-web/Cargo.toml b/rust/crates/mnote-web/Cargo.toml index a0c3f371..ca21dfb4 100644 --- a/rust/crates/mnote-web/Cargo.toml +++ b/rust/crates/mnote-web/Cargo.toml @@ -27,4 +27,4 @@ tower = "0.5" base64 = "0.22" comrak = { version = "0.52", default-features = false } notify = "8.2.0" -time = { version = "0.3", features = ["formatting"] } +time = { version = "0.3", features = ["formatting", "local-offset"] } diff --git a/rust/crates/mnote-web/src/app.rs b/rust/crates/mnote-web/src/app.rs index 36ea002c..d0b1bf5c 100644 --- a/rust/crates/mnote-web/src/app.rs +++ b/rust/crates/mnote-web/src/app.rs @@ -1,4 +1,5 @@ use crate::acp_runtime::AcpRuntimeManager; +use crate::document_buffer_store::BufferStore; use crate::editor_actor::EditorRuntimeActor; use crate::local_folder_watcher_registry::LocalFolderWatcherRegistry; use crate::middleware::request_context::inject_request_context; @@ -144,6 +145,7 @@ pub struct AppState { pub block_delta_tx: broadcast::Sender, pub stream_delta_tx: broadcast::Sender, pub acp_runtime: Arc, + pub buffer_store: BufferStore, } impl AppState { @@ -152,13 +154,15 @@ impl AppState { let (stream_delta_tx, _) = broadcast::channel(256); let actor = EditorRuntimeActor::new(); actor.set_block_delta_tx(block_delta_tx.clone()); + let buffer_store = BufferStore::new(); Self { config: Arc::new(config), - local_folder_watcher_registry: LocalFolderWatcherRegistry::new(), + local_folder_watcher_registry: LocalFolderWatcherRegistry::new(buffer_store.clone()), editor_actor: actor, block_delta_tx, stream_delta_tx, acp_runtime: Arc::new(AcpRuntimeManager::from_env()), + buffer_store, } } diff --git a/rust/crates/mnote-web/src/context.rs b/rust/crates/mnote-web/src/context.rs index cce9c6c0..1b3957ad 100644 --- a/rust/crates/mnote-web/src/context.rs +++ b/rust/crates/mnote-web/src/context.rs @@ -82,6 +82,7 @@ impl RequestContext { cookie_header: header_value(headers, axum::http::header::COOKIE.as_str()), actor_id: header_value(headers, HEADER_ACTOR_ID) .or_else(|| cookie_value(headers, COOKIE_ACTOR_ID)) + .and_then(|value| stable_actor_id(&value)) .unwrap_or_else(|| "anonymous".into()), actor_type: header_value(headers, HEADER_ACTOR_TYPE) .or_else(|| cookie_value(headers, COOKIE_ACTOR_TYPE)) @@ -117,6 +118,22 @@ impl RequestContext { } } +pub(crate) fn stable_actor_id(value: &str) -> Option { + let trimmed = value.trim(); + if trimmed.is_empty() { + return None; + } + let stable = trimmed + .split_once('|') + .map(|(actor_id, _)| actor_id.trim()) + .unwrap_or(trimmed); + if stable.is_empty() { + None + } else { + Some(stable.to_string()) + } +} + fn header_or_generated(headers: &HeaderMap, key: &str, prefix: &str) -> String { header_value(headers, key).unwrap_or_else(|| generate_id(prefix)) } @@ -181,6 +198,23 @@ mod tests { assert_eq!(context.auth.actor_id, "user_demo"); } + #[test] + fn request_context_normalizes_pipe_separated_actor_id() { + let mut headers = HeaderMap::new(); + headers.insert( + HEADER_ACTOR_ID, + HeaderValue::from_static("user_demo|session_123"), + ); + + let context = RequestContext::from_http_parts( + &Method::GET, + &"/".parse::().expect("uri"), + &headers, + ); + + assert_eq!(context.auth.actor_id, "user_demo"); + } + #[test] fn request_context_falls_back_to_actor_cookies() { let mut headers = HeaderMap::new(); @@ -198,6 +232,26 @@ mod tests { assert_eq!(context.auth.actor_id, "user_cookie"); assert_eq!(context.auth.actor_type, "user"); } + + #[test] + fn request_context_normalizes_pipe_separated_actor_cookie() { + let mut headers = HeaderMap::new(); + headers.insert( + axum::http::header::COOKIE, + HeaderValue::from_static( + "mnote_actor_id=user_cookie|session_abc; mnote_actor_type=user", + ), + ); + + let context = RequestContext::from_http_parts( + &Method::GET, + &"/".parse::().expect("uri"), + &headers, + ); + + assert_eq!(context.auth.actor_id, "user_cookie"); + assert_eq!(context.auth.actor_type, "user"); + } } fn cookie_value(headers: &HeaderMap, name: &str) -> Option { diff --git a/rust/crates/mnote-web/src/document_buffer_store.rs b/rust/crates/mnote-web/src/document_buffer_store.rs new file mode 100644 index 00000000..e2cb7f4b --- /dev/null +++ b/rust/crates/mnote-web/src/document_buffer_store.rs @@ -0,0 +1,452 @@ +//! BufferStore — 运行时 DocumentBuffer 管理器 +//! +//! 职责: +//! - 持有所有已打开文档的 `DocumentBuffer` 实例 +//! - 以 `ObjectWorkspacePath` 的稳定复合身份作为 buffer key +//! - tiptap 保存链、AI 写入、外部 watcher 均通过此模块仲裁状态 +//! +//! 不是持久化结构,纯内存运行时。 + +use core_protocol::{ + DocBufferDirtyState, DocumentBuffer, KernelObjectIdentity, KernelObjectKind, + ObjectWorkspacePath, WorkspaceSourceKind, +}; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// BufferKey — 从 ObjectWorkspacePath 导出的稳定复合 key。 +/// +/// 由 workspace_id + source_kind + root_uri + relative_path + object_identity.document_id 组成。 +/// 同一 .md 文件由 tiptap、AI、外部 watcher 访问时落到同一 key。 +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct BufferKey { + pub workspace_id: String, + pub source_kind: String, + pub root_uri: String, + pub relative_path: String, + pub document_id: Option, +} + +impl BufferKey { + /// 从 ObjectWorkspacePath 构建 BufferKey。 + pub fn from_workspace_path(path: &ObjectWorkspacePath) -> Self { + Self { + workspace_id: path.workspace_id.clone(), + source_kind: format!("{:?}", path.source_kind), + root_uri: path.root_uri.clone(), + relative_path: path.relative_path.clone(), + document_id: path.object_identity.document_id.clone(), + } + } + + /// 从文档基本属性构建 BufferKey。 + pub fn from_parts( + workspace_id: &str, + source_kind: &str, + root_uri: &str, + relative_path: &str, + document_id: Option, + ) -> Self { + Self { + workspace_id: workspace_id.to_string(), + source_kind: source_kind.to_string(), + root_uri: root_uri.to_string(), + relative_path: relative_path.to_string(), + document_id, + } + } +} + +/// BufferStore — 运行时文档缓冲区管理器。 +/// +/// 每个 `AppState` 有一个实例,所有文档打开/保存/冲突检测共享同一状态模型。 +#[derive(Debug, Clone)] +pub struct BufferStore { + inner: Arc>, +} + +#[derive(Debug)] +struct BufferStoreInner { + buffers: HashMap, +} + +impl BufferStore { + pub fn new() -> Self { + Self { + inner: Arc::new(RwLock::new(BufferStoreInner { + buffers: HashMap::new(), + })), + } + } + + /// 获取或创建 buffer。 + /// + /// 如果该 key 已有 buffer,直接返回。否则用 `ObjectWorkspacePath` 创建一个 Clean buffer。 + pub fn get_or_create(&self, path: &ObjectWorkspacePath) -> DocumentBuffer { + let key = BufferKey::from_workspace_path(path); + let mut inner = self.inner.write().expect("BufferStore lock"); + if let Some(buf) = inner.buffers.get(&key) { + return buf.clone(); + } + let now_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; + let buf = DocumentBuffer { + workspace_path: path.clone(), + file_version: None, + base_content_hash: None, + current_content_hash: None, + dirty_state: DocBufferDirtyState::Clean, + last_loaded_at: Some(now_ms), + last_saved_at: Some(now_ms), + external_actor: None, + }; + inner.buffers.insert(key, buf.clone()); + buf + } + + /// 按 key 获取 buffer(不创建)。 + pub fn get(&self, key: &BufferKey) -> Option { + let inner = self.inner.read().expect("BufferStore lock"); + inner.buffers.get(key).cloned() + } + + /// 更新 buffer。 + pub fn update(&self, key: &BufferKey, buffer: DocumentBuffer) { + let mut inner = self.inner.write().expect("BufferStore lock"); + inner.buffers.insert(key.clone(), buffer); + } + + /// 通过 ObjectWorkspacePath 获取 buffer。不存在则返回 None。 + pub fn get_by_path(&self, path: &ObjectWorkspacePath) -> Option { + let key = BufferKey::from_workspace_path(path); + self.get(&key) + } + + /// 标记保存成功:更新 file_version、base_content_hash、清除 dirty 状态。 + pub fn mark_saved( + &self, + path: &ObjectWorkspacePath, + file_version: String, + content_hash: String, + ) -> Option { + let key = BufferKey::from_workspace_path(path); + let mut inner = self.inner.write().expect("BufferStore lock"); + if let Some(buf) = inner.buffers.get_mut(&key) { + buf.mark_saved(file_version, content_hash); + Some(buf.clone()) + } else { + None + } + } + + /// 标记外部修改:如果 buffer 是 Clean → ExternalModified;是 Dirty → Stale。 + pub fn mark_external_modified( + &self, + path: &ObjectWorkspacePath, + actor: Option, + ) -> Option { + let key = BufferKey::from_workspace_path(path); + let mut inner = self.inner.write().expect("BufferStore lock"); + if let Some(buf) = inner.buffers.get_mut(&key) { + buf.mark_external_modified(actor); + Some(buf.clone()) + } else { + None + } + } + + /// 标记 buffer 为 dirty(编辑器内容已修改)。 + pub fn mark_dirty( + &self, + path: &ObjectWorkspacePath, + content_hash: String, + ) -> Option { + let key = BufferKey::from_workspace_path(path); + let mut inner = self.inner.write().expect("BufferStore lock"); + if let Some(buf) = inner.buffers.get_mut(&key) { + buf.mark_dirty(content_hash); + Some(buf.clone()) + } else { + None + } + } + + /// 获取或创建 buffer 时设置 file_version 和 base_content_hash(从 aggregate 加载后调用)。 + pub fn init_buffer( + &self, + path: &ObjectWorkspacePath, + file_version: Option, + base_content_hash: Option, + ) -> DocumentBuffer { + let mut buf = self.get_or_create(path); + if file_version.is_some() || base_content_hash.is_some() { + if let Some(fv) = file_version { + buf.file_version = Some(fv); + } + if let Some(ch) = base_content_hash { + buf.base_content_hash = Some(ch); + } + let key = BufferKey::from_workspace_path(path); + self.update(&key, buf.clone()); + } + buf + } + + /// 返回当前 buffer 数量(仅用于测试和监控)。 + pub fn buffer_count(&self) -> usize { + let inner = self.inner.read().expect("BufferStore lock"); + inner.buffers.len() + } + + /// 清除所有 buffers(测试用途)。 + pub fn clear(&self) { + let mut inner = self.inner.write().expect("BufferStore lock"); + inner.buffers.clear(); + } + + /// 返回当前所有 buffers 的拷贝(测试/监控用途)。 + pub fn all_buffers(&self) -> Vec { + let inner = self.inner.read().expect("BufferStore lock"); + inner.buffers.values().cloned().collect() + } +} + +impl Default for BufferStore { + fn default() -> Self { + Self::new() + } +} + +// ── helpers ────────────────────────────────────────────────────────── + +/// 从 ObjectWorkspacePath 构建 buffer key 字符串(用于日志/调试)。 +pub fn buffer_key_string(path: &ObjectWorkspacePath) -> String { + format!( + "{}:{}:{}:{}", + path.workspace_id, + path.root_uri, + path.relative_path, + path.object_identity.document_id.as_deref().unwrap_or("_") + ) +} + +/// 从本地文件夹写入上下文的参数构建 ObjectWorkspacePath。 +/// +/// 在 save_local_markdown_page、watcher event 和 Hermes 写入链中统一使用此函数构造路径。 +pub fn build_local_folder_workspace_path( + workspace_id: &str, + root_uri: &str, + relative_path: &str, + document_id: &str, +) -> ObjectWorkspacePath { + ObjectWorkspacePath { + workspace_id: workspace_id.to_string(), + source_kind: WorkspaceSourceKind::LocalFolder, + root_uri: root_uri.to_string(), + relative_path: relative_path.to_string(), + object_identity: KernelObjectIdentity { + object_kind: KernelObjectKind::Page, + document_id: Some(document_id.to_string()), + block_id: None, + asset_id: None, + }, + resource_kind: Some("document".to_string()), + } +} + +// ── tests ──────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use core_protocol::{ + DocBufferDirtyState, DocumentBuffer, KernelObjectIdentity, KernelObjectKind, + ObjectWorkspacePath, WorkspaceSourceKind, + }; + + fn make_test_workspace_path( + workspace_id: &str, + relative_path: &str, + document_id: &str, + ) -> ObjectWorkspacePath { + ObjectWorkspacePath { + workspace_id: workspace_id.to_string(), + source_kind: WorkspaceSourceKind::LocalFolder, + root_uri: "file:///tmp/test-root".to_string(), + relative_path: relative_path.to_string(), + object_identity: KernelObjectIdentity { + object_kind: KernelObjectKind::Page, + document_id: Some(document_id.to_string()), + block_id: None, + asset_id: None, + }, + resource_kind: Some("document".to_string()), + } + } + + fn make_fresh_buffer(path: &ObjectWorkspacePath) -> DocumentBuffer { + DocumentBuffer { + workspace_path: path.clone(), + file_version: Some("v1".to_string()), + base_content_hash: Some("sha256:base".to_string()), + current_content_hash: None, + dirty_state: DocBufferDirtyState::Clean, + last_loaded_at: Some(1_700_000_000_000i64), + last_saved_at: Some(1_700_000_000_001i64), + external_actor: None, + } + } + + #[test] + fn document_buffer_reuses_workspace_path_key() { + let store = BufferStore::new(); + let path_a = make_test_workspace_path("ws_1", "doc.md", "local-md:doc.md"); + let path_b = make_test_workspace_path("ws_1", "doc.md", "local-md:doc.md"); + + // 同一个路径应得同一个 buffer + let buf1 = store.get_or_create(&path_a); + let buf2 = store.get_or_create(&path_b); + + assert_eq!( + buf1.workspace_path.relative_path, + buf2.workspace_path.relative_path + ); + assert_eq!( + BufferKey::from_workspace_path(&path_a), + BufferKey::from_workspace_path(&path_b) + ); + } + + #[test] + fn document_buffer_marks_external_modified_for_clean_buffer() { + let store = BufferStore::new(); + let path = make_test_workspace_path("ws_1", "doc.md", "local-md:doc.md"); + let mut buf = make_fresh_buffer(&path); + let key = BufferKey::from_workspace_path(&path); + store.update(&key, buf.clone()); + + buf.mark_external_modified(Some("external-editor".into())); + store.update(&key, buf); + + let result = store.get(&key).expect("buffer should exist"); + assert_eq!(result.dirty_state, DocBufferDirtyState::ExternalModified); + assert_eq!(result.external_actor.as_deref(), Some("external-editor")); + } + + #[test] + fn document_buffer_marks_stale_for_dirty_buffer() { + let store = BufferStore::new(); + let path = make_test_workspace_path("ws_1", "doc.md", "local-md:doc.md"); + let mut buf = make_fresh_buffer(&path); + let key = BufferKey::from_workspace_path(&path); + + // 先 mark_dirty + buf.mark_dirty("sha256:dirty".into()); + store.update(&key, buf); + + // 外部修改 → 应变为 Stale + let result = store.mark_external_modified(&path, Some("external-editor".into())); + assert!(result.is_some()); + assert_eq!( + result.as_ref().unwrap().dirty_state, + DocBufferDirtyState::Stale + ); + assert_eq!( + result.as_ref().unwrap().external_actor.as_deref(), + Some("external-editor") + ); + } + + #[test] + fn document_buffer_mark_saved_after_page_body_write() { + let store = BufferStore::new(); + let path = make_test_workspace_path("ws_1", "doc.md", "local-md:doc.md"); + let mut buf = make_fresh_buffer(&path); + let key = BufferKey::from_workspace_path(&path); + buf.mark_dirty("sha256:dirty".into()); + store.update(&key, buf); + + // 保存后应变为 Clean,更新 file_version + let result = store.mark_saved(&path, "v2".into(), "sha256:saved".into()); + assert!(result.is_some()); + let buf = result.unwrap(); + assert_eq!(buf.dirty_state, DocBufferDirtyState::Clean); + assert_eq!(buf.file_version.as_deref(), Some("v2")); + assert_eq!(buf.base_content_hash.as_deref(), Some("sha256:saved")); + assert!(!buf.is_dirty()); + } + + #[test] + fn document_buffer_different_paths_different_keys() { + let store = BufferStore::new(); + let path_a = make_test_workspace_path("ws_1", "doc-a.md", "local-md:doc-a.md"); + let path_b = make_test_workspace_path("ws_2", "doc-b.md", "local-md:doc-b.md"); + + let key_a = BufferKey::from_workspace_path(&path_a); + let key_b = BufferKey::from_workspace_path(&path_b); + + assert_ne!(key_a, key_b); + + let buf1 = store.get_or_create(&path_a); + let buf2 = store.get_or_create(&path_b); + + assert_ne!( + buf1.workspace_path.workspace_id, + buf2.workspace_path.workspace_id + ); + } + + #[test] + fn document_buffer_reuses_across_multiple_get_or_create_calls() { + let store = BufferStore::new(); + let path = make_test_workspace_path("ws_1", "doc.md", "local-md:doc.md"); + + let buf1 = store.get_or_create(&path); + let buf2 = store.get_or_create(&path); + + // 同一个路径的 buffer 应该共享同一 key + assert_eq!( + BufferKey::from_workspace_path(&buf1.workspace_path), + BufferKey::from_workspace_path(&buf2.workspace_path) + ); + assert_eq!(store.buffer_count(), 1); + } + + #[test] + fn document_buffer_init_buffer_sets_file_version() { + let store = BufferStore::new(); + let path = make_test_workspace_path("ws_1", "doc.md", "local-md:doc.md"); + + let buf = store.init_buffer(&path, Some("v3".into()), Some("sha256:v3base".into())); + assert_eq!(buf.file_version.as_deref(), Some("v3")); + assert_eq!(buf.base_content_hash.as_deref(), Some("sha256:v3base")); + assert_eq!(buf.dirty_state, DocBufferDirtyState::Clean); + + // 再次获取应保留状态 + let buf2 = store.get_or_create(&path); + assert_eq!(buf2.file_version.as_deref(), Some("v3")); + assert_eq!(store.buffer_count(), 1); + } + + #[test] + fn document_buffer_mark_dirty_and_saved_round_trip() { + let store = BufferStore::new(); + let path = make_test_workspace_path("ws_1", "doc-cycle.md", "local-md:doc-cycle.md"); + + // init → mark_dirty → mark_saved → 应回到 Clean + let _ = store.init_buffer(&path, Some("v1".into()), Some("sha256:base".into())); + let dirty = store.mark_dirty(&path, "sha256:dirty".into()); + assert!(dirty.is_some()); + assert_eq!(dirty.unwrap().dirty_state, DocBufferDirtyState::Dirty); + + let saved = store.mark_saved(&path, "v2".into(), "sha256:saved".into()); + assert!(saved.is_some()); + let buf = saved.unwrap(); + assert_eq!(buf.dirty_state, DocBufferDirtyState::Clean); + assert_eq!(buf.file_version.as_deref(), Some("v2")); + assert_eq!(buf.base_content_hash.as_deref(), Some("sha256:saved")); + } +} diff --git a/rust/crates/mnote-web/src/hermes_tools/doc.rs b/rust/crates/mnote-web/src/hermes_tools/doc.rs index 9985bf11..38c4850b 100644 --- a/rust/crates/mnote-web/src/hermes_tools/doc.rs +++ b/rust/crates/mnote-web/src/hermes_tools/doc.rs @@ -1708,6 +1708,7 @@ pub async fn doc_markdown_edit( content: next_content, editor_source: Some("mnote.doc.markdown_edit".into()), }, + Some(&state.buffer_store), )?; return Ok(json!({ "ok": true, diff --git a/rust/crates/mnote-web/src/hermes_tools/page.rs b/rust/crates/mnote-web/src/hermes_tools/page.rs index ba7f3ea2..8adaf874 100644 --- a/rust/crates/mnote-web/src/hermes_tools/page.rs +++ b/rust/crates/mnote-web/src/hermes_tools/page.rs @@ -233,6 +233,7 @@ async fn page_command( content, editor_source: Some("mnote.page.save".into()), }, + Some(&state.buffer_store), )? } "page.head.updateTitle" => { diff --git a/rust/crates/mnote-web/src/lib.rs b/rust/crates/mnote-web/src/lib.rs index 0a993cf1..d5fe40fe 100644 --- a/rust/crates/mnote-web/src/lib.rs +++ b/rust/crates/mnote-web/src/lib.rs @@ -7,6 +7,7 @@ pub mod acp_session_manager; pub mod acp_types; pub mod app; pub mod context; +pub mod document_buffer_store; pub mod editor_actor; pub mod error; pub mod hermes_tools; diff --git a/rust/crates/mnote-web/src/local_folder_watcher_registry.rs b/rust/crates/mnote-web/src/local_folder_watcher_registry.rs index 81ec2b2b..7ee6472b 100644 --- a/rust/crates/mnote-web/src/local_folder_watcher_registry.rs +++ b/rust/crates/mnote-web/src/local_folder_watcher_registry.rs @@ -1,3 +1,4 @@ +use crate::document_buffer_store::BufferStore; use crate::routes::{local_workspace_id_from_root_uri, refresh_local_search_index_for_path}; use notify::event::ModifyKind; use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; @@ -12,6 +13,7 @@ use tokio::sync::{broadcast, mpsc, oneshot}; #[derive(Clone)] pub struct LocalFolderWatcherRegistry { inner: Arc, + buffer_store: BufferStore, } impl std::fmt::Debug for LocalFolderWatcherRegistry { @@ -23,11 +25,12 @@ impl std::fmt::Debug for LocalFolderWatcherRegistry { } impl LocalFolderWatcherRegistry { - pub fn new() -> Self { + pub fn new(buffer_store: BufferStore) -> Self { Self { inner: Arc::new(LocalFolderWatcherRegistryInner { entries: Mutex::new(HashMap::new()), }), + buffer_store, } } @@ -36,7 +39,10 @@ impl LocalFolderWatcherRegistry { canonical_root: &Path, ) -> Result { let key = canonical_root_uri(canonical_root); - let channel = self.inner.get_or_create_channel(&key, canonical_root)?; + let buffer_store = self.buffer_store.clone(); + let channel = self + .inner + .get_or_create_channel(&key, canonical_root, buffer_store)?; channel.subscriber_count.fetch_add(1, Ordering::SeqCst); Ok(LocalFolderWatcherSubscription { receiver: channel.sender.subscribe(), @@ -63,6 +69,7 @@ impl LocalFolderWatcherRegistryInner { &self, key: &str, canonical_root: &Path, + buffer_store: BufferStore, ) -> Result, String> { if let Some(existing) = self .entries @@ -76,7 +83,7 @@ impl LocalFolderWatcherRegistryInner { let channel = Arc::new(LocalFolderWatchChannel::new( key.to_string(), - spawn_local_folder_watcher(key, canonical_root.to_path_buf())?, + spawn_local_folder_watcher(key, canonical_root.to_path_buf(), buffer_store)?, )); let mut entries = self.entries.lock().expect("registry lock"); @@ -157,6 +164,7 @@ impl Drop for LocalFolderWatcherSubscriptionGuard { fn spawn_local_folder_watcher( root_uri: &str, canonical_root: PathBuf, + buffer_store: BufferStore, ) -> Result<(broadcast::Sender, oneshot::Sender<()>), String> { let (event_sender, mut event_receiver) = mpsc::unbounded_channel::>(); let mut watcher = RecommendedWatcher::new( @@ -174,6 +182,7 @@ fn spawn_local_folder_watcher( let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>(); let sender_for_task = sender.clone(); let root_uri_for_task = root_uri.to_string(); + let buffer_store_for_task = buffer_store.clone(); tokio::spawn(async move { let _watcher = watcher; loop { @@ -206,11 +215,29 @@ fn spawn_local_folder_watcher( if !is_markdown_path(&path) { continue; } + + // 外部文件变更→更新 BufferStore + let document_id = + format!("local-md:{}", encode_local_id_segment(&relative_path)); + if let Ok(ws_id) = + local_workspace_id_from_root_uri(&root_uri_for_task) + { + let ws_path = + crate::document_buffer_store::build_local_folder_workspace_path( + &ws_id, + &root_uri_for_task, + &relative_path, + &document_id, + ); + buffer_store_for_task + .mark_external_modified(&ws_path, Some("external-editor".into())); + } + let payload = json!({ "sourceKind": "local_folder", "rootUri": root_uri_for_task, "relativePath": relative_path, - "documentId": format!("local-md:{}", encode_local_id_segment(&relative_path)), + "documentId": document_id, "eventKind": format!("{:?}", event.kind), "revision": event_revision(&path), }); @@ -319,6 +346,7 @@ mod tests { is_local_search_index_path, refresh_local_search_index_for_event, should_emit_event_kind, LocalFolderWatcherRegistry, }; + use crate::document_buffer_store::BufferStore; use notify::event::{AccessKind, CreateKind, DataChange, ModifyKind}; use notify::EventKind; @@ -337,7 +365,7 @@ mod tests { #[tokio::test] async fn same_root_subscribers_share_single_watcher() { - let registry = LocalFolderWatcherRegistry::new(); + let registry = LocalFolderWatcherRegistry::new(BufferStore::new()); let root = test_root("shared"); let first = registry.subscribe(&root).expect("first subscription"); @@ -358,7 +386,7 @@ mod tests { #[tokio::test] async fn different_roots_create_independent_watchers() { - let registry = LocalFolderWatcherRegistry::new(); + let registry = LocalFolderWatcherRegistry::new(BufferStore::new()); let first_root = test_root("first"); let second_root = test_root("second"); diff --git a/rust/crates/mnote-web/src/routes/documents.rs b/rust/crates/mnote-web/src/routes/documents.rs index 6fc7a8c3..6bc4596a 100644 --- a/rust/crates/mnote-web/src/routes/documents.rs +++ b/rust/crates/mnote-web/src/routes/documents.rs @@ -521,6 +521,7 @@ pub async fn content( } pub async fn page_body_write( + State(state): State, Extension(context): Extension, Json(body): Json, ) -> Result<(StatusCode, HeaderMap, Json), WebError> { @@ -548,7 +549,7 @@ pub async fn page_body_write( } ensure_local_workspace_access(&context, root_uri) .map_err(|error| error.with_context(&context))?; - let result = write_local_markdown_page_body(&body)?; + let result = write_local_markdown_page_body(&body, Some(&state.buffer_store))?; Ok(ok_response(&context, result)) } @@ -580,23 +581,26 @@ pub async fn save( .expected_file_version .as_deref() .or(body.conflict_detection_key.as_deref()); - let result = write_local_markdown_page_body(&core_protocol::PageBodyWriteRequest { - document_id: document_id.to_string(), - workspace_id: body.workspace_id.clone().unwrap_or_default(), - source_kind: core_protocol::WorkspaceSourceKind::LocalFolder, - root_uri: root_uri.to_string(), - expected_file_version: expected_file_version.map(ToOwned::to_owned), - base_content_hash: body.base_content_hash.clone(), - content_format: body - .content_format - .clone() - .unwrap_or_else(|| "editorBlocks".into()), - content: body.content.clone(), - editor_source: body - .editor_source - .clone() - .or_else(|| Some("documents/save-compat".into())), - })?; + let result = write_local_markdown_page_body( + &core_protocol::PageBodyWriteRequest { + document_id: document_id.to_string(), + workspace_id: body.workspace_id.clone().unwrap_or_default(), + source_kind: core_protocol::WorkspaceSourceKind::LocalFolder, + root_uri: root_uri.to_string(), + expected_file_version: expected_file_version.map(ToOwned::to_owned), + base_content_hash: body.base_content_hash.clone(), + content_format: body + .content_format + .clone() + .unwrap_or_else(|| "editorBlocks".into()), + content: body.content.clone(), + editor_source: body + .editor_source + .clone() + .or_else(|| Some("documents/save-compat".into())), + }, + Some(&state.buffer_store), + )?; return Ok(ok_response(&context, result)); } let effective_workspace_id = @@ -1368,10 +1372,10 @@ mod tests { std::process::id() )); let _ = std::fs::remove_dir_all(&root); - std::fs::create_dir_all(&root).expect("create local root"); + std::fs::create_dir_all(root.join("Old Local Title")).expect("create local page bundle"); std::fs::write( - root.join("README.md"), - "---\nmnote_id: local-stable\ntitle: Old Title\n---\n# Old\n", + root.join("Old Local Title").join("Old Local Title.md"), + "# Old\n", ) .expect("write md"); let root_uri = format!("file://{}", root.display()); @@ -1380,7 +1384,7 @@ mod tests { &root_uri, ) .expect("init local workspace"); - let document_id = "local-mdid:local-stable"; + let document_id = "local-md:Old~20Local~20Title~2FOld~20Local~20Title.md"; let title_response = app() .oneshot( @@ -1404,6 +1408,14 @@ mod tests { .await .expect("title response"); assert_eq!(title_response.status(), StatusCode::OK); + let title_body = to_bytes(title_response.into_body(), usize::MAX) + .await + .expect("title body"); + let title_payload: Value = serde_json::from_slice(&title_body).expect("title json"); + let renamed_document_id = title_payload["result"]["documentId"] + .as_str() + .expect("renamed document id") + .to_string(); let save_response = app() .oneshot( @@ -1415,7 +1427,7 @@ mod tests { .header("x-mnote-actor-type", "user") .body(Body::from( serde_json::json!({ - "documentId": document_id, + "documentId": renamed_document_id, "sourceKind": "local_folder", "rootUri": root_uri, "content": [ @@ -1460,7 +1472,7 @@ mod tests { .header("x-mnote-actor-type", "user") .body(Body::from( serde_json::json!({ - "documentId": document_id, + "documentId": renamed_document_id, "sourceKind": "local_folder", "rootUri": root_uri, "options": { @@ -1476,17 +1488,23 @@ mod tests { .expect("options response"); assert_eq!(options_response.status(), StatusCode::OK); - let markdown = std::fs::read_to_string(root.join("README.md")).expect("read md"); - assert!(markdown.contains("mnote_id: local-stable")); - assert!(markdown.contains("title: New Local Title")); + let markdown = + std::fs::read_to_string(root.join("New Local Title").join("New Local Title.md")) + .expect("read md"); assert!(markdown.contains("## Saved Heading")); assert!(markdown.contains("Saved body")); let options = std::fs::read_to_string(root.join(".mnote").join("page-options.json")) .expect("read page options"); let options_json: Value = serde_json::from_str(&options).expect("options json"); - assert_eq!(options_json["pages"][document_id]["wideLayout"], true); - assert_eq!(options_json["pages"][document_id]["showToc"], false); + assert_eq!( + options_json["pages"][&renamed_document_id]["wideLayout"], + true + ); + assert_eq!( + options_json["pages"][&renamed_document_id]["showToc"], + false + ); let _ = std::fs::remove_dir_all(&root); } @@ -1510,7 +1528,7 @@ mod tests { &root_uri, ) .expect("init local workspace"); - let document_id = "local-mdid:expected-file-version"; + let document_id = "local-md:README.md"; let aggregate = crate::routes::local_folder_source::resolve_local_markdown_page_aggregate( &root_uri, document_id, @@ -1582,7 +1600,7 @@ mod tests { assert!(payload["details"]["conflict"]["currentDiskVersion"] .as_str() .unwrap_or("") - .starts_with("local-md:local-mdid:expected-file-version:")); + .starts_with("local-md:local-md:README.md:")); assert!(payload["details"]["conflict"]["suggestedActions"] .as_array() .unwrap() diff --git a/rust/crates/mnote-web/src/routes/gateway.rs b/rust/crates/mnote-web/src/routes/gateway.rs index 49ca9bee..aa11d719 100644 --- a/rust/crates/mnote-web/src/routes/gateway.rs +++ b/rust/crates/mnote-web/src/routes/gateway.rs @@ -1,9 +1,10 @@ use crate::app::AppState; -use crate::context::RequestContext; +use crate::context::{stable_actor_id, RequestContext}; use crate::error::WebError; use crate::routes::local_folder_source::{ - ensure_local_workspace_read_access, is_local_access_policy_admin_context, - load_local_folder_page_tree_snapshot, local_access_policy_path_display, + create_default_local_workspace_for_actor, ensure_local_workspace_read_access, + is_local_access_policy_admin_context, load_local_folder_page_tree_snapshot, + load_local_trash_entries, local_access_policy_path_display, }; use crate::routes::snapshot_support::load_sidebar_dataset; use crate::routes::web_shell::{ @@ -22,10 +23,12 @@ use axum::extract::ws::{Message as AxumWsMessage, WebSocket, WebSocketUpgrade}; use axum::extract::{Extension, Query, State}; use axum::http::{header, HeaderName, HeaderValue, Request, StatusCode, Uri}; use axum::response::{Html, IntoResponse, Response}; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use futures_util::{SinkExt, StreamExt}; use leptos::prelude::InnerHtmlAttribute; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use std::collections::BTreeMap; use std::time::Duration; use tokio_tungstenite::tungstenite::Message as TungsteniteMessage; @@ -36,6 +39,8 @@ const COOKIE_CONVEX_AUTH_JWT: &str = "__convexAuthJWT"; const COOKIE_CONVEX_AUTH_REFRESH_TOKEN: &str = "__convexAuthRefreshToken"; const COOKIE_MNOTE_WEB_CONVEX_TOKEN: &str = "mnote_web_convex_token"; const COOKIE_MNOTE_WEB_DEV_SESSION: &str = "mnote_web_dev_session"; +const COOKIE_MNOTE_ACTOR_ID: &str = "mnote_actor_id"; +const COOKIE_MNOTE_ACTOR_TYPE: &str = "mnote_actor_type"; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -305,24 +310,58 @@ pub async fn root_entry( Some(root_uri.to_string()), ) } else if should_render_local_first_landing { - let workspace_id = "local-first-entry".to_string(); + let payload = create_default_local_workspace_for_actor( + &context.auth.actor_id, + &context.auth.actor_type, + ) + .map_err(|error| error.with_context(&context))?; + let root_uri = payload + .get("workspace") + .and_then(|workspace| workspace.get("rootUri")) + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + WebError::internal("默认本地工作区初始化未返回 rootUri").with_context(&context) + })? + .to_string(); + let snapshot = load_local_folder_page_tree_snapshot(&root_uri)?; + let workspace_id = snapshot + .dataset + .get("workspace") + .and_then(|workspace| workspace.get("id")) + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or("local-folder") + .to_string(); let workspace_projection = build_workspace_shell_projection( - &json!({ - "workspaces": [{ "id": workspace_id, "name": "我的空间" }], - "documents": [], - }), + &snapshot.dataset, &workspace_id, - None, + requested_page_id.as_deref(), "我的空间", ); + let selected_active_page_id = choose_root_entry_active_page_id( + requested_page_id.clone(), + recent_page_id.clone(), + workspace_projection.active_page_id.as_deref(), + workspace_projection + .my_page_items + .first() + .map(|item| item.id.as_str()), + ); + let sidebar_tree_html = + render_local_sidebar_tree_html(&root_uri, selected_active_page_id.as_deref())?; + let file_tree_html = + render_local_file_tree_html(&root_uri, selected_active_page_id.as_deref())?; ( workspace_id, workspace_projection, - String::new(), - String::new(), - None, - None, - None, + sidebar_tree_html, + file_tree_html, + selected_active_page_id, + Some("local_folder".to_string()), + Some(root_uri), ) } else { let workspace_id = @@ -448,6 +487,7 @@ pub async fn root_entry( workspace_sidebar_html={workspace_sidebar_html.clone()} page_subtree_json={page_subtree_json} show_admin_access_policy={show_admin_access_policy} + enable_tree_live={active_source_kind.as_deref() != Some("local_folder")} /> }); let body_extra = format!( @@ -475,7 +515,7 @@ pub async fn root_entry( {} - + {} {} @@ -483,6 +523,8 @@ pub async fn root_entry( escape_html(&html_title), crate::ssr::MNOTE_CSS, escape_html(context.auth.actor_id.as_str()), + escape_html(active_source_kind.as_deref().unwrap_or("convex_workspace")), + escape_html(active_root_uri.as_deref().unwrap_or("")), content, body_extra )) @@ -506,6 +548,81 @@ pub async fn trash_entry( return Ok(response); } + let source_kind = query + .source_kind + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()); + if source_kind == Some("local_folder") { + let root_uri = query + .root_uri + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri") + })?; + ensure_local_workspace_read_access(&context, root_uri) + .map_err(|error| error.with_context(&context))?; + let workspace_id = + crate::routes::local_folder_source::local_workspace_id_from_root_uri(root_uri)?; + let sidebar_tree_html = render_local_sidebar_tree_html(root_uri, None).unwrap_or_default(); + let file_tree_html = render_local_file_tree_html(root_uri, None).unwrap_or_default(); + let workspace_projection = build_workspace_shell_projection( + &json!({ + "active_workspace_id": workspace_id, + "workspaces": [{ "id": workspace_id, "name": "我的空间" }], + "documents": [], + }), + &workspace_id, + None, + "我的空间", + ); + let workspace_sidebar_html = render_workspace_shell_sidebar_html( + &workspace_projection, + Some(sidebar_tree_html.as_str()), + Some(file_tree_html.as_str()), + ); + let trash_workbench_html = render_local_trash_workbench_html( + &workspace_id, + root_uri, + &load_local_trash_entries(root_uri)?, + ); + let content = crate::ssr::render_view(leptos::view! { + +
+
+ }); + + let mut response = Html(format!( + r#" + + + + 本地文件夹垃圾箱 + + + + {} + +"#, + crate::ssr::MNOTE_CSS, + escape_html(root_uri), + content, + )) + .into_response(); + stamp_gateway_headers(response.headers_mut(), false); + context.apply_response_headers(response.headers_mut()); + return Ok(response); + } + let default_workspace_name = format!("{} 的空间", state.config().dev_user_name); let workspace_id = resolve_root_workspace_id(&state, &context, query.workspace_id.as_deref()).await?; @@ -815,6 +932,202 @@ fn render_trash_workbench_html(workspace_id: &str, dataset: &Value) -> String { ) } +fn render_local_trash_workbench_html( + workspace_id: &str, + root_uri: &str, + entries: &BTreeMap, +) -> String { + let mut document_rows = Vec::new(); + let mut resource_rows = Vec::new(); + for (entry_id, entry) in entries { + let deleted_at = entry.deleted_at_ms.to_string(); + let original = escape_html(&entry.original_relative_path); + let trash_path = escape_html(&entry.trash_relative_path); + let kind = escape_html(&entry.resource_kind); + let row = format!( + r#"
+
+ {kind} + {original} + 回收站:{trash_path} · {deleted_at} +
+
+ + +
+
"#, + entry_id = escape_html(entry_id), + kind = kind, + original = original, + trash_path = trash_path, + deleted_at = escape_html(&deleted_at), + command_id = escape_html(local_trash_command_id(entry_id, entry).as_str()), + ); + if entry.resource_kind == "markdown" || entry.resource_kind == "markdown_bundle" { + document_rows.push(row); + } else { + resource_rows.push(row); + } + } + let document_body = if document_rows.is_empty() { + r#"
暂无已删除页面
"#.to_string() + } else { + document_rows.join("") + }; + let resource_body = if resource_rows.is_empty() { + r#"
暂无已删除资源
"#.to_string() + } else { + resource_rows.join("") + }; + format!( + r#"
+
+

本地文件夹垃圾箱

+

本地删除项保存在当前目录的 .mnote/trash.mnote/trash-index.json 中。

+

+
+
+
+

页面 {document_count}

+ +
+ {document_body} +
+
+
+

资源 {resource_count}

+ +
+ {resource_body} +
+
+"#, + workspace_id = escape_html(workspace_id), + root_uri = escape_html(root_uri), + document_count = document_rows.len(), + resource_count = resource_rows.len(), + document_empty_disabled = if document_rows.is_empty() { + " disabled" + } else { + "" + }, + resource_empty_disabled = if resource_rows.is_empty() { + " disabled" + } else { + "" + }, + document_body = document_body, + resource_body = resource_body, + ) +} + +fn local_trash_command_id( + entry_id: &str, + entry: &crate::routes::local_folder_source::LocalTrashEntry, +) -> String { + if entry.resource_kind == "markdown" || entry.resource_kind == "markdown_bundle" { + return entry.document_id.clone(); + } + entry_id.to_string() +} + fn json_array<'a>(dataset: &'a Value, key: &str) -> &'a [Value] { dataset .get(key) @@ -1334,6 +1647,10 @@ fn build_auth_proxy_response( COOKIE_CONVEX_AUTH_REFRESH_TOKEN, tokens.get("refreshToken"), ); + if let Some(actor_id) = resolve_mnote_actor_id(&value, tokens, context) { + set_literal_cookie(response.headers_mut(), COOKIE_MNOTE_ACTOR_ID, &actor_id); + set_literal_cookie(response.headers_mut(), COOKIE_MNOTE_ACTOR_TYPE, "user"); + } expire_cookie(response.headers_mut(), COOKIE_MNOTE_WEB_DEV_SESSION); } } @@ -1342,6 +1659,61 @@ fn build_auth_proxy_response( response } +fn resolve_mnote_actor_id( + value: &serde_json::Value, + tokens: &serde_json::Value, + context: &RequestContext, +) -> Option { + [ + "/userId", + "/user/id", + "/user/_id", + "/user/subject", + "/profile/userId", + "/profile/id", + ] + .iter() + .find_map(|pointer| { + value + .pointer(pointer) + .and_then(Value::as_str) + .and_then(stable_actor_id) + }) + .or_else(|| { + tokens + .get("token") + .and_then(Value::as_str) + .and_then(extract_actor_id_from_jwt) + }) + .or_else(|| { + let actor_id = stable_actor_id(&context.auth.actor_id)?; + if actor_id == "anonymous" { + None + } else { + Some(actor_id) + } + }) +} + +fn extract_actor_id_from_jwt(token: &str) -> Option { + let payload_segment = token.split('.').nth(1)?; + let decoded = URL_SAFE_NO_PAD.decode(payload_segment.as_bytes()).ok()?; + let payload: Value = serde_json::from_slice(&decoded).ok()?; + ["sub", "userId", "id", "_id"].iter().find_map(|key| { + payload + .get(key) + .and_then(Value::as_str) + .and_then(stable_actor_id) + }) +} + +fn set_literal_cookie(headers: &mut axum::http::HeaderMap, name: &'static str, value: &str) { + let cookie = format!("{name}={value}; Path=/; HttpOnly; SameSite=Lax"); + if let Ok(value) = HeaderValue::from_str(&cookie) { + headers.append(header::SET_COOKIE, value); + } +} + fn set_auth_cookie_from_value( headers: &mut axum::http::HeaderMap, name: &'static str, @@ -1360,6 +1732,8 @@ fn clear_auth_cookies(headers: &mut axum::http::HeaderMap) { expire_cookie(headers, COOKIE_CONVEX_AUTH_JWT); expire_cookie(headers, COOKIE_CONVEX_AUTH_REFRESH_TOKEN); expire_cookie(headers, COOKIE_MNOTE_WEB_DEV_SESSION); + expire_cookie(headers, COOKIE_MNOTE_ACTOR_ID); + expire_cookie(headers, COOKIE_MNOTE_ACTOR_TYPE); } fn expire_cookie(headers: &mut axum::http::HeaderMap, name: &'static str) { @@ -1456,9 +1830,20 @@ mod tests { use axum::http::{header, HeaderMap, HeaderValue, Request, StatusCode}; use axum::response::{Html, IntoResponse}; use axum::routing::{get, post}; + use base64::Engine; use tokio::net::TcpListener; use tower::util::ServiceExt; + fn temp_root(name: &str) -> std::path::PathBuf { + let stamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + let path = std::env::temp_dir().join(format!("{name}-{stamp}")); + std::fs::create_dir_all(&path).expect("temp root"); + path + } + fn app() -> axum::Router { app_with_legacy_next_base_url("http://127.0.0.1:3100".into()) } @@ -1526,6 +1911,7 @@ mod tests { axum::Json(serde_json::json!({ "status": "success", "value": { + "userId": "user_demo", "tokens": { "token": "jwt-demo", "refreshToken": "refresh-demo" @@ -1542,6 +1928,43 @@ mod tests { format!("http://{addr}") } + async fn spawn_convex_auth_upstream_with_token(token: String) -> String { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("convex auth listener"); + let addr = listener.local_addr().expect("convex auth addr"); + let app = axum::Router::new().route( + "/api/action", + post(move || { + let token = token.clone(); + async move { + axum::Json(serde_json::json!({ + "status": "success", + "value": { + "tokens": { + "token": token, + "refreshToken": "refresh-demo" + } + } + })) + } + }), + ); + tokio::spawn(async move { + axum::serve(listener, app) + .await + .expect("convex auth server"); + }); + format!("http://{addr}") + } + + fn unsigned_jwt_with_subject(subject: &str) -> String { + let payload = serde_json::json!({ "sub": subject }); + let encoded_payload = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(payload.to_string().as_bytes()); + format!("header.{encoded_payload}.signature") + } + async fn spawn_legacy_auth_upstream() -> String { let listener = TcpListener::bind("127.0.0.1:0") .await @@ -1745,6 +2168,66 @@ mod tests { assert!(!html.contains("window.location.reload")); } + #[tokio::test] + async fn trash_entry_renders_local_folder_trash_workbench() { + let root = temp_root("mnote-local-folder-trash-entry"); + crate::routes::local_folder_source::initialize_local_workspace_for_actor( + "user_real", + &format!("file://{}", root.display()), + ) + .expect("init local workspace"); + std::fs::create_dir_all(root.join(".mnote")).expect("create metadata dir"); + std::fs::create_dir_all(root.join(".mnote").join("trash")).expect("create trash dir"); + std::fs::write( + root.join(".mnote").join("trash-index.json"), + serde_json::json!({ + "entries": { + "local-md:Deleted~2FDeleted.md": { + "documentId": "local-md:Deleted~2FDeleted.md", + "resourceKind": "markdown_bundle", + "resourceScope": "local_folder", + "originalRelativePath": "Deleted", + "trashRelativePath": ".mnote/trash/Deleted", + "deletedAtMs": 1770000000000u64 + } + } + }) + .to_string(), + ) + .expect("write trash index"); + let root_uri = format!("file://{}", root.display()); + let uri = format!( + "/trash?sourceKind=local_folder&rootUri={}", + root_uri.replace(':', "%3A").replace('/', "%2F") + ); + + let response = app() + .oneshot( + Request::builder() + .uri(uri) + .header("x-mnote-actor-id", "user_real") + .header("x-mnote-actor-type", "user") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let html = String::from_utf8(body.to_vec()).expect("utf8"); + assert!(html.contains(r#"data-trash-source-kind="local_folder""#)); + assert!(html.contains("Deleted")); + assert!(html.contains(r#"data-trash-action="local-restore""#)); + assert!(html.contains(r#"data-trash-action="local-purge""#)); + assert!(html.contains(r#"data-trash-action="local-empty-documents""#)); + assert!(html.contains(r#"data-trash-action="local-empty-resources" disabled"#)); + + let _ = std::fs::remove_dir_all(&root); + } + #[test] fn root_entry_active_selection_prefers_page_id_over_recent_projection_and_first_page() { let selected = super::choose_root_entry_active_page_id( @@ -1860,6 +2343,11 @@ mod tests { #[tokio::test] async fn root_entry_renders_local_first_landing_without_convex() { + let _guard = crate::test_support::hermes_env_lock() + .lock() + .expect("env lock"); + let base = temp_root("mnote-root-local-first-landing"); + std::env::set_var("MNOTE_LOCAL_WORKSPACE_BASE_DIR", &base); let response = app_with_query_fixtures("http://127.0.0.1:3100".into(), false, None, None) .oneshot( Request::builder() @@ -1871,15 +2359,19 @@ mod tests { ) .await .expect("response"); + std::env::remove_var("MNOTE_LOCAL_WORKSPACE_BASE_DIR"); assert_eq!(response.status(), StatusCode::OK); let body = to_bytes(response.into_body(), usize::MAX) .await .expect("body"); let html = String::from_utf8(body.to_vec()).expect("utf8"); - assert!(html.contains(r#"data-testid="mnote-create-default-local-workspace""#)); - assert!(html.contains(r#"data-testid="mnote-open-local-folder-empty""#)); + assert!(html.contains("初始化的新页面")); + assert!(html.contains(r#"data-mnote-source-kind="local_folder""#)); + assert!(!html.contains(r#"data-testid="mnote-create-default-local-workspace""#)); + assert!(!html.contains(r#"data-testid="mnote-open-local-folder-empty""#)); assert!(!html.contains("workspaces:ensureDefaultWorkspace")); + let _ = std::fs::remove_dir_all(&base); } #[tokio::test] @@ -2060,6 +2552,52 @@ mod tests { assert!(html.contains(r#"data-mnote-shell="workspace""#)); } + #[tokio::test] + async fn root_entry_initializes_default_local_workspace_page() { + let _guard = crate::test_support::hermes_env_lock() + .lock() + .expect("env lock"); + let base = temp_root("mnote-root-default-local-workspace"); + std::env::set_var("MNOTE_LOCAL_WORKSPACE_BASE_DIR", &base); + let response = app_with_config("http://127.0.0.1:3100".into(), false) + .oneshot( + Request::builder() + .uri("/") + .header("x-mnote-actor-id", "user_real") + .header("x-mnote-actor-type", "user") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + std::env::remove_var("MNOTE_LOCAL_WORKSPACE_BASE_DIR"); + + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let html = String::from_utf8(body.to_vec()).expect("utf8"); + let expected_root = base + .join("users") + .join("user_real") + .join("workspaces") + .join("my-space"); + assert!(expected_root + .join("初始化的新页面") + .join("初始化的新页面.md") + .exists()); + assert!(html.contains("初始化的新页面")); + assert!(html.contains("local_folder")); + assert!(html.contains(r#"data-mnote-source-kind="local_folder""#)); + assert!(html.contains(r#"data-mnote-root-uri="file://"#)); + assert!(!html.contains(r#"data-testid="mnote-workspace-empty-state""#)); + assert!(!html.contains("当前还没有可显示的本地工作区")); + assert!(!html.contains(r#"data-testid="mnote-empty-create-page""#)); + assert!(html.contains(r#""transport":"disabled""#)); + + let _ = std::fs::remove_dir_all(&base); + } + #[tokio::test] async fn auth_entry_returns_gateway_fallback_shell_when_compat_disabled() { let response = app_with_config("http://127.0.0.1:3100".into(), false) @@ -2150,6 +2688,12 @@ mod tests { assert!(values .iter() .any(|value| value.contains("__convexAuthRefreshToken=refresh-demo"))); + assert!(values + .iter() + .any(|value| value.contains("mnote_actor_id=user_demo"))); + assert!(values + .iter() + .any(|value| value.contains("mnote_actor_type=user"))); let body = to_bytes(response.into_body(), usize::MAX) .await .expect("body"); @@ -2158,6 +2702,42 @@ mod tests { assert_eq!(payload["tokens"]["refreshToken"], "dummy"); } + #[tokio::test] + async fn auth_api_normalizes_session_subject_before_setting_actor_cookie() { + let token = unsigned_jwt_with_subject("user_stable|session_rotating"); + let convex_url = spawn_convex_auth_upstream_with_token(token).await; + let response = app_with_config_and_convex_url( + "http://127.0.0.1:3100".into(), + false, + Some(convex_url), + ) + .oneshot( + Request::builder() + .method("POST") + .uri("/api/auth") + .header("content-type", "application/json") + .body(Body::from( + r#"{"action":"auth:signIn","args":{"provider":"password","params":{"email":"mnote.e2e@example.com","password":"MnoteE2E123!","flow":"signIn"}}}"#, + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let cookies = response.headers().get_all(header::SET_COOKIE); + let values = cookies + .iter() + .map(|value| value.to_str().unwrap_or_default()) + .collect::>(); + assert!(values + .iter() + .any(|value| value.contains("mnote_actor_id=user_stable"))); + assert!(!values + .iter() + .any(|value| value.contains("mnote_actor_id=user_stable|session_rotating"))); + } + #[tokio::test] async fn auth_entry_uses_mnote_web_login_ui_when_compat_enabled() { let legacy_base_url = spawn_legacy_auth_upstream().await; diff --git a/rust/crates/mnote-web/src/routes/kernel.rs b/rust/crates/mnote-web/src/routes/kernel.rs index 28841bb0..b74efb3d 100644 --- a/rust/crates/mnote-web/src/routes/kernel.rs +++ b/rust/crates/mnote-web/src/routes/kernel.rs @@ -1,6 +1,10 @@ use crate::app::AppState; use crate::context::RequestContext; use crate::error::WebError; +use crate::routes::local_folder_source::{ + ensure_local_workspace_read_access, load_local_folder_file_tree_snapshot, + load_local_folder_page_tree_snapshot, +}; use crate::routes::query_support::resolve_effective_workspace_id; use crate::routes::snapshot_support::{ execute_kernel_query, load_projection_snapshot, load_sidebar_dataset, subtree_query, @@ -22,6 +26,8 @@ pub struct KernelProjectionQuery { pub depth: Option, pub query: Option, pub max_results: Option, + pub source_kind: Option, + pub root_uri: Option, } #[derive(Debug, Deserialize)] @@ -65,6 +71,30 @@ async fn project_projection( Query(query): Query, projection: KernelProjectionKind, ) -> Result<(StatusCode, Json), WebError> { + let source_kind = query + .source_kind + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()); + if source_kind == Some("local_folder") { + let root_uri = query + .root_uri + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri") + })?; + ensure_local_workspace_read_access(&context, root_uri) + .map_err(|error| error.with_context(&context))?; + let snapshot = if projection == KernelProjectionKind::FileTree { + load_local_folder_file_tree_snapshot(root_uri)? + } else { + load_local_folder_page_tree_snapshot(root_uri)? + }; + return Ok(ok_response(&context, snapshot.projection)); + } + let effective_workspace_id = resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), true)? .expect("workspace_required 已确保存在"); @@ -189,6 +219,7 @@ pub async fn graph( #[cfg(test)] mod tests { use crate::app::{build_app, AppConfig, AppState}; + use crate::routes::local_folder_source::initialize_local_workspace_for_actor; use axum::body::{to_bytes, Body}; use axum::http::{Request, StatusCode}; use serde_json::Value; @@ -296,6 +327,50 @@ mod tests { } } + #[tokio::test] + async fn tree_projection_routes_short_circuit_local_folder_without_convex_dataset() { + let root = std::env::temp_dir().join(format!( + "mnote-local-kernel-projection-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir_all(root.join("本地页面")).expect("create local page bundle"); + std::fs::write(root.join("本地页面").join("本地页面.md"), "") + .expect("write local markdown"); + let root_uri = format!("file://{}", root.display()); + initialize_local_workspace_for_actor("dev-user", &root_uri).expect("init local workspace"); + + let response = app() + .oneshot( + Request::builder() + .uri(format!( + "/api/tree/projections/file?workspaceId=local-ws:dev-user:my-space&sourceKind=local_folder&rootUri={root_uri}&rootNodeId=local-md:%E6%9C%AC%E5%9C%B0%E9%A1%B5%E9%9D%A2~2F%E6%9C%AC%E5%9C%B0%E9%A1%B5%E9%9D%A2.md" + )) + .header("x-mnote-actor-id", "dev-user") + .header("x-mnote-actor-type", "user") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["ok"], true); + assert_eq!(payload["result"]["projection"], "file_tree"); + assert_eq!(payload["result"]["sourceKind"], "local_folder"); + assert!(payload["result"]["items"] + .as_array() + .expect("items") + .iter() + .any(|item| item["title"] == "本地页面.md")); + + let _ = std::fs::remove_dir_all(&root); + } + #[tokio::test] async fn tree_projection_routes_keep_ok_response_shape() { let response = app() diff --git a/rust/crates/mnote-web/src/routes/local_folder_source.rs b/rust/crates/mnote-web/src/routes/local_folder_source.rs index c1a1b8a3..58eb6b2b 100644 --- a/rust/crates/mnote-web/src/routes/local_folder_source.rs +++ b/rust/crates/mnote-web/src/routes/local_folder_source.rs @@ -16,6 +16,7 @@ use bridge_runtime::project_legacy_content_to_block_document; use core_protocol::{ KernelObjectIdentity, KernelObjectKind, ObjectWorkspacePath, WorkspaceSourceKind, }; +use reqwest::Url; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; use std::cmp::Ordering; @@ -25,6 +26,7 @@ use std::fs; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; +use time::OffsetDateTime; #[derive(Debug, Clone)] struct LocalFolderEntry { @@ -38,7 +40,6 @@ struct LocalFolderEntry { #[derive(Debug, Clone, Default)] struct LocalFolderMetadata { - page_ids: BTreeMap, page_options: BTreeMap, trash_entries: BTreeMap, } @@ -130,29 +131,29 @@ enum LocalAccessMode { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -struct LocalTrashEntry { +pub(crate) struct LocalTrashEntry { #[serde(default, alias = "documentId")] - document_id: String, + pub(crate) document_id: String, #[serde(default, alias = "resourceKind")] - resource_kind: String, + pub(crate) resource_kind: String, #[serde(default, alias = "resourceScope")] - resource_scope: String, + pub(crate) resource_scope: String, #[serde(default, alias = "originalFilePath")] - original_file_path: String, + pub(crate) original_file_path: String, #[serde(default, alias = "trashedFilePath")] - trashed_file_path: String, + pub(crate) trashed_file_path: String, #[serde(default, alias = "trashEntryId")] - trash_entry_id: String, + pub(crate) trash_entry_id: String, #[serde(default, alias = "originalRelativePath")] - original_relative_path: String, + pub(crate) original_relative_path: String, #[serde(default, alias = "trashRelativePath")] - trash_relative_path: String, + pub(crate) trash_relative_path: String, #[serde(default, alias = "deletedAtMs")] - deleted_at_ms: u128, + pub(crate) deleted_at_ms: u128, #[serde(default, alias = "archivedAt")] - archived_at: u128, + pub(crate) archived_at: u128, #[serde(default, alias = "purgedAt")] - purged_at: Option, + pub(crate) purged_at: Option, } #[derive(Debug, Clone)] @@ -1707,15 +1708,7 @@ fn ensure_default_workspace_manifest( } fn ensure_default_workspace_directories(root: &Path) -> Result<(), WebError> { - for relative in [ - "pages", - "assets/shared", - "assets/imports", - "mindmaps", - "ai-sessions/private", - "ai-sessions/shared", - ".mnote/trash", - ] { + for relative in [".mnote/trash"] { let path = root.join(relative); if !path.starts_with(root) { return Err(WebError::bad_request_code( @@ -1734,7 +1727,8 @@ fn ensure_default_workspace_directories(root: &Path) -> Result<(), WebError> { } fn ensure_default_workspace_home_page(root: &Path) -> Result<(), WebError> { - let page = root.join("pages").join("我的空间.md"); + let page_dir = root.join("初始化的新页面"); + let page = page_dir.join("初始化的新页面.md"); if page.exists() { return Ok(()); } @@ -1744,7 +1738,16 @@ fn ensure_default_workspace_home_page(root: &Path) -> Result<(), WebError> { "默认本地工作区首页不能越过 root", )); } - let content = "---\ntitle: 我的空间\nmnote_id: my-space-home\n---\n# 我的空间\n\n"; + fs::create_dir_all(&page_dir).map_err(|error| { + WebError::bad_request_code( + "local_workspace_create_failed", + format!( + "无法创建默认本地工作区首页目录 {}: {error}", + page_dir.display() + ), + ) + })?; + let content = ""; fs::write(&page, content).map_err(|error| { WebError::bad_request_code( "local_workspace_create_failed", @@ -1909,6 +1912,7 @@ pub fn load_local_folder_page_tree_snapshot( &canonical_root, None, 0, + None, &metadata, &mut rows, &workspace_id, @@ -2138,7 +2142,7 @@ pub fn save_local_markdown_page( } } let (frontmatter, _) = split_frontmatter(¤t); - let body = editor_blocks_to_markdown(content); + let body = editor_blocks_to_markdown_for_file(content, &canonical_root, &markdown_file.path); let next_markdown = if let Some(frontmatter) = frontmatter { format!("---\n{frontmatter}\n---\n{body}") } else { @@ -2154,12 +2158,20 @@ pub fn save_local_markdown_page( ) })?; let workspace_id = local_workspace_id(&canonical_root); + let relative_path_for_buffer = markdown_file + .path + .strip_prefix(&canonical_root) + .ok() + .map(|p| p.to_string_lossy().replace('\\', "/")) + .unwrap_or_default(); refresh_local_search_index_best_effort(&canonical_root, root_uri, &workspace_id); let next_conflict_key = local_markdown_conflict_detection_key(document_id, &markdown_file.path)?; Ok(json!({ "ok": true, "documentId": document_id, + "workspaceId": workspace_id, + "relativePath": relative_path_for_buffer, "revision": now_ms(), "conflict_detection_key": next_conflict_key, "fileVersion": next_conflict_key, @@ -2203,6 +2215,7 @@ fn local_markdown_conflict_error( pub fn write_local_markdown_page_body( request: &core_protocol::PageBodyWriteRequest, + buffer_store: Option<&crate::document_buffer_store::BufferStore>, ) -> Result { if request.source_kind != core_protocol::WorkspaceSourceKind::LocalFolder { return Err(WebError::bad_request_code( @@ -2223,6 +2236,28 @@ pub fn write_local_markdown_page_body( request.expected_file_version.as_deref(), &request.content, )?; + + // 保存成功后更新 BufferStore + if let Some(store) = buffer_store { + if let (Some(workspace_id), Some(relative_path), Some(file_version)) = ( + result.get("workspaceId").and_then(Value::as_str), + result.get("relativePath").and_then(Value::as_str), + result.get("fileVersion").and_then(Value::as_str), + ) { + let ws_path = crate::document_buffer_store::build_local_folder_workspace_path( + workspace_id, + &request.root_uri, + relative_path, + &request.document_id, + ); + store.mark_saved( + &ws_path, + file_version.to_string(), + format!("sha256:{file_version}"), + ); + } + } + if let Value::Object(map) = &mut result { map.insert("canonicalCommand".into(), json!("page.body.write")); map.insert("compatCommand".into(), json!("page.body.save")); @@ -2278,10 +2313,7 @@ pub async fn open_local_file( .with_context(&context) })?; let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - HeaderValue::from_static("application/octet-stream"), - ); + headers.insert(header::CONTENT_TYPE, content_type_for_path(&target)); if query.download.unwrap_or(false) { headers.insert( header::CONTENT_DISPOSITION, @@ -2452,14 +2484,14 @@ pub(crate) fn write_local_markdown_asset( )); } - let markdown_stem = Path::new(&markdown_file.file_name) - .file_stem() - .and_then(|value| value.to_str()) - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or("page"); - let asset_dir_name = format!("{}.assets", sanitize_file_name(markdown_stem, "page")); - let asset_dir = markdown_dir.join(asset_dir_name); + let page_resource_dir = + markdown_page_resource_directory(&markdown_file.path).ok_or_else(|| { + WebError::bad_request_code( + "local_asset_upload_bad_markdown_path", + "无法解析本地页面资源目录", + ) + })?; + let asset_dir = page_resource_dir; if !asset_dir.starts_with(&canonical_root) { return Err(WebError::bad_request_code( "local_folder_root_escape", @@ -2507,6 +2539,174 @@ pub(crate) fn write_local_markdown_asset( })) } +fn local_mindmap_relative_path_from_id(mindmap_id: &str) -> Option { + let value = mindmap_id.trim(); + value + .strip_prefix("local-file:") + .or_else(|| value.strip_prefix("local:asset:")) + .map(str::trim) + .filter(|path| !path.is_empty()) + .map(ToOwned::to_owned) +} + +fn ensure_relative_path_stays_in_root( + root: &Path, + relative_path: &str, +) -> Result { + let requested = Path::new(relative_path); + if requested.is_absolute() + || requested + .components() + .any(|component| matches!(component, std::path::Component::ParentDir)) + { + return Err(WebError::bad_request_code( + "local_mindmap_root_escape", + "本地思维导图路径不能越过 root", + )); + } + let target = root.join(requested); + if !target.starts_with(root) { + return Err(WebError::bad_request_code( + "local_mindmap_root_escape", + "本地思维导图路径不能越过 root", + )); + } + Ok(target) +} + +fn local_mindmap_file_name(mindmap_id: &str) -> String { + let sanitized = sanitize_file_name(mindmap_id, "mindmap"); + if sanitized.to_ascii_lowercase().ends_with(".json") { + sanitized + } else { + format!("{}.json", sanitize_file_stem(&sanitized, "mindmap")) + } +} + +fn is_local_mindmap_file_name(file_name: &str) -> bool { + let trimmed = file_name.trim(); + let lower = trimmed.to_ascii_lowercase(); + lower.ends_with(".mindmap.json") + || (trimmed.starts_with("思维导图") && lower.ends_with(".json")) +} + +fn resolve_local_mindmap_path( + root: &Path, + metadata: &LocalFolderMetadata, + document_id: &str, + mindmap_id: &str, +) -> Result { + if let Some(relative_path) = local_mindmap_relative_path_from_id(mindmap_id) { + return ensure_relative_path_stays_in_root(root, &relative_path); + } + + let markdown_file = + find_markdown_by_page_id(root, metadata, document_id)?.ok_or_else(|| { + WebError::bad_request_code( + "local_markdown_not_found", + "找不到本地思维导图所属的 Markdown 页面", + ) + })?; + let page_resource_dir = + markdown_page_resource_directory(&markdown_file.path).ok_or_else(|| { + WebError::bad_request_code( + "local_mindmap_bad_markdown_path", + "无法解析本地页面思维导图目录", + ) + })?; + Ok(page_resource_dir.join(local_mindmap_file_name(mindmap_id))) +} + +fn local_mindmap_default_data() -> Value { + json!({ + "data": {"uid": "root", "text": "KMIND"}, + "children": [] + }) +} + +pub(crate) fn read_local_mindmap_data( + root_uri: &str, + document_id: &str, + mindmap_id: &str, +) -> Result { + let root_path = parse_file_root_uri(root_uri)?; + let canonical_root = root_path.canonicalize().map_err(|error| { + WebError::bad_request_code( + "local_folder_unavailable", + format!("无法访问本地文件夹: {error}"), + ) + })?; + let metadata = load_local_folder_metadata(&canonical_root)?; + let target = resolve_local_mindmap_path(&canonical_root, &metadata, document_id, mindmap_id)?; + if !target.exists() { + return Ok(local_mindmap_default_data()); + } + if !target.is_file() { + return Err(WebError::bad_request_code( + "local_mindmap_not_file", + "本地思维导图目标必须是文件", + )); + } + let raw = fs::read_to_string(&target).map_err(|error| { + WebError::bad_request_code( + "local_mindmap_read_failed", + format!("无法读取本地思维导图 {}: {error}", target.display()), + ) + })?; + serde_json::from_str(&raw).map_err(|error| { + WebError::bad_request_code( + "local_mindmap_invalid", + format!("本地思维导图 JSON 损坏 {}: {error}", target.display()), + ) + }) +} + +pub(crate) fn write_local_mindmap_data( + root_uri: &str, + document_id: &str, + mindmap_id: &str, + data: Value, + create_only: bool, +) -> Result { + let root_path = parse_file_root_uri(root_uri)?; + let canonical_root = root_path.canonicalize().map_err(|error| { + WebError::bad_request_code( + "local_folder_unavailable", + format!("无法访问本地文件夹: {error}"), + ) + })?; + let metadata = load_local_folder_metadata(&canonical_root)?; + let target = resolve_local_mindmap_path(&canonical_root, &metadata, document_id, mindmap_id)?; + if !target.starts_with(&canonical_root) { + return Err(WebError::bad_request_code( + "local_mindmap_root_escape", + "本地思维导图路径不能越过 root", + )); + } + let parent = target.parent().ok_or_else(|| { + WebError::bad_request_code("local_mindmap_bad_path", "无法解析本地思维导图父目录") + })?; + fs::create_dir_all(parent).map_err(|error| { + WebError::bad_request_code( + "local_mindmap_create_dir_failed", + format!("无法创建本地思维导图目录 {}: {error}", parent.display()), + ) + })?; + if !(create_only && target.exists()) { + write_json_atomic(&target, &data)?; + } + let relative_path = normalize_relative_path(&canonical_root, &target)?; + Ok(json!({ + "ok": true, + "sourceKind": "local_folder", + "documentId": document_id, + "mindmapId": mindmap_id, + "assetId": format!("local-file:{relative_path}"), + "relativePath": relative_path, + "fileName": target.file_name().and_then(|value| value.to_str()).unwrap_or("思维导图.json"), + })) +} + pub fn update_local_markdown_title( root_uri: &str, document_id: &str, @@ -2519,45 +2719,10 @@ pub fn update_local_markdown_title( format!("无法访问本地文件夹: {error}"), ) })?; - let metadata = load_local_folder_metadata(&canonical_root)?; - let markdown_file = find_markdown_by_page_id(&canonical_root, &metadata, document_id)? - .ok_or_else(|| { - WebError::bad_request_code( - "local_markdown_not_found", - "找不到要更新标题的本地 Markdown 页面", - ) - })?; - let current = fs::read_to_string(&markdown_file.path).map_err(|error| { - WebError::bad_request_code( - "local_markdown_read_failed", - format!( - "无法读取本地 Markdown 文件 {}: {error}", - markdown_file.path.display() - ), - ) - })?; - fs::write( - &markdown_file.path, - markdown_with_frontmatter_fields(¤t, &[("title", title.trim())]), - ) - .map_err(|error| { - WebError::bad_request_code( - "local_markdown_write_failed", - format!( - "无法写入本地 Markdown 标题 {}: {error}", - markdown_file.path.display() - ), - ) - })?; + let result = rename_local_markdown_page(&canonical_root, document_id, title)?; let workspace_id = local_workspace_id(&canonical_root); refresh_local_search_index_best_effort(&canonical_root, root_uri, &workspace_id); - Ok(json!({ - "ok": true, - "documentId": document_id, - "title": title.trim(), - "updated_at": Value::Null, - "sourceKind": "local_folder", - })) + Ok(result) } pub fn update_local_page_options( @@ -2630,6 +2795,144 @@ pub fn execute_local_tree_command( Ok(result) } +/// 生成新页面文件名主体。 +/// 默认标题“新页面”会追加本地时间 HHMMSS,例如“新页面061240”。 +/// 候选名必须同时检查 `.md` 文件和同名目录,避免页面 bundle 错配。 +fn determine_page_stem(directory: &Path, title: &str) -> String { + let base = if title.trim() == "新页面" { + let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); + format!( + "新页面{:02}{:02}{:02}", + now.hour(), + now.minute(), + now.second() + ) + } else { + sanitize_file_stem(title, "新页面") + }; + // 同时检查 .md 文件和同名目录,确保页面 bundle 使用同一个 stem。 + let mut candidate = base.clone(); + let mut index = 2u32; + while directory.join(format!("{}.md", candidate)).exists() + || directory.join(&candidate).exists() + { + candidate = format!("{}-{}", base, index); + index += 1; + } + candidate +} + +fn determine_page_stem_for_rename( + directory: &Path, + title: &str, + current_file: &Path, + current_directory: Option<&Path>, +) -> String { + let base = sanitize_file_stem(title, "新页面"); + let mut candidate = base.clone(); + let mut index = 2u32; + loop { + let file_candidate = directory.join(format!("{candidate}.md")); + let directory_candidate = directory.join(&candidate); + let file_conflict = file_candidate.exists() && file_candidate != current_file; + let directory_conflict = directory_candidate.exists() + && current_directory + .map(|current| directory_candidate != current) + .unwrap_or(true); + if !file_conflict && !directory_conflict { + return candidate; + } + candidate = format!("{}-{}", base, index); + index += 1; + } +} + +fn determine_page_bundle_stem_for_rename( + directory: &Path, + title: &str, + current_bundle_directory: &Path, +) -> String { + let base = sanitize_file_stem(title, "新页面"); + let mut candidate = base.clone(); + let mut index = 2u32; + loop { + let bundle_candidate = directory.join(&candidate); + let loose_markdown_candidate = directory.join(format!("{candidate}.md")); + let bundle_conflict = + bundle_candidate.exists() && bundle_candidate != current_bundle_directory; + let loose_markdown_conflict = loose_markdown_candidate.exists(); + if !bundle_conflict && !loose_markdown_conflict { + return candidate; + } + candidate = format!("{}-{}", base, index); + index += 1; + } +} + +fn markdown_file_stem(path: &Path) -> Option { + path.file_stem() + .and_then(|stem| stem.to_str()) + .map(str::trim) + .filter(|stem| !stem.is_empty()) + .map(ToOwned::to_owned) +} + +fn is_nested_page_bundle_markdown(path: &Path) -> bool { + let Some(parent_name) = path + .parent() + .and_then(|parent| parent.file_name()) + .and_then(|name| name.to_str()) + else { + return false; + }; + markdown_file_stem(path) + .map(|stem| stem == parent_name) + .unwrap_or(false) +} + +fn nested_bundle_main_markdown(directory: &Path) -> Option { + let name = directory.file_name().and_then(|value| value.to_str())?; + let candidate = directory.join(format!("{name}.md")); + if candidate.is_file() { + Some(candidate) + } else { + None + } +} + +fn markdown_page_bundle_directory(markdown_path: &Path) -> Option { + let parent = markdown_path.parent()?; + let stem = markdown_file_stem(markdown_path)?; + if parent + .file_name() + .and_then(|name| name.to_str()) + .map(|name| name == stem) + .unwrap_or(false) + { + return Some(parent.to_path_buf()); + } + None +} + +fn markdown_page_resource_directory(markdown_path: &Path) -> Option { + let parent = markdown_path.parent()?; + let stem = markdown_file_stem(markdown_path)?; + if parent + .file_name() + .and_then(|name| name.to_str()) + .map(|name| name == stem) + .unwrap_or(false) + { + return Some(parent.to_path_buf()); + } + let sibling_dir = parent.join(&stem); + if sibling_dir.is_dir() { + Some(sibling_dir) + } else { + Some(parent.join(sanitize_file_name(&stem, "page"))) + } +} + fn create_local_markdown_page( root: &Path, parent_id: Option<&str>, @@ -2637,30 +2940,30 @@ fn create_local_markdown_page( ) -> Result { let metadata = load_local_folder_metadata(root)?; let parent_directory = resolve_local_parent_directory(root, &metadata, parent_id)?; - let safe_title = sanitize_file_stem(title, "新页面"); - let target = next_available_path(&parent_directory, &safe_title, "md"); + let stem = determine_page_stem(&parent_directory, title); + let dir_target = parent_directory.join(&stem); + let target = dir_target.join(format!("{}.md", stem)); let relative_path = normalize_relative_path(root, &target)?; - let page_id = normalize_metadata_page_id(&format!( - "local-mdid:{}", - encode_local_id_segment(&format!("{}:{relative_path}", local_workspace_id(root))) - )); - let markdown = markdown_with_frontmatter_fields( - &format!("# {}\n", title.trim()), - &[("mnote_id", page_id.as_str()), ("title", title.trim())], - ); + let page_id = local_markdown_path_page_id(&relative_path); + let display_title = file_stem_title(&format!("{stem}.md")); + let markdown = ""; + fs::create_dir_all(&dir_target).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!("无法创建本地页面目录 {}: {error}", dir_target.display()), + ) + })?; fs::write(&target, markdown).map_err(|error| { WebError::bad_request_code( "local_tree_command_failed", format!("无法创建本地 Markdown 文件 {}: {error}", target.display()), ) })?; - let mut page_ids = metadata.page_ids; - page_ids.insert(relative_path.clone(), page_id.clone()); - write_page_ids_metadata(root, &page_ids)?; Ok(json!({ "ok": true, "id": page_id, "documentId": page_id, + "title": display_title, "relativePath": relative_path, "action": "create", "sourceKind": "local_folder", @@ -2741,7 +3044,7 @@ fn rename_local_markdown_page( document_id: &str, title: &str, ) -> Result { - let mut metadata = load_local_folder_metadata(root)?; + let metadata = load_local_folder_metadata(root)?; let markdown_file = find_markdown_by_page_id(root, &metadata, document_id)?.ok_or_else(|| { WebError::bad_request_code( @@ -2752,8 +3055,22 @@ fn rename_local_markdown_page( let parent = markdown_file.path.parent().ok_or_else(|| { WebError::bad_request_code("local_tree_command_failed", "无法解析 Markdown 父目录") })?; - let target = next_available_path(parent, &sanitize_file_stem(title, "无标题"), "md"); + if is_nested_page_bundle_markdown(&markdown_file.path) { + return rename_nested_bundle_markdown_page(root, document_id, &markdown_file, title); + } + let new_stem = determine_page_stem_for_rename(parent, title, &markdown_file.path, None); + let target = parent.join(format!("{new_stem}.md")); + if target.exists() && target != markdown_file.path { + return Err(WebError::bad_request_code( + "local_tree_command_failed", + format!( + "目标 Markdown 文件 {} 已存在,禁止覆盖重命名", + target.display() + ), + )); + } let old_relative_path = markdown_file.relative_path; + fs::rename(&markdown_file.path, &target).map_err(|error| { WebError::bad_request_code( "local_tree_command_failed", @@ -2763,17 +3080,101 @@ fn rename_local_markdown_page( ), ) })?; + let new_relative_path = normalize_relative_path(root, &target)?; - metadata.page_ids.remove(&old_relative_path); - metadata - .page_ids - .insert(new_relative_path.clone(), document_id.to_string()); - write_page_ids_metadata(root, &metadata.page_ids)?; + let new_document_id = local_markdown_path_page_id(&new_relative_path); Ok(json!({ "ok": true, - "id": document_id, - "documentId": document_id, + "id": new_document_id, + "documentId": new_document_id, + "title": file_stem_title(&format!("{new_stem}.md")), "relativePath": new_relative_path, + "previousDocumentId": document_id, + "previousRelativePath": old_relative_path, + "action": "rename", + "sourceKind": "local_folder", + })) +} + +fn rename_nested_bundle_markdown_page( + root: &Path, + document_id: &str, + markdown_file: &LocalFolderEntry, + title: &str, +) -> Result { + let bundle_dir = markdown_file.path.parent().ok_or_else(|| { + WebError::bad_request_code("local_tree_command_failed", "无法解析页面目录") + })?; + if bundle_dir == root { + return Err(WebError::bad_request_code( + "local_tree_command_failed", + "页面目录不能是 workspace root", + )); + } + let bundle_parent = bundle_dir.parent().ok_or_else(|| { + WebError::bad_request_code("local_tree_command_failed", "无法解析页面目录父级") + })?; + let new_stem = determine_page_bundle_stem_for_rename(bundle_parent, title, bundle_dir); + let new_bundle_dir = bundle_parent.join(&new_stem); + let new_markdown = new_bundle_dir.join(format!("{new_stem}.md")); + if new_bundle_dir.exists() && new_bundle_dir != bundle_dir { + return Err(WebError::bad_request_code( + "local_tree_command_failed", + format!( + "目标页面目录 {} 已存在,禁止覆盖重命名", + new_bundle_dir.display() + ), + )); + } + let old_relative_path = markdown_file.relative_path.clone(); + if new_bundle_dir == bundle_dir { + if new_markdown != markdown_file.path { + fs::rename(&markdown_file.path, &new_markdown).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!( + "无法重命名本地 Markdown 文件 {}: {error}", + markdown_file.path.display() + ), + ) + })?; + } + } else { + fs::rename(bundle_dir, &new_bundle_dir).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!("无法重命名本地页面目录 {}: {error}", bundle_dir.display()), + ) + })?; + let moved_old_markdown = new_bundle_dir.join( + markdown_file + .path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("page.md"), + ); + if moved_old_markdown != new_markdown { + fs::rename(&moved_old_markdown, &new_markdown).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!( + "无法重命名本地 Markdown 文件 {}: {error}", + moved_old_markdown.display() + ), + ) + })?; + } + } + let new_relative_path = normalize_relative_path(root, &new_markdown)?; + let new_document_id = local_markdown_path_page_id(&new_relative_path); + Ok(json!({ + "ok": true, + "id": new_document_id, + "documentId": new_document_id, + "title": file_stem_title(&format!("{new_stem}.md")), + "relativePath": new_relative_path, + "previousDocumentId": document_id, + "previousRelativePath": old_relative_path, "action": "rename", "sourceKind": "local_folder", })) @@ -2786,12 +3187,10 @@ fn rename_local_directory(root: &Path, directory: &Path, title: &str) -> Result< "不能重命名本地 workspace root", )); } - let mut metadata = load_local_folder_metadata(root)?; let parent = directory.parent().ok_or_else(|| { WebError::bad_request_code("local_tree_command_failed", "无法解析文件夹父目录") })?; let target = next_available_directory_path(parent, &sanitize_file_stem(title, "文件夹")); - let old_relative_path = normalize_relative_path(root, directory)?; fs::rename(directory, &target).map_err(|error| { WebError::bad_request_code( "local_tree_command_failed", @@ -2799,12 +3198,6 @@ fn rename_local_directory(root: &Path, directory: &Path, title: &str) -> Result< ) })?; let new_relative_path = normalize_relative_path(root, &target)?; - rewrite_page_ids_prefix( - &mut metadata.page_ids, - &old_relative_path, - &new_relative_path, - ); - write_page_ids_metadata(root, &metadata.page_ids)?; Ok(json!({ "ok": true, "id": local_directory_group_id(&new_relative_path), @@ -2820,7 +3213,7 @@ fn copy_local_markdown_page( parent_id: Option<&str>, title: &str, ) -> Result { - let mut metadata = load_local_folder_metadata(root)?; + let metadata = load_local_folder_metadata(root)?; let markdown_file = find_markdown_by_page_id(root, &metadata, document_id)?.ok_or_else(|| { WebError::bad_request_code( @@ -2839,39 +3232,64 @@ fn copy_local_markdown_page( } else { title.trim() }; - let target = next_available_path( - &target_directory, - &sanitize_file_stem(copy_stem, "页面"), - "md", - ); + let copy_stem = sanitize_file_stem(copy_stem, "页面"); + let target; + if let Some(source_bundle) = markdown_page_bundle_directory(&markdown_file.path) { + let target_bundle = next_available_directory_path(&target_directory, ©_stem); + let target_stem = target_bundle + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(©_stem) + .to_string(); + target = target_bundle.join(format!("{target_stem}.md")); + copy_directory_recursively(&source_bundle, &target_bundle).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!( + "无法复制本地页面目录 {} 到 {}: {error}", + source_bundle.display(), + target_bundle.display() + ), + ) + })?; + let copied_main = target_bundle.join( + markdown_file + .path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("page.md"), + ); + if copied_main != target { + fs::rename(&copied_main, &target).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!( + "无法重命名复制后的 Markdown 文件 {}: {error}", + copied_main.display() + ), + ) + })?; + } + } else { + target = next_available_path(&target_directory, ©_stem, "md"); + let source_markdown = fs::read_to_string(&markdown_file.path).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!( + "无法读取待复制 Markdown 文件 {}: {error}", + markdown_file.path.display() + ), + ) + })?; + fs::write(&target, source_markdown).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!("无法复制本地 Markdown 文件到 {}: {error}", target.display()), + ) + })?; + } let new_relative_path = normalize_relative_path(root, &target)?; - let new_page_id = normalize_metadata_page_id(&format!( - "local-mdid:{}", - encode_local_id_segment(&format!("{}:{new_relative_path}", local_workspace_id(root))) - )); - let source_markdown = fs::read_to_string(&markdown_file.path).map_err(|error| { - WebError::bad_request_code( - "local_tree_command_failed", - format!( - "无法读取待复制 Markdown 文件 {}: {error}", - markdown_file.path.display() - ), - ) - })?; - fs::write( - &target, - markdown_with_frontmatter_fields(&source_markdown, &[("mnote_id", new_page_id.as_str())]), - ) - .map_err(|error| { - WebError::bad_request_code( - "local_tree_command_failed", - format!("无法复制本地 Markdown 文件到 {}: {error}", target.display()), - ) - })?; - metadata - .page_ids - .insert(new_relative_path.clone(), new_page_id.clone()); - write_page_ids_metadata(root, &metadata.page_ids)?; + let new_page_id = local_markdown_path_page_id(&new_relative_path); Ok(json!({ "ok": true, "id": new_page_id, @@ -2898,7 +3316,7 @@ fn move_local_markdown_page( document_id: &str, parent_id: Option<&str>, ) -> Result { - let mut metadata = load_local_folder_metadata(root)?; + let metadata = load_local_folder_metadata(root)?; let markdown_file = find_markdown_by_page_id(root, &metadata, document_id)?.ok_or_else(|| { WebError::bad_request_code( @@ -2907,6 +3325,15 @@ fn move_local_markdown_page( ) })?; let target_directory = resolve_local_parent_directory(root, &metadata, parent_id)?; + if let Some(bundle_dir) = markdown_page_bundle_directory(&markdown_file.path) { + return move_local_markdown_bundle( + root, + document_id, + &markdown_file, + &bundle_dir, + &target_directory, + ); + } if markdown_file.path.parent() == Some(target_directory.as_path()) { return Ok(json!({ "ok": true, @@ -2951,16 +3378,85 @@ fn move_local_markdown_page( ) })?; let new_relative_path = normalize_relative_path(root, &target)?; - metadata.page_ids.remove(&old_relative_path); - metadata - .page_ids - .insert(new_relative_path.clone(), document_id.to_string()); - write_page_ids_metadata(root, &metadata.page_ids)?; + let new_document_id = local_markdown_path_page_id(&new_relative_path); Ok(json!({ "ok": true, - "id": document_id, - "documentId": document_id, + "id": new_document_id, + "documentId": new_document_id, "relativePath": new_relative_path, + "previousDocumentId": document_id, + "previousRelativePath": old_relative_path, + "action": "move", + "sourceKind": "local_folder", + })) +} + +fn move_local_markdown_bundle( + root: &Path, + document_id: &str, + markdown_file: &LocalFolderEntry, + bundle_dir: &Path, + target_directory: &Path, +) -> Result { + if bundle_dir.parent() == Some(target_directory) { + return Ok(json!({ + "ok": true, + "id": document_id, + "documentId": document_id, + "relativePath": markdown_file.relative_path, + "action": "move", + "sourceKind": "local_folder", + })); + } + if target_directory == bundle_dir || target_directory.starts_with(bundle_dir) { + return Err(WebError::bad_request_code( + "local_tree_command_failed", + "禁止把本地页面移动到自身或后代", + )); + } + let stem = bundle_dir + .file_name() + .and_then(|name| name.to_str()) + .map(|name| sanitize_file_stem(name, "页面")) + .unwrap_or_else(|| "页面".to_string()); + let target_bundle = next_available_directory_path(target_directory, &stem); + fs::rename(bundle_dir, &target_bundle).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!("无法移动本地页面目录 {}: {error}", bundle_dir.display()), + ) + })?; + let main_file_name = markdown_file + .path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("page.md"); + let moved_markdown = target_bundle.join(main_file_name); + let target_stem = target_bundle + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(&stem); + let target_markdown = target_bundle.join(format!("{target_stem}.md")); + if moved_markdown != target_markdown { + fs::rename(&moved_markdown, &target_markdown).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!( + "无法重命名移动后的 Markdown 文件 {}: {error}", + moved_markdown.display() + ), + ) + })?; + } + let new_relative_path = normalize_relative_path(root, &target_markdown)?; + let new_document_id = local_markdown_path_page_id(&new_relative_path); + Ok(json!({ + "ok": true, + "id": new_document_id, + "documentId": new_document_id, + "relativePath": new_relative_path, + "previousDocumentId": document_id, + "previousRelativePath": markdown_file.relative_path, "action": "move", "sourceKind": "local_folder", })) @@ -2977,7 +3473,7 @@ fn move_local_directory( "不能移动本地 workspace root", )); } - let mut metadata = load_local_folder_metadata(root)?; + let metadata = load_local_folder_metadata(root)?; let target_directory = resolve_local_parent_directory(root, &metadata, parent_id)?; if target_directory == directory || target_directory.starts_with(directory) { return Err(WebError::bad_request_code( @@ -3000,7 +3496,6 @@ fn move_local_directory( .map(|name| sanitize_file_stem(name, "文件夹")) .unwrap_or_else(|| "文件夹".to_string()); let target = next_available_directory_path(&target_directory, &stem); - let old_relative_path = normalize_relative_path(root, directory)?; fs::rename(directory, &target).map_err(|error| { WebError::bad_request_code( "local_tree_command_failed", @@ -3008,12 +3503,6 @@ fn move_local_directory( ) })?; let new_relative_path = normalize_relative_path(root, &target)?; - rewrite_page_ids_prefix( - &mut metadata.page_ids, - &old_relative_path, - &new_relative_path, - ); - write_page_ids_metadata(root, &metadata.page_ids)?; Ok(json!({ "ok": true, "id": local_directory_group_id(&new_relative_path), @@ -3024,6 +3513,9 @@ fn move_local_directory( } fn trash_local_entry(root: &Path, entry_id: &str) -> Result { + if let Some(directory) = resolve_local_directory_id(root, entry_id)? { + return trash_local_directory(root, entry_id, &directory); + } if let Some(file) = resolve_local_raw_file_id(root, entry_id)? { return trash_local_raw_file(root, entry_id, &file); } @@ -3032,6 +3524,9 @@ fn trash_local_entry(root: &Path, entry_id: &str) -> Result { fn restore_local_entry(root: &Path, entry_id: &str) -> Result { let metadata = load_local_folder_metadata(root)?; + if find_local_directory_trash_entry_key(&metadata, entry_id).is_some() { + return restore_local_directory(root, entry_id); + } if find_local_file_trash_entry_key(&metadata, entry_id).is_some() { return restore_local_raw_file(root, entry_id); } @@ -3040,6 +3535,9 @@ fn restore_local_entry(root: &Path, entry_id: &str) -> Result { fn purge_local_entry(root: &Path, entry_id: &str) -> Result { let metadata = load_local_folder_metadata(root)?; + if find_local_directory_trash_entry_key(&metadata, entry_id).is_some() { + return purge_local_directory(root, entry_id); + } if find_local_file_trash_entry_key(&metadata, entry_id).is_some() { return purge_local_raw_file(root, entry_id); } @@ -3068,45 +3566,69 @@ fn trash_local_markdown_page(root: &Path, document_id: &str) -> Result Result Result { + if directory == root { + return Err(WebError::bad_request_code( + "local_tree_command_failed", + "不能删除本地 workspace root", + )); + } + let mut metadata = load_local_folder_metadata(root)?; + let relative_path = normalize_relative_path(root, directory)?; + if relative_path == ".mnote" || relative_path.starts_with(".mnote/") { + return Err(WebError::bad_request_code( + "local_tree_command_failed", + "不能删除 MNote 元数据目录", + )); + } + let trash_dir = root.join(".mnote").join("trash"); + fs::create_dir_all(&trash_dir).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!("无法创建本地回收站 {}: {error}", trash_dir.display()), + ) + })?; + let directory_name = directory + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("local-folder"); + let target = next_available_directory_path(&trash_dir, directory_name); + fs::rename(directory, &target).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!( + "无法移动本地文件夹到回收站 {}: {error}", + directory.display() + ), + ) + })?; + let trash_relative_path = normalize_relative_path(root, &target)?; + let trash_entry_id = local_directory_trash_entry_id(&relative_path); + let now = now_ms(); + metadata.trash_entries.insert( + trash_entry_id.clone(), + LocalTrashEntry { + document_id: entry_id.to_string(), + resource_kind: "local_directory".to_string(), + resource_scope: "local_folder".to_string(), + original_file_path: relative_path.clone(), + trashed_file_path: trash_relative_path.clone(), + trash_entry_id: trash_entry_id.clone(), + original_relative_path: relative_path.clone(), + trash_relative_path: trash_relative_path.clone(), + deleted_at_ms: now, + archived_at: now, + purged_at: None, + }, + ); + write_trash_index_metadata(root, &metadata.trash_entries)?; + Ok(json!({ + "ok": true, + "id": entry_id, + "documentId": entry_id, + "resourceKind": "local_directory", + "resourceScope": "local_folder", + "originalFilePath": relative_path, + "trashEntryId": trash_entry_id, + "trashPath": trash_relative_path, + "action": "delete", + "canonicalCommand": "tree.resource.archive", + "sourceKind": "local_folder", + })) +} + fn restore_local_markdown_page(root: &Path, document_id: &str) -> Result { let mut metadata = load_local_folder_metadata(root)?; let trash_entry = metadata @@ -3188,6 +3781,15 @@ fn restore_local_markdown_page(root: &Path, document_id: &str) -> Result Result Result { + if !trash_path.is_dir() { + return Err(WebError::bad_request_code( + "local_trash_entry_not_found", + "本地回收站页面目录不存在,无法恢复", + )); + } + let original_bundle_path = + resolve_metadata_relative_path(root, &trash_entry.original_relative_path)?; + let parent = original_bundle_path.parent().ok_or_else(|| { + WebError::bad_request_code("local_tree_command_failed", "无法解析页面恢复目标目录") + })?; + fs::create_dir_all(parent).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!("无法创建页面恢复目标目录 {}: {error}", parent.display()), + ) + })?; + let stem = original_bundle_path + .file_name() + .and_then(|name| name.to_str()) + .map(|name| sanitize_file_stem(name, "页面")) + .unwrap_or_else(|| "页面".to_string()); + let target_bundle = if original_bundle_path.exists() { + next_available_directory_path(parent, &stem) + } else { + original_bundle_path + }; + fs::rename(trash_path, &target_bundle).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!( + "无法从本地回收站恢复页面目录 {}: {error}", + trash_path.display() + ), + ) + })?; + let target_stem = target_bundle + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(&stem); + let restored_markdown = target_bundle.join(format!("{target_stem}.md")); + if !restored_markdown.is_file() { + return Err(WebError::bad_request_code( + "local_trash_entry_not_found", + "恢复后的页面目录缺少同名 Markdown 文件", + )); + } + let restored_relative_path = normalize_relative_path(root, &restored_markdown)?; + let restored_document_id = local_markdown_path_page_id(&restored_relative_path); + metadata.trash_entries.remove(document_id); + write_trash_index_metadata(root, &metadata.trash_entries)?; + Ok(json!({ + "ok": true, + "id": restored_document_id, + "documentId": restored_document_id, + "relativePath": restored_relative_path, + "previousDocumentId": document_id, "action": "restore", "sourceKind": "local_folder", })) @@ -3304,20 +3973,96 @@ fn restore_local_raw_file(root: &Path, entry_id: &str) -> Result Result { + let mut metadata = load_local_folder_metadata(root)?; + let trash_key = find_local_directory_trash_entry_key(&metadata, entry_id).ok_or_else(|| { + WebError::bad_request_code( + "local_trash_entry_not_found", + "找不到要恢复的本地文件夹回收站记录", + ) + })?; + let trash_entry = metadata + .trash_entries + .get(&trash_key) + .cloned() + .ok_or_else(|| { + WebError::bad_request_code( + "local_trash_entry_not_found", + "找不到要恢复的本地文件夹回收站记录", + ) + })?; + let original_relative_path = local_trash_original_path(&trash_entry); + let trash_relative_path = local_trash_file_path(&trash_entry); + let trash_path = resolve_metadata_relative_path(root, &trash_relative_path)?; + if !trash_path.is_dir() { + return Err(WebError::bad_request_code( + "local_trash_entry_not_found", + "本地文件夹回收站目录不存在,无法恢复", + )); + } + let original_path = resolve_metadata_relative_path(root, &original_relative_path)?; + if original_path.exists() { + return Err(WebError::new( + StatusCode::CONFLICT, + "local_directory_restore_conflict", + "本地文件夹原路径已存在,恢复会覆盖用户文件", + )); + } + let parent = original_path.parent().ok_or_else(|| { + WebError::bad_request_code("local_tree_command_failed", "无法解析文件夹恢复目标目录") + })?; + fs::create_dir_all(parent).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!("无法创建文件夹恢复目标目录 {}: {error}", parent.display()), + ) + })?; + fs::rename(&trash_path, &original_path).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!( + "无法从本地回收站恢复文件夹 {}: {error}", + trash_path.display() + ), + ) + })?; + metadata.trash_entries.remove(&trash_key); + write_trash_index_metadata(root, &metadata.trash_entries)?; + Ok(json!({ + "ok": true, + "id": entry_id, + "documentId": entry_id, + "resourceKind": "local_directory", + "resourceScope": "local_folder", + "relativePath": normalize_relative_path(root, &original_path)?, + "trashEntryId": trash_key, + "action": "restore", + "canonicalCommand": "tree.resource.restore", + "sourceKind": "local_folder", + })) +} + fn purge_local_markdown_page(root: &Path, document_id: &str) -> Result { let mut metadata = load_local_folder_metadata(root)?; if let Some(markdown_file) = find_markdown_by_page_id(root, &metadata, document_id)? { - fs::remove_file(&markdown_file.path).map_err(|error| { - WebError::bad_request_code( - "local_tree_command_failed", - format!( - "无法永久删除本地 Markdown 文件 {}: {error}", - markdown_file.path.display() - ), - ) - })?; - metadata.page_ids.remove(&markdown_file.relative_path); - write_page_ids_metadata(root, &metadata.page_ids)?; + if let Some(bundle_dir) = markdown_page_bundle_directory(&markdown_file.path) { + fs::remove_dir_all(&bundle_dir).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!("无法永久删除本地页面目录 {}: {error}", bundle_dir.display()), + ) + })?; + } else { + fs::remove_file(&markdown_file.path).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!( + "无法永久删除本地 Markdown 文件 {}: {error}", + markdown_file.path.display() + ), + ) + })?; + } return Ok(json!({ "ok": true, "id": document_id, @@ -3334,11 +4079,16 @@ fn purge_local_markdown_page(root: &Path, document_id: &str) -> Result Result Result { + let mut metadata = load_local_folder_metadata(root)?; + let trash_key = find_local_directory_trash_entry_key(&metadata, entry_id).ok_or_else(|| { + WebError::bad_request_code( + "local_trash_entry_not_found", + "找不到要永久删除的本地文件夹回收站记录", + ) + })?; + let trash_entry = metadata.trash_entries.remove(&trash_key).ok_or_else(|| { + WebError::bad_request_code( + "local_trash_entry_not_found", + "找不到要永久删除的本地文件夹回收站记录", + ) + })?; + let trash_relative_path = local_trash_file_path(&trash_entry); + let trash_path = resolve_metadata_relative_path(root, &trash_relative_path)?; + if trash_path.exists() { + fs::remove_dir_all(&trash_path).map_err(|error| { + WebError::bad_request_code( + "local_tree_command_failed", + format!( + "无法永久删除本地回收站文件夹 {}: {error}", + trash_path.display() + ), + ) + })?; + } + write_trash_index_metadata(root, &metadata.trash_entries)?; + Ok(json!({ + "ok": true, + "id": entry_id, + "documentId": entry_id, + "resourceKind": "local_directory", + "resourceScope": "local_folder", + "trashEntryId": trash_key, + "action": "purge", + "canonicalCommand": "tree.resource.purge", + "sourceKind": "local_folder", + })) +} + fn purge_local_raw_file(root: &Path, entry_id: &str) -> Result { let mut metadata = load_local_folder_metadata(root)?; let trash_key = find_local_file_trash_entry_key(&metadata, entry_id).ok_or_else(|| { @@ -3538,10 +4329,18 @@ fn local_file_trash_entry_id(relative_path: &str) -> String { format!("local-file:{relative_path}") } +fn local_directory_trash_entry_id(relative_path: &str) -> String { + format!("local-dir-trash:{relative_path}") +} + fn is_local_file_trash_entry(entry: &LocalTrashEntry) -> bool { entry.resource_kind == "local_file" || entry.trash_entry_id.starts_with("local-file:") } +fn is_local_directory_trash_entry(entry: &LocalTrashEntry) -> bool { + entry.resource_kind == "local_directory" || entry.trash_entry_id.starts_with("local-dir-trash:") +} + fn local_trash_original_path(entry: &LocalTrashEntry) -> String { if !entry.original_file_path.trim().is_empty() { entry.original_file_path.clone() @@ -3593,6 +4392,47 @@ fn find_local_file_trash_entry_key( .map(|(key, _)| key.clone()) } +fn find_local_directory_trash_entry_key( + metadata: &LocalFolderMetadata, + entry_id: &str, +) -> Option { + let trimmed = entry_id.trim(); + if metadata + .trash_entries + .get(trimmed) + .map(is_local_directory_trash_entry) + .unwrap_or(false) + { + return Some(trimmed.to_string()); + } + let relative_path = if let Some(encoded) = trimmed.strip_prefix("local-dir:") { + decode_local_id_segment(encoded).ok()? + } else { + trimmed + .strip_prefix("local:node:") + .or_else(|| trimmed.strip_prefix("local:folder:")) + .unwrap_or(trimmed) + .to_string() + }; + let expected_key = local_directory_trash_entry_id(&relative_path); + if metadata + .trash_entries + .get(&expected_key) + .map(is_local_directory_trash_entry) + .unwrap_or(false) + { + return Some(expected_key); + } + metadata + .trash_entries + .iter() + .find(|(_, entry)| { + is_local_directory_trash_entry(entry) + && local_trash_original_path(entry) == relative_path + }) + .map(|(key, _)| key.clone()) +} + pub fn local_workspace_id_from_root_uri(root_uri: &str) -> Result { let root_path = parse_file_root_uri(root_uri)?; let canonical_root = root_path.canonicalize().map_err(|error| { @@ -3663,36 +4503,28 @@ fn percent_decode_file_uri_path(value: &str) -> Result { fn load_local_folder_metadata(root: &Path) -> Result { Ok(LocalFolderMetadata { - page_ids: load_metadata_string_map(&root.join(".mnote").join("page-ids.json"))?, page_options: load_metadata_value_map(&root.join(".mnote").join("page-options.json"))?, trash_entries: load_trash_index_map(&root.join(".mnote").join("trash-index.json"))?, }) } -fn load_metadata_string_map(path: &Path) -> Result, WebError> { - if !path.exists() { - return Ok(BTreeMap::new()); +pub(crate) fn load_local_trash_entries( + root_uri: &str, +) -> Result, WebError> { + let root_path = parse_file_root_uri(root_uri)?; + let canonical_root = root_path.canonicalize().map_err(|error| { + WebError::bad_request_code( + "local_folder_unavailable", + format!("无法访问本地文件夹: {error}"), + ) + })?; + if !canonical_root.is_dir() { + return Err(WebError::bad_request_code( + "local_folder_not_directory", + "本地 rootUri 必须指向目录", + )); } - let value = read_metadata_json(path)?; - let source = value - .get("pages") - .or_else(|| value.get("pageIds")) - .unwrap_or(&value); - let Some(map) = source.as_object() else { - return Err(metadata_invalid(path, "元数据必须是对象")); - }; - let mut result = BTreeMap::new(); - for (key, value) in map { - let Some(page_id) = value - .as_str() - .map(str::trim) - .filter(|item| !item.is_empty()) - else { - return Err(metadata_invalid(path, "page id 必须是非空字符串")); - }; - result.insert(key.clone(), page_id.to_string()); - } - Ok(result) + Ok(load_local_folder_metadata(&canonical_root)?.trash_entries) } fn load_metadata_value_map(path: &Path) -> Result, WebError> { @@ -3767,42 +4599,7 @@ pub fn initialize_local_page_id(root_uri: &str, relative_path: &str) -> Result, -) -> Result<(), WebError> { - let mnote_dir = root.join(".mnote"); - fs::create_dir_all(&mnote_dir).map_err(|error| { - WebError::bad_request_code( - "local_metadata_write_failed", - format!("无法创建本地元数据目录 {}: {error}", mnote_dir.display()), - ) - })?; - let path = mnote_dir.join("page-ids.json"); - let value = json!({ - "version": 1, - "pages": page_ids, - }); - write_json_atomic(&path, &value) + Ok(local_markdown_path_page_id(relative_path)) } fn write_trash_index_metadata( @@ -3909,7 +4706,7 @@ fn scan_directory( expandable: entry.is_dir && child_count > 0, expanded_by_default: depth < 1 && entry.is_dir && entry_count <= 80, document_id: if is_markdown_file(&entry.file_name) { - Some(local_markdown_page_id(&entry.relative_path, None, metadata)) + Some(local_markdown_path_page_id(&entry.relative_path)) } else { None }, @@ -3931,32 +4728,6 @@ fn scan_directory( } } - if rows.is_empty() && directory == root { - rows.push(LocalFolderRow { - node_id: local_node_id("."), - row_id: "local:folder:.".to_string(), - parent_node_id: None, - title: root - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("local folder") - .to_string(), - depth: 0, - position: 0, - row_kind: "folder".to_string(), - icon_hint: "folder".to_string(), - relative_path: ".".to_string(), - source_uri: root_source_uri.to_string(), - child_count: 0, - expandable: false, - expanded_by_default: false, - document_id: None, - capabilities: local_path_capabilities(root), - workspace_id: workspace_id.to_string(), - root_source_uri: root_source_uri.to_string(), - }); - } - Ok(()) } @@ -4018,13 +4789,6 @@ fn local_entry_capabilities(entry: &LocalFolderEntry) -> Vec { capabilities } -fn local_path_capabilities(path: &Path) -> Vec { - match fs::symlink_metadata(path) { - Ok(metadata) if metadata.permissions().readonly() => vec!["readonly".to_string()], - _ => Vec::new(), - } -} - fn normalize_relative_path(root: &Path, path: &Path) -> Result { if !path.starts_with(root) { return Err(WebError::bad_request_code( @@ -4088,6 +4852,7 @@ fn scan_markdown_page_tree( directory: &Path, parent_node_id: Option, depth: u32, + skip_markdown_relative_path: Option, metadata: &LocalFolderMetadata, rows: &mut Vec, workspace_id: &str, @@ -4097,39 +4862,106 @@ fn scan_markdown_page_tree( let mut directory_rows = Vec::::new(); let mut contains_markdown = false; for (position, entry) in entries.into_iter().enumerate() { + if skip_markdown_relative_path + .as_deref() + .map(|skip| skip == entry.relative_path) + .unwrap_or(false) + { + continue; + } if entry.is_dir && !entry.is_symlink { - let node_id = local_directory_group_id(&entry.relative_path); + if let Some(nested_main) = nested_bundle_main_markdown(&entry.path) { + let nested_relative = normalize_relative_path(root, &nested_main)?; + let markdown = fs::read_to_string(&nested_main).unwrap_or_default(); + let parsed = parse_markdown_page( + &markdown, + nested_main + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("page.md"), + ); + let page_id = local_markdown_path_page_id(&nested_relative); + let mut child_rows = Vec::new(); + let child_contains_markdown = scan_markdown_page_tree( + root, + &entry.path, + Some(page_id.clone()), + depth + 1, + Some(nested_relative.clone()), + metadata, + &mut child_rows, + workspace_id, + root_source_uri, + )?; + let child_count = child_rows.len() as u32; + directory_rows.push(LocalFolderRow { + node_id: page_id.clone(), + row_id: format!("local:page:{nested_relative}"), + parent_node_id: parent_node_id.clone(), + title: parsed.title, + depth, + position: position as u32, + row_kind: "document".to_string(), + icon_hint: "markdown".to_string(), + relative_path: nested_relative, + source_uri: file_uri_for_path(&nested_main), + child_count, + expandable: child_contains_markdown, + expanded_by_default: child_contains_markdown && depth < 2, + document_id: Some(page_id), + capabilities: local_entry_capabilities(&entry), + workspace_id: workspace_id.to_string(), + root_source_uri: root_source_uri.to_string(), + }); + directory_rows.extend(child_rows); + contains_markdown = true; + continue; + } + // VSCode 风格 loose Markdown 也可作为页面节点管理; + // 若同级存在同名目录,该目录中的 Markdown 作为该页面的子页面投影。 + let sibling_md = directory.join(format!("{}.md", entry.file_name)); + let has_sibling_md = + sibling_md.exists() && sibling_md.starts_with(root) && sibling_md.is_file(); + let parent_for_children = if has_sibling_md { + let sibling_relative = normalize_relative_path(root, &sibling_md)?; + Some(local_markdown_path_page_id(&sibling_relative)) + } else { + Some(local_directory_group_id(&entry.relative_path)) + }; let mut child_rows = Vec::new(); let child_contains_markdown = scan_markdown_page_tree( root, &entry.path, - Some(node_id.clone()), + parent_for_children, depth + 1, + None, metadata, &mut child_rows, workspace_id, root_source_uri, )?; if child_contains_markdown { - directory_rows.push(LocalFolderRow { - node_id, - row_id: format!("local:page-group:{}", entry.relative_path), - parent_node_id: parent_node_id.clone(), - title: entry.file_name.clone(), - depth, - position: position as u32, - row_kind: "document".to_string(), - icon_hint: "folder".to_string(), - relative_path: entry.relative_path.clone(), - source_uri: file_uri_for_path(&entry.path), - child_count: 0, - expandable: true, - expanded_by_default: depth < 2, - document_id: None, - capabilities: local_entry_capabilities(&entry), - workspace_id: workspace_id.to_string(), - root_source_uri: root_source_uri.to_string(), - }); + if !has_sibling_md { + directory_rows.push(LocalFolderRow { + node_id: local_directory_group_id(&entry.relative_path), + row_id: format!("local:page-group:{}", entry.relative_path), + parent_node_id: parent_node_id.clone(), + title: entry.file_name.clone(), + depth, + position: position as u32, + row_kind: "document".to_string(), + icon_hint: "folder".to_string(), + relative_path: entry.relative_path.clone(), + source_uri: file_uri_for_path(&entry.path), + child_count: 0, + expandable: true, + expanded_by_default: depth < 2, + document_id: None, + capabilities: local_entry_capabilities(&entry), + workspace_id: workspace_id.to_string(), + root_source_uri: root_source_uri.to_string(), + }); + } directory_rows.extend(child_rows); contains_markdown = true; } @@ -4138,8 +4970,15 @@ fn scan_markdown_page_tree( if is_markdown_file(&entry.file_name) { let markdown = fs::read_to_string(&entry.path).unwrap_or_default(); let parsed = parse_markdown_page(&markdown, &entry.file_name); - let page_id = - local_markdown_page_id(&entry.relative_path, parsed.mnote_id.as_deref(), metadata); + let page_id = local_markdown_path_page_id(&entry.relative_path); + let md_stem = entry + .path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + let sibling_dir = directory.join(&md_stem); + let has_sibling_dir = sibling_dir.is_dir() && sibling_dir.starts_with(root); directory_rows.push(LocalFolderRow { node_id: page_id.clone(), row_id: format!("local:page:{}", entry.relative_path), @@ -4152,8 +4991,8 @@ fn scan_markdown_page_tree( relative_path: entry.relative_path.clone(), source_uri: file_uri_for_path(&entry.path), child_count: 0, - expandable: false, - expanded_by_default: false, + expandable: has_sibling_dir, + expanded_by_default: has_sibling_dir && depth < 2, document_id: Some(page_id), capabilities: local_entry_capabilities(&entry), workspace_id: workspace_id.to_string(), @@ -4187,11 +5026,8 @@ fn find_markdown_by_page_id( if !is_markdown_file(&entry.file_name) { continue; } - let markdown = fs::read_to_string(&entry.path).unwrap_or_default(); - let parsed = parse_markdown_page(&markdown, &entry.file_name); - let page_id = - local_markdown_page_id(&entry.relative_path, parsed.mnote_id.as_deref(), metadata); - if page_id == document_id { + let path_page_id = local_markdown_path_page_id(&entry.relative_path); + if path_page_id == document_id { return Ok(Some(entry)); } } @@ -4279,38 +5115,10 @@ fn local_directory_group_id(relative_path: &str) -> String { format!("local-dir:{}", encode_local_id_segment(relative_path)) } -fn local_markdown_page_id( - relative_path: &str, - mnote_id: Option<&str>, - metadata: &LocalFolderMetadata, -) -> String { - if let Some(mnote_id) = mnote_id.map(str::trim).filter(|value| !value.is_empty()) { - if mnote_id.starts_with("local-mdid:") || mnote_id.starts_with("local-md:") { - return mnote_id.to_string(); - } - return format!("local-mdid:{}", encode_local_id_segment(mnote_id)); - } - if let Some(page_id) = metadata - .page_ids - .get(relative_path) - .map(String::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - { - return normalize_metadata_page_id(page_id); - } +fn local_markdown_path_page_id(relative_path: &str) -> String { format!("local-md:{}", encode_local_id_segment(relative_path)) } -fn normalize_metadata_page_id(page_id: &str) -> String { - let trimmed = page_id.trim(); - if trimmed.starts_with("local-md:") || trimmed.starts_with("local-mdid:") { - trimmed.to_string() - } else { - format!("local-mdid:{}", encode_local_id_segment(trimmed)) - } -} - fn page_options_from_metadata(value: &Value) -> PageOptions { let mut options = PageOptions::default(); if let Some(wide_layout) = value @@ -4367,7 +5175,7 @@ fn page_options_from_metadata(value: &Value) -> PageOptions { options } -fn local_workspace_id(root: &Path) -> String { +pub(crate) fn local_workspace_id(root: &Path) -> String { if let Ok(manifest) = load_local_workspace_manifest(root) { if !manifest.workspace_id.trim().is_empty() { return manifest.workspace_id; @@ -4458,7 +5266,9 @@ fn icon_hint_for_entry(entry: &LocalFolderEntry) -> String { return "symlink".to_string(); } let lower = entry.file_name.to_ascii_lowercase(); - if lower.ends_with(".md") || lower.ends_with(".markdown") { + if is_local_mindmap_file_name(&entry.file_name) { + "mindmap" + } else if lower.ends_with(".md") || lower.ends_with(".markdown") { "markdown" } else if matches!( extension(&lower).as_deref(), @@ -4477,13 +5287,25 @@ fn icon_hint_for_entry(entry: &LocalFolderEntry) -> String { "audio" } else if lower.ends_with(".pdf") { "pdf" - } else if matches!(extension(&lower).as_deref(), Some("epub" | "mobi")) { - "book" } else if matches!( extension(&lower).as_deref(), - Some("csv" | "tsv" | "xlsx" | "xls") + Some( + "doc" + | "docx" + | "odt" + | "rtf" + | "ppt" + | "pptx" + | "odp" + | "xls" + | "xlsx" + | "ods" + | "csv" + ) ) { - "table" + "office" + } else if matches!(extension(&lower).as_deref(), Some("epub" | "mobi")) { + "book" } else { "unknown" } @@ -4497,7 +5319,7 @@ fn extension(file_name: &str) -> Option { .map(ToOwned::to_owned) } -fn encode_local_id_segment(value: &str) -> String { +pub(crate) fn encode_local_id_segment(value: &str) -> String { let mut encoded = String::with_capacity(value.len()); for byte in value.as_bytes() { let character = *byte as char; @@ -4538,38 +5360,6 @@ pub(crate) fn decode_local_id_segment(value: &str) -> Result { .map_err(|_| WebError::bad_request_code("local_id_invalid", "本地 page id 不是 UTF-8")) } -fn rewrite_page_ids_prefix( - page_ids: &mut BTreeMap, - old_relative_path: &str, - new_relative_path: &str, -) { - let old_prefix = format!("{old_relative_path}/"); - let replacements = page_ids - .iter() - .filter_map(|(relative_path, page_id)| { - if relative_path == old_relative_path { - Some(( - relative_path.clone(), - new_relative_path.to_string(), - page_id.clone(), - )) - } else if let Some(rest) = relative_path.strip_prefix(&old_prefix) { - Some(( - relative_path.clone(), - format!("{new_relative_path}/{rest}"), - page_id.clone(), - )) - } else { - None - } - }) - .collect::>(); - for (old_key, new_key, page_id) in replacements { - page_ids.remove(&old_key); - page_ids.insert(new_key, page_id); - } -} - fn sanitize_file_stem(value: &str, fallback: &str) -> String { let sanitized = value .trim() @@ -4630,6 +5420,25 @@ fn next_available_path(directory: &Path, stem: &str, extension: &str) -> PathBuf directory.join(format!("{stem}-{}.{}", std::process::id(), extension)) } +fn copy_directory_recursively(source: &Path, target: &Path) -> std::io::Result<()> { + fs::create_dir_all(target)?; + for entry in fs::read_dir(source)? { + let entry = entry?; + let source_path = entry.path(); + let target_path = target.join(entry.file_name()); + let metadata = fs::symlink_metadata(&source_path)?; + if metadata.file_type().is_symlink() { + continue; + } + if metadata.is_dir() { + copy_directory_recursively(&source_path, &target_path)?; + } else if metadata.is_file() { + fs::copy(&source_path, &target_path)?; + } + } + Ok(()) +} + fn next_available_raw_path(directory: &Path, file_name: &str) -> PathBuf { let first = directory.join(file_name); if !first.exists() { @@ -4720,29 +5529,18 @@ fn next_available_directory_path(directory: &Path, stem: &str) -> PathBuf { directory.join(format!("{stem}-{}", std::process::id())) } -fn markdown_with_frontmatter_fields(markdown: &str, fields: &[(&str, &str)]) -> String { - let (frontmatter, body) = split_frontmatter(markdown); - let mut lines = frontmatter - .unwrap_or_default() - .lines() - .map(ToOwned::to_owned) - .collect::>(); - for (key, value) in fields { - let replacement = format!("{key}: {}", value.trim()); - if let Some(existing) = lines.iter_mut().find(|line| { - line.split_once(':') - .map(|(candidate, _)| candidate.trim() == *key) - .unwrap_or(false) - }) { - *existing = replacement; - } else { - lines.push(replacement); - } - } - format!("---\n{}\n---\n{}", lines.join("\n"), body) +fn editor_blocks_to_markdown_for_file( + content: &Value, + root: &Path, + markdown_path: &Path, +) -> String { + editor_blocks_to_markdown_with_rewrite(content, Some((root, markdown_path))) } -fn editor_blocks_to_markdown(content: &Value) -> String { +fn editor_blocks_to_markdown_with_rewrite( + content: &Value, + local_file_context: Option<(&Path, &Path)>, +) -> String { let blocks = if let Some(array) = content.as_array() { array.clone() } else { @@ -4769,7 +5567,12 @@ fn editor_blocks_to_markdown(content: &Value) -> String { } else { inline_markdown }; - if text.is_empty() && !matches!(block_type, "divider" | "media" | "image" | "table") { + if text.is_empty() + && !matches!( + block_type, + "divider" | "media" | "image" | "mindmap" | "table" + ) + { lines.push(String::new()); continue; } @@ -4789,7 +5592,13 @@ fn editor_blocks_to_markdown(content: &Value) -> String { if src.is_empty() { lines.push(text); } else { - lines.push(format!("![{}]({src})", alt.replace(']', r"\]"))); + let src = rewrite_local_open_url_to_markdown_relative(src, local_file_context) + .unwrap_or_else(|| src.to_string()); + lines.push(format!( + "![{}]({})", + alt.replace(']', r"\]"), + markdown_link_target(&src) + )); } } "media" => { @@ -4812,10 +5621,49 @@ fn editor_blocks_to_markdown(content: &Value) -> String { if url.is_empty() { lines.push(text); } else { - lines.push(format!( - "[{}]({url})", - if name.is_empty() { url } else { name } - )); + let url = rewrite_local_open_url_to_markdown_relative(url, local_file_context) + .unwrap_or_else(|| url.to_string()); + let label = if name.is_empty() { url.as_str() } else { name }; + lines.push(format!("[{}]({})", label, markdown_link_target(&url))); + } + } + "mindmap" => { + let props = block.get("props").and_then(Value::as_object); + let source = props + .and_then(|props| { + props + .get("sourcePath") + .or_else(|| props.get("mindmapId")) + .or_else(|| props.get("mindmap_id")) + }) + .and_then(Value::as_str) + .unwrap_or("") + .trim(); + let label = props + .and_then(|props| props.get("name").or_else(|| props.get("title"))) + .and_then(Value::as_str) + .unwrap_or("思维导图") + .trim(); + if source.is_empty() { + lines.push(text); + } else { + let source = rewrite_local_mindmap_source_to_markdown_relative( + source, + local_file_context, + ) + .unwrap_or_else(|| { + if is_local_mindmap_file_name(source) || source.contains('/') { + source.to_string() + } else { + local_mindmap_file_name(source) + } + }); + let label = if label.is_empty() { + "思维导图" + } else { + label + }; + lines.push(format!("[{}]({})", label, markdown_link_target(&source))); } } "heading" => { @@ -4871,6 +5719,91 @@ fn editor_blocks_to_markdown(content: &Value) -> String { } } +fn markdown_link_target(target: &str) -> String { + let trimmed = target.trim(); + if trimmed.is_empty() { + return String::new(); + } + if trimmed + .chars() + .any(|ch| ch.is_whitespace() || matches!(ch, '(' | ')' | '<' | '>')) + { + format!("<{}>", trimmed.replace('>', "%3E")) + } else { + trimmed.to_string() + } +} + +fn rewrite_local_mindmap_source_to_markdown_relative( + value: &str, + local_file_context: Option<(&Path, &Path)>, +) -> Option { + if let Some(relative_path) = local_mindmap_relative_path_from_id(value) { + let (root, markdown_path) = local_file_context?; + let asset_path = root.canonicalize().ok()?.join(relative_path); + let markdown_dir = markdown_path.parent()?; + return normalize_markdown_relative_asset_path(markdown_dir, &asset_path).ok(); + } + rewrite_local_open_url_to_markdown_relative(value, local_file_context) +} + +fn rewrite_local_open_url_to_markdown_relative( + value: &str, + local_file_context: Option<(&Path, &Path)>, +) -> Option { + let (root, markdown_path) = local_file_context?; + let parsed = Url::parse(value).ok()?; + if parsed.path() != "/api/local-folder/files/open" { + return None; + } + let root_uri = parsed + .query_pairs() + .find(|(key, _)| key == "rootUri") + .map(|(_, value)| value.into_owned())?; + let asset_relative_path = parsed + .query_pairs() + .find(|(key, _)| key == "path") + .map(|(_, value)| value.into_owned())?; + let parsed_root = parse_file_root_uri(&root_uri).ok()?.canonicalize().ok()?; + let canonical_root = root.canonicalize().ok()?; + if parsed_root != canonical_root { + return None; + } + let asset_path = canonical_root.join(asset_relative_path); + let markdown_dir = markdown_path.parent()?; + normalize_markdown_relative_asset_path(markdown_dir, &asset_path).ok() +} + +fn content_type_for_path(path: &Path) -> HeaderValue { + let extension = path + .extension() + .and_then(|value| value.to_str()) + .unwrap_or("") + .to_ascii_lowercase(); + let content_type = match extension.as_str() { + "jpg" | "jpeg" => "image/jpeg", + "png" => "image/png", + "gif" => "image/gif", + "webp" => "image/webp", + "svg" => "image/svg+xml; charset=utf-8", + "md" | "markdown" => "text/markdown; charset=utf-8", + "txt" | "log" => "text/plain; charset=utf-8", + "json" => "application/json; charset=utf-8", + "pdf" => "application/pdf", + "doc" => "application/msword", + "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "ppt" => "application/vnd.ms-powerpoint", + "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "xls" => "application/vnd.ms-excel", + "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "odt" => "application/vnd.oasis.opendocument.text", + "odp" => "application/vnd.oasis.opendocument.presentation", + "ods" => "application/vnd.oasis.opendocument.spreadsheet", + _ => "application/octet-stream", + }; + HeaderValue::from_static(content_type) +} + fn editor_block_table_to_markdown(block: &Value) -> String { let table = block .get("props") @@ -5241,17 +6174,19 @@ fn markdown_page_subtree(document_id: &str, title: &str, content: &Value) -> Val mod tests { use super::{ add_local_access_grant_for_context, create_default_local_workspace_for_actor_at_base, - create_local_access_grant, create_share_grant, ensure_local_path_read_access, - ensure_local_workspace_access_for_actor, ensure_local_workspace_read_access_for_actor, - execute_local_tree_command, get_local_access_policy, get_share_grants, - initialize_local_page_id, initialize_local_workspace_for_actor, - load_local_folder_page_tree_snapshot, local_folder_watch_revision, open_local_file, - record_shared_cache, record_sync_pending_change, resolve_local_markdown_page_aggregate, - save_local_markdown_page, validate_local_access_root, write_local_markdown_asset, - write_local_markdown_page_body, write_sync_conflict_report, LocalAccessGrantRequest, - LocalAccessValidateRootRequest, LocalFileOpenQuery, LocalShareGrantRequest, - LocalUploadFile, SharedCacheRecordRequest, SyncConflictReportRequest, - SyncPendingChangeRequest, + create_local_access_grant, create_share_grant, encode_local_id_segment, + ensure_local_path_read_access, ensure_local_workspace_access_for_actor, + ensure_local_workspace_read_access_for_actor, execute_local_tree_command, + get_local_access_policy, get_share_grants, initialize_local_page_id, + initialize_local_workspace_for_actor, load_local_folder_file_tree_snapshot, + load_local_folder_page_tree_snapshot, local_folder_watch_revision, local_workspace_id, + open_local_file, record_shared_cache, record_sync_pending_change, + resolve_local_markdown_page_aggregate, save_local_markdown_page, + update_local_markdown_title, validate_local_access_root, write_local_markdown_asset, + write_local_markdown_page_body, write_local_mindmap_data, write_sync_conflict_report, + LocalAccessGrantRequest, LocalAccessValidateRootRequest, LocalFileOpenQuery, + LocalShareGrantRequest, LocalUploadFile, SharedCacheRecordRequest, + SyncConflictReportRequest, SyncPendingChangeRequest, }; use crate::context::RequestContext; use axum::extract::{Extension, Path as AxumPath, Query}; @@ -5288,8 +6223,8 @@ mod tests { } #[test] - fn local_frontmatter_mnote_id_survives_rename_and_move() { - let root = temp_root("mnote-local-frontmatter-id"); + fn local_markdown_identity_uses_path_even_when_frontmatter_has_mnote_id() { + let root = temp_root("mnote-local-frontmatter-path-id"); init_workspace(&root); std::fs::write( root.join("page.md"), @@ -5299,24 +6234,24 @@ mod tests { let root_uri = format!("file://{}", root.display()); let first = load_local_folder_page_tree_snapshot(&root_uri).expect("first snapshot"); - assert!(first - .projection - .to_string() - .contains("local-mdid:stable-frontmatter-id")); + let first_json = first.projection.to_string(); + assert!(first_json.contains("local-md:page.md")); + assert!(!first_json.contains("local-mdid:stable-frontmatter-id")); std::fs::create_dir_all(root.join("docs")).expect("create docs"); std::fs::rename(root.join("page.md"), root.join("docs").join("renamed.md")) .expect("move md"); let second = load_local_folder_page_tree_snapshot(&root_uri).expect("second snapshot"); let second_json = second.projection.to_string(); - assert!(second_json.contains("local-mdid:stable-frontmatter-id")); + assert!(second_json.contains("local-md:docs~2Frenamed.md")); + assert!(!second_json.contains("local-mdid:stable-frontmatter-id")); assert!(second_json.contains("renamed.md")); let _ = std::fs::remove_dir_all(&root); } #[test] - fn local_page_ids_metadata_overrides_path_derived_id_and_hides_mnote() { + fn local_page_ids_metadata_does_not_override_path_derived_id() { let root = temp_root("mnote-local-page-ids"); init_workspace(&root); std::fs::create_dir_all(root.join(".mnote")).expect("create metadata dir"); @@ -5331,25 +6266,23 @@ mod tests { let snapshot = load_local_folder_page_tree_snapshot(&root_uri).expect("snapshot"); let html_json = snapshot.projection.to_string(); - assert!(html_json.contains("local-mdid:stable-from-page-ids")); + assert!(html_json.contains("local-md:docs~2Fpage.md")); + assert!(!html_json.contains("local-mdid:stable-from-page-ids")); assert!(!html_json.contains("page-ids.json")); let _ = std::fs::remove_dir_all(&root); } #[test] - fn local_page_id_initialization_writes_page_ids_atomically() { + fn local_page_id_initialization_returns_path_id_without_writing_page_ids() { let root = temp_root("mnote-local-page-id-init"); init_workspace(&root); std::fs::write(root.join("draft.md"), "# Draft\n").expect("write md"); let root_uri = format!("file://{}", root.display()); let page_id = initialize_local_page_id(&root_uri, "draft.md").expect("init page id"); - assert!(page_id.starts_with("local-mdid:")); - let metadata = std::fs::read_to_string(root.join(".mnote").join("page-ids.json")) - .expect("read page ids"); - let parsed: Value = serde_json::from_str(&metadata).expect("json"); - assert_eq!(parsed["pages"]["draft.md"], page_id); + assert_eq!(page_id, "local-md:draft.md"); + assert!(!root.join(".mnote").join("page-ids.json").exists()); assert!(!root.join(".mnote").join("page-ids.json.tmp").exists()); let _ = std::fs::remove_dir_all(&root); @@ -5360,7 +6293,7 @@ mod tests { let root = temp_root("mnote-local-bad-metadata"); std::fs::create_dir_all(root.join(".mnote")).expect("create metadata"); std::fs::write(root.join("README.md"), "# Broken Metadata\n").expect("write md"); - std::fs::write(root.join(".mnote").join("page-ids.json"), "{not-json") + std::fs::write(root.join(".mnote").join("page-options.json"), "{not-json") .expect("write bad metadata"); let root_uri = format!("file://{}", root.display()); @@ -5392,6 +6325,29 @@ mod tests { let _ = std::fs::remove_dir_all(&root); } + #[test] + fn local_page_aggregate_accepts_path_id_when_frontmatter_has_mnote_id() { + let root = temp_root("mnote-local-path-id-frontmatter"); + init_workspace(&root); + std::fs::create_dir_all(root.join("pages")).expect("create pages"); + std::fs::write( + root.join("pages").join("初始化的新页面.md"), + "---\ntitle: 初始化的新页面\nmnote_id: initial-page\n---\n# 初始化的新页面\n\n正文\n", + ) + .expect("write md"); + let root_uri = format!("file://{}", root.display()); + let path_id = format!( + "local-md:{}", + encode_local_id_segment("pages/初始化的新页面.md") + ); + + let aggregate = + resolve_local_markdown_page_aggregate(&root_uri, &path_id).expect("aggregate"); + assert_eq!(aggregate.title, "初始化的新页面"); + + let _ = std::fs::remove_dir_all(&root); + } + #[test] fn local_markdown_parser_preserves_pipe_table_as_table_block() { let blocks = crate::routes::local_markdown_parser::markdown_to_blocks( @@ -5591,7 +6547,7 @@ mod tests { editor_source: Some("unit-test".into()), }; - let result = write_local_markdown_page_body(&request).expect("write"); + let result = write_local_markdown_page_body(&request, None).expect("write"); assert_eq!(result["canonicalCommand"], "page.body.write"); assert_eq!(result["compatCommand"], "page.body.save"); @@ -5666,7 +6622,7 @@ fn main() {} let root_uri = format!("file://{}", root.display()); save_local_markdown_page( &root_uri, - "local-mdid:stable", + "local-md:README.md", None, &serde_json::json!([ {"type":"heading","props":{"level":2},"content":[{"type":"text","text":"Heading"}]}, @@ -5905,6 +6861,96 @@ fn main() {} let _ = std::fs::remove_dir_all(&root); } + #[test] + fn local_markdown_save_rewrites_local_file_open_url_to_markdown_relative_path() { + let root = temp_root("mnote-local-markdown-save-image-open-url"); + init_workspace(&root); + std::fs::create_dir_all(root.join("Page")).expect("create page dir"); + std::fs::write(root.join("Page").join("Page.md"), "").expect("write md"); + std::fs::write(root.join("Page").join("屏幕截图 2025.png"), b"jpg").expect("write image"); + let root_uri = format!("file://{}", root.display()); + let image_url = format!( + "http://localhost:3000/api/local-folder/files/open?rootUri={}&path=Page%2F%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE%202025.png", + root_uri + ); + + save_local_markdown_page( + &root_uri, + "local-md:Page~2FPage.md", + None, + &serde_json::json!([ + { + "type": "image", + "props": { + "src": image_url, + "alt": "示例图片" + } + } + ]), + ) + .expect("save"); + + let saved = std::fs::read_to_string(root.join("Page").join("Page.md")).expect("read md"); + assert!(saved.contains("![示例图片](<屏幕截图 2025.png>)")); + assert!(!saved.contains("/api/local-folder/files/open")); + let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:Page~2FPage.md") + .expect("aggregate"); + let image = aggregate.body.content.as_array().expect("blocks")[0].clone(); + assert_eq!(image["type"], "image"); + assert_eq!(image["props"]["src"], "屏幕截图 2025.png"); + + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn local_markdown_mindmap_blocks_roundtrip_next_to_page() { + let root = temp_root("mnote-local-markdown-mindmap-roundtrip"); + init_workspace(&root); + std::fs::create_dir_all(root.join("Page")).expect("create page dir"); + std::fs::write(root.join("Page").join("Page.md"), "").expect("write md"); + let root_uri = format!("file://{}", root.display()); + + let write_result = write_local_mindmap_data( + &root_uri, + "local-md:Page~2FPage.md", + "思维导图123456.json", + serde_json::json!({"data":{"text":"中心主题"},"children":[]}), + false, + ) + .expect("write mindmap"); + assert_eq!(write_result["relativePath"], "Page/思维导图123456.json"); + assert!(root.join("Page").join("思维导图123456.json").is_file()); + + save_local_markdown_page( + &root_uri, + "local-md:Page~2FPage.md", + None, + &serde_json::json!([ + { + "type": "mindmap", + "props": { + "name": "主页思维导图", + "mindmapId": "思维导图123456.json", + "rootNodeId": "root" + } + } + ]), + ) + .expect("save"); + + let saved = std::fs::read_to_string(root.join("Page").join("Page.md")).expect("read md"); + assert!(saved.contains("[主页思维导图](思维导图123456.json)")); + + let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:Page~2FPage.md") + .expect("aggregate"); + let mindmap = aggregate.body.content.as_array().expect("blocks")[0].clone(); + assert_eq!(mindmap["type"], "mindmap"); + assert_eq!(mindmap["props"]["mindmapId"], "思维导图123456.json"); + assert_eq!(mindmap["props"]["sourcePath"], "思维导图123456.json"); + + let _ = std::fs::remove_dir_all(&root); + } + #[test] fn local_workspace_access_rejects_owner_mismatch() { let root = temp_root("mnote-local-workspace-owner-mismatch"); @@ -6388,9 +7434,14 @@ fn main() {} assert_eq!(std::path::Path::new(root_path), expected_root.as_path()); assert!(expected_root.join(".mnote").join("workspace.json").exists()); - assert!(expected_root.join("pages").join("我的空间.md").exists()); - assert!(expected_root.join("assets").join("shared").is_dir()); - assert!(expected_root.join("ai-sessions").join("private").is_dir()); + assert!(expected_root + .join("初始化的新页面") + .join("初始化的新页面.md") + .exists()); + assert!(!expected_root.join("pages").exists()); + assert!(!expected_root.join("assets").exists()); + assert!(!expected_root.join("mindmaps").exists()); + assert!(!expected_root.join("ai-sessions").exists()); assert_eq!( payload["workspace"]["manifest"]["workspaceId"], "local-ws:user~40example.com:my-space" @@ -6418,6 +7469,23 @@ fn main() {} let _ = std::fs::remove_dir_all(&base); } + #[test] + fn local_file_tree_empty_root_does_not_emit_workspace_folder_row() { + let root = temp_root("mnote-local-empty-root-file-tree"); + init_workspace(&root); + let root_uri = format!("file://{}", root.display()); + + let snapshot = load_local_folder_file_tree_snapshot(&root_uri).expect("load file tree"); + let items = snapshot.projection["items"].as_array().expect("items"); + + assert!( + items.is_empty(), + "只剩 .mnote 的空 workspace 不应投影出 root 文件夹行: {items:?}" + ); + + let _ = std::fs::remove_dir_all(&root); + } + #[test] fn local_tree_command_refreshes_search_index_after_rename() { let root = temp_root("mnote-local-tree-refresh-search-index"); @@ -6458,6 +7526,155 @@ fn main() {} let _ = std::fs::remove_dir_all(&root); } + #[test] + fn local_markdown_page_title_comes_from_file_name_not_body_heading() { + let root = temp_root("mnote-local-title-file-name"); + init_workspace(&root); + std::fs::write(root.join("File Name.md"), "# Body Heading\n").expect("write md"); + let root_uri = format!("file://{}", root.display()); + + let snapshot = load_local_folder_page_tree_snapshot(&root_uri).expect("page tree"); + let items = snapshot.projection["items"].as_array().expect("items"); + let row = items + .iter() + .find(|item| { + item["resourceMeta"]["documentId"].as_str() == Some("local-md:File~20Name.md") + }) + .expect("page row"); + + assert_eq!(row["title"].as_str(), Some("File Name")); + + let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:File~20Name.md") + .expect("aggregate"); + assert_eq!(aggregate.title, "File Name"); + + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn local_document_title_save_renames_nested_bundle_directory_and_markdown() { + let root = temp_root("mnote-local-title-save-renames-file"); + init_workspace(&root); + std::fs::create_dir_all(root.join("Old")).expect("create Old dir"); + std::fs::write(root.join("Old").join("Old.md"), "# Body Heading\n").expect("write Old.md"); + std::fs::write(root.join("Old").join("Child.md"), "# Child\n").expect("write child"); + let root_uri = format!("file://{}", root.display()); + + let result = update_local_markdown_title(&root_uri, "local-md:Old~2FOld.md", "New") + .expect("save title"); + + assert!( + root.join("New").join("New.md").is_file(), + "New/New.md should exist" + ); + assert!( + root.join("New").join("Child.md").is_file(), + "New/Child.md should exist" + ); + assert!(!root.join("Old").exists(), "Old/ should not exist"); + assert_eq!(result["documentId"].as_str(), Some("local-md:New~2FNew.md")); + assert_eq!( + result["previousDocumentId"].as_str(), + Some("local-md:Old~2FOld.md") + ); + assert_eq!(result["title"].as_str(), Some("New")); + + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn local_tree_command_manages_loose_markdown_like_regular_file() { + let root = temp_root("mnote-local-loose-markdown-file-management"); + init_workspace(&root); + std::fs::create_dir_all(root.join("docs")).expect("create docs"); + std::fs::write(root.join("docs").join("README.md"), "# Loose\n").expect("write loose md"); + let root_uri = format!("file://{}", root.display()); + + let renamed = execute_local_tree_command( + &root_uri, + "rename", + "local-md:docs~2FREADME.md", + None, + Some("Renamed"), + ) + .expect("rename loose markdown"); + assert!(root.join("docs").join("Renamed.md").is_file()); + assert!(!root.join("docs").join("README.md").exists()); + assert_eq!( + renamed["documentId"].as_str(), + Some("local-md:docs~2FRenamed.md") + ); + + let moved = + execute_local_tree_command(&root_uri, "move", "local-md:docs~2FRenamed.md", None, None) + .expect("move loose markdown"); + assert!(root.join("Renamed.md").is_file()); + assert!(!root.join("docs").join("Renamed.md").exists()); + assert_eq!(moved["documentId"].as_str(), Some("local-md:Renamed.md")); + + let copied = + execute_local_tree_command(&root_uri, "copy", "local-md:Renamed.md", None, None) + .expect("copy loose markdown"); + assert!(root.join("Renamed 2.md").is_file()); + assert_eq!( + copied["documentId"].as_str(), + Some("local-md:Renamed~202.md") + ); + + let deleted = + execute_local_tree_command(&root_uri, "delete", "local-md:Renamed.md", None, None) + .expect("delete loose markdown"); + assert!(!root.join("Renamed.md").exists()); + assert!(root + .join(".mnote") + .join("trash") + .join("Renamed.md") + .is_file()); + assert_eq!(deleted["resourceKind"].as_str(), Some("markdown")); + + let restored = + execute_local_tree_command(&root_uri, "restore", "local-md:Renamed.md", None, None) + .expect("restore loose markdown"); + assert!(root.join("Renamed.md").is_file()); + assert_eq!(restored["documentId"].as_str(), Some("local-md:Renamed.md")); + + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn local_tree_command_delete_folder_moves_directory_to_trash() { + let root = temp_root("mnote-local-delete-folder"); + init_workspace(&root); + std::fs::create_dir_all(root.join("Folder").join("Nested")).expect("create folder"); + std::fs::write(root.join("Folder").join("Nested").join("note.txt"), "note") + .expect("write nested file"); + let root_uri = format!("file://{}", root.display()); + + let folder_id = format!("local-dir:{}", encode_local_id_segment("Folder")); + let result = execute_local_tree_command(&root_uri, "delete", &folder_id, None, None) + .expect("delete folder"); + + assert!( + !root.join("Folder").exists(), + "Folder should move away from root" + ); + let trash_path = result["trashPath"].as_str().expect("trashPath"); + assert!( + root.join(trash_path) + .join("Nested") + .join("note.txt") + .is_file(), + "folder contents should exist in trash" + ); + assert_eq!(result["resourceKind"].as_str(), Some("local_directory")); + assert_eq!( + result["trashEntryId"].as_str(), + Some("local-dir-trash:Folder") + ); + + let _ = std::fs::remove_dir_all(&root); + } + #[test] fn local_markdown_save_refreshes_search_index_after_write() { let root = temp_root("mnote-local-save-refresh-search-index"); @@ -6500,45 +7717,6 @@ fn main() {} let _ = std::fs::remove_dir_all(&root); } - #[test] - fn local_markdown_asset_upload_copies_next_to_markdown_with_relative_path() { - let root = temp_root("mnote-local-markdown-asset-upload"); - init_workspace(&root); - std::fs::create_dir_all(root.join("docs")).expect("create docs"); - std::fs::write(root.join("docs").join("README.md"), "# Assets\n").expect("write md"); - std::fs::create_dir_all(root.join("docs").join("README.assets")).expect("create assets"); - std::fs::write( - root.join("docs").join("README.assets").join("photo.png"), - b"old", - ) - .expect("write existing asset"); - let root_uri = format!("file://{}", root.display()); - - let asset = write_local_markdown_asset( - &root_uri, - "local-md:docs~2FREADME.md", - "image", - LocalUploadFile { - name: "photo.png".to_string(), - content_type: "image/png".to_string(), - bytes: b"new-image".to_vec(), - }, - ) - .expect("upload asset"); - - assert_eq!(asset["sourcePath"], "README.assets/photo-1.png"); - assert_eq!(asset["file_url"], "README.assets/photo-1.png"); - assert_eq!(asset["asset_type"], "image"); - assert_eq!(asset["sourceKind"], "local_folder"); - assert_eq!( - std::fs::read(root.join("docs").join("README.assets").join("photo-1.png")) - .expect("read copied asset"), - b"new-image" - ); - - let _ = std::fs::remove_dir_all(&root); - } - #[tokio::test] async fn local_file_open_rejects_root_escape() { let root = temp_root("mnote-local-file-open-root-escape"); @@ -6618,4 +7796,503 @@ fn main() {} let _ = std::fs::remove_dir_all(&root); let _ = std::fs::remove_dir_all(&policy_root); } + + #[tokio::test] + async fn local_file_open_sets_image_and_markdown_content_type() { + let root = temp_root("mnote-local-file-open-content-type"); + init_workspace(&root); + std::fs::write(root.join("photo.jpg"), b"jpg").expect("write image"); + std::fs::write(root.join("note.md"), "# Note\n").expect("write markdown"); + let root_uri = format!("file://{}", root.display()); + let mut headers = HeaderMap::new(); + headers.insert("x-mnote-actor-id", "user_test".parse().unwrap()); + headers.insert("x-mnote-actor-type", "user".parse().unwrap()); + let context = RequestContext::from_http_parts( + &Method::GET, + &"/api/local-folder/files/open".parse().expect("uri"), + &headers, + ); + + let (_, image_headers, _) = open_local_file( + Extension(context.clone()), + Query(LocalFileOpenQuery { + root_uri: root_uri.clone(), + path: "photo.jpg".into(), + download: None, + }), + ) + .await + .expect("open image"); + assert_eq!( + image_headers + .get(axum::http::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()), + Some("image/jpeg") + ); + + let (_, markdown_headers, _) = open_local_file( + Extension(context), + Query(LocalFileOpenQuery { + root_uri, + path: "note.md".into(), + download: None, + }), + ) + .await + .expect("open markdown"); + assert_eq!( + markdown_headers + .get(axum::http::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()), + Some("text/markdown; charset=utf-8") + ); + + let _ = std::fs::remove_dir_all(&root); + } + + #[tokio::test] + async fn local_file_open_sets_pdf_and_office_content_type() { + let root = temp_root("mnote-local-file-open-office-content-type"); + init_workspace(&root); + std::fs::write(root.join("spec.pdf"), b"pdf").expect("write pdf"); + std::fs::write(root.join("report.docx"), b"docx").expect("write docx"); + let root_uri = format!("file://{}", root.display()); + let mut headers = HeaderMap::new(); + headers.insert("x-mnote-actor-id", "user_test".parse().unwrap()); + headers.insert("x-mnote-actor-type", "user".parse().unwrap()); + let context = RequestContext::from_http_parts( + &Method::GET, + &"/api/local-folder/files/open".parse().expect("uri"), + &headers, + ); + + let (_, pdf_headers, _) = open_local_file( + Extension(context.clone()), + Query(LocalFileOpenQuery { + root_uri: root_uri.clone(), + path: "spec.pdf".into(), + download: None, + }), + ) + .await + .expect("open pdf"); + assert_eq!( + pdf_headers + .get(axum::http::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()), + Some("application/pdf") + ); + + let (_, office_headers, _) = open_local_file( + Extension(context), + Query(LocalFileOpenQuery { + root_uri, + path: "report.docx".into(), + download: None, + }), + ) + .await + .expect("open docx"); + assert_eq!( + office_headers + .get(axum::http::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()), + Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document") + ); + + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn local_file_tree_classifies_mindmap_and_office_assets() { + let root = temp_root("mnote-local-filetree-resource-kinds"); + init_workspace(&root); + std::fs::create_dir_all(root.join("Page")).expect("create page dir"); + std::fs::write(root.join("Page").join("Page.md"), "").expect("write md"); + std::fs::write(root.join("Page").join("思维导图123456.json"), "{}").expect("write mindmap"); + std::fs::write(root.join("Page").join("report.docx"), b"docx").expect("write docx"); + let root_uri = format!("file://{}", root.display()); + + let snapshot = load_local_folder_file_tree_snapshot(&root_uri).expect("file tree"); + let items = snapshot.projection["items"].as_array().expect("items"); + let mindmap = items + .iter() + .find(|item| item["title"].as_str() == Some("思维导图123456.json")) + .expect("mindmap row"); + assert_eq!(mindmap["rowKind"].as_str(), Some("asset")); + assert_eq!(mindmap["iconHint"].as_str(), Some("mindmap")); + assert_eq!( + mindmap["resourceMeta"]["workspacePath"]["objectIdentity"]["objectKind"].as_str(), + Some("mindmap") + ); + + let office = items + .iter() + .find(|item| item["title"].as_str() == Some("report.docx")) + .expect("office row"); + assert_eq!(office["rowKind"].as_str(), Some("asset")); + assert_eq!(office["iconHint"].as_str(), Some("office")); + assert_eq!( + office["resourceMeta"]["workspacePath"]["objectIdentity"]["objectKind"].as_str(), + Some("only_office") + ); + + let _ = std::fs::remove_dir_all(&root); + } + + // ── Gap A2 BufferStore 运行时接入集成测试 ──────────────────────── + + #[test] + fn document_buffer_mark_saved_after_page_body_write() { + let root = temp_root("mnote-buf-save-after-write"); + std::fs::write(root.join("README.md"), "# Old\n").expect("write md"); + let root_uri = format!("file://{}", root.display()); + + let store = crate::document_buffer_store::BufferStore::new(); + let ws_path = crate::document_buffer_store::build_local_folder_workspace_path( + &local_workspace_id(&root), + &root_uri, + "README.md", + "local-md:README.md", + ); + // 模拟编辑器打开文档:先初始化 buffer + store.get_or_create(&ws_path); + + // 经过 write_local_markdown_page_body 保存入口 + write_local_markdown_page_body( + &core_protocol::PageBodyWriteRequest { + document_id: "local-md:README.md".into(), + workspace_id: String::new(), + source_kind: core_protocol::WorkspaceSourceKind::LocalFolder, + root_uri: root_uri.clone(), + expected_file_version: None, + base_content_hash: None, + content_format: "editorBlocks".into(), + content: serde_json::json!([ + {"type":"heading","props":{"level":1},"content":[{"type":"text","text":"Written"}]} + ]), + editor_source: Some("unit-test".into()), + }, + Some(&store), + ) + .expect("write_local_markdown_page_body"); + + // 验证 buffer store 已被消费 + let buf = store + .get_by_path(&ws_path) + .expect("buffer should exist after write"); + assert_eq!( + buf.dirty_state, + core_protocol::DocBufferDirtyState::Clean, + "保存后 buffer 应为 Clean" + ); + assert!(buf.file_version.is_some(), "file_version 应被更新"); + assert!(buf.last_saved_at.is_some(), "last_saved_at 应被更新"); + + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn document_buffer_marks_external_modified_from_local_folder_event() { + let store = crate::document_buffer_store::BufferStore::new(); + let ws_path = crate::document_buffer_store::build_local_folder_workspace_path( + "ws_test", + "file:///tmp/test-root", + "doc.md", + "local-md:doc.md", + ); + // 模拟编辑器打开文档:Clean 状态 buffer + store.get_or_create(&ws_path); + + // 模拟 watcher 事件调用 + let result = store.mark_external_modified(&ws_path, Some("external-editor".into())); + assert!(result.is_some(), "外部修改应返回 buffer"); + + let buf = result.unwrap(); + assert_eq!( + buf.dirty_state, + core_protocol::DocBufferDirtyState::ExternalModified, + "Clean buffer 受外部修改后应变为 ExternalModified" + ); + assert_eq!(buf.external_actor.as_deref(), Some("external-editor")); + } + + #[test] + fn document_buffer_marks_stale_from_local_folder_event_when_dirty() { + let store = crate::document_buffer_store::BufferStore::new(); + let ws_path = crate::document_buffer_store::build_local_folder_workspace_path( + "ws_test", + "file:///tmp/test-root", + "doc.md", + "local-md:doc.md", + ); + // 模拟编辑器打开并且有未保存修改:Dirty 状态 buffer + let buf = store.get_or_create(&ws_path); + let mut dirty_buf = buf; + dirty_buf.mark_dirty("sha256:dirty_content".into()); + let key = crate::document_buffer_store::BufferKey::from_workspace_path(&ws_path); + store.update(&key, dirty_buf); + + // 模拟 watcher 事件调用 + let result = store.mark_external_modified(&ws_path, Some("vim".into())); + assert!(result.is_some(), "外部修改应返回 buffer"); + + let buf = result.unwrap(); + assert_eq!( + buf.dirty_state, + core_protocol::DocBufferDirtyState::Stale, + "Dirty buffer 受外部修改后应变为 Stale" + ); + assert_eq!(buf.external_actor.as_deref(), Some("vim")); + } + + #[test] + fn local_tree_command_create_page_creates_timestamped_nested_bundle() { + let root = temp_root("mnote-local-tree-create-page-timestamp"); + init_workspace(&root); + let root_uri = format!("file://{}", root.display()); + + let result = execute_local_tree_command(&root_uri, "create", "", None, Some("新页面")) + .expect("create page with default title"); + + let relative_path = result["relativePath"].as_str().expect("relativePath"); + let document_id = result["documentId"].as_str().expect("documentId"); + + // 应形如“新页面HHMMSS/新页面HHMMSS.md”。 + assert!( + relative_path.starts_with("新页面"), + "relativePath should start with 新页面, got: {relative_path}" + ); + assert!( + relative_path.ends_with(".md"), + "relativePath should end with .md, got: {relative_path}" + ); + assert!( + relative_path.len() > "新页面.md".len(), + "expected timestamp suffix" + ); + + let (dir, file) = relative_path.split_once('/').expect("nested bundle path"); + assert_eq!(file, format!("{dir}.md")); + assert!( + root.join(dir).is_dir(), + "bundle directory should exist: {}", + dir + ); + + // Markdown 文件应已创建。 + assert!( + root.join(relative_path).is_file(), + "markdown file should exist: {relative_path}" + ); + + // documentId 继续使用路径型身份。 + assert!( + document_id.starts_with("local-md:"), + "documentId should be path-based" + ); + assert_eq!(result["action"], "create"); + assert_eq!(result["sourceKind"], "local_folder"); + + // 页面标题来自文件名,默认正文不再写第二个标题真相。 + let content = std::fs::read_to_string(root.join(relative_path)).expect("read markdown"); + assert!( + content.is_empty(), + "new page body should be empty: {content}" + ); + + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn local_page_tree_projects_nested_bundle_as_page_node() { + let root = temp_root("mnote-local-page-tree-nested-bundle"); + init_workspace(&root); + let root_uri = format!("file://{}", root.display()); + + // 构造 Root/Root.md + Root/Child/Child.md。 + std::fs::create_dir_all(root.join("Root")).expect("create Root dir"); + std::fs::write(root.join("Root").join("Root.md"), "# Body Root\n").expect("write Root.md"); + std::fs::create_dir_all(root.join("Root").join("Child")).expect("create Child dir"); + std::fs::write( + root.join("Root").join("Child").join("Child.md"), + "# Body Child\n", + ) + .expect("write Child.md"); + + // 读取页面树。 + let snapshot = load_local_folder_page_tree_snapshot(&root_uri).expect("load page tree"); + let items = snapshot.projection["items"].as_array().expect("items"); + + // 找到 Root 页面节点和 Child 页面节点。 + let root_node = items + .iter() + .find(|item| { + item["title"].as_str() == Some("Root") + && item["resourceMeta"]["resourceKind"].as_str() == Some("document") + }) + .expect("Root page node"); + + let child_node = items + .iter() + .find(|item| { + item["title"].as_str() == Some("Child") + && item["resourceMeta"]["resourceKind"].as_str() == Some("document") + }) + .expect("Child page node"); + + // Child 的父节点应是 Root 的 documentId。 + let root_doc_id = root_node["resourceMeta"]["documentId"] + .as_str() + .or_else(|| root_node["nodeId"].as_str()) + .expect("root documentId"); + assert_eq!(child_node["parentNodeId"].as_str(), Some(root_doc_id)); + assert_eq!(root_doc_id, "local-md:Root~2FRoot.md"); + + // Root bundle 目录不应额外生成 page-group 行。 + let page_group_for_root = items.iter().find(|item| { + item["rowId"] + .as_str() + .map_or(false, |id| id.contains("page-group:Root")) + }); + assert!( + page_group_for_root.is_none(), + "should not have page-group for Root" + ); + + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn local_file_tree_keeps_nested_bundle_filesystem_details() { + let root = temp_root("mnote-local-file-tree-nested-bundle"); + init_workspace(&root); + let root_uri = format!("file://{}", root.display()); + + // 构造 Root/Root.md + Root/Child/Child.md。 + std::fs::create_dir_all(root.join("Root")).expect("create Root dir"); + std::fs::write(root.join("Root").join("Root.md"), "# Root\n").expect("write Root.md"); + std::fs::create_dir_all(root.join("Root").join("Child")).expect("create Child dir"); + std::fs::write( + root.join("Root").join("Child").join("Child.md"), + "# Child\n", + ) + .expect("write Child.md"); + + // 读取文件树。 + let snapshot = load_local_folder_file_tree_snapshot(&root_uri).expect("load file tree"); + let items = snapshot.projection["items"].as_array().expect("items"); + + // 文件树应保留 Root 文件夹和其中的 Root.md 真实条目。 + let root_md = items.iter().find(|item| { + item["title"].as_str() == Some("Root.md") + && item["resourceMeta"]["resourceKind"].as_str() == Some("markdown") + }); + assert!( + root_md.is_some(), + "file tree should have Root.md as markdown row" + ); + + let root_folder = items.iter().find(|item| { + item["title"].as_str() == Some("Root") + && item["resourceMeta"]["resourceKind"].as_str() == Some("folder") + }); + assert!( + root_folder.is_some(), + "file tree should have Root as folder row" + ); + + // Child.md 应作为 Root/Child 文件夹下的真实文件条目存在。 + let child_md = items.iter().find(|item| { + item["title"].as_str() == Some("Child.md") + && item["resourceMeta"]["resourceKind"].as_str() == Some("markdown") + }); + assert!( + child_md.is_some(), + "file tree should have Child.md under Root" + ); + + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn local_markdown_asset_upload_copies_next_to_markdown_with_relative_path() { + let root = temp_root("mnote-local-markdown-asset-upload"); + init_workspace(&root); + std::fs::create_dir_all(root.join("docs")).expect("create docs"); + std::fs::create_dir_all(root.join("docs").join("README")).expect("create page dir"); + std::fs::write( + root.join("docs").join("README").join("README.md"), + "# Assets\n", + ) + .expect("write md"); + std::fs::create_dir_all(root.join("docs").join("README")).expect("create page dir"); + std::fs::write(root.join("docs").join("README").join("photo.png"), b"old") + .expect("write existing asset"); + let root_uri = format!("file://{}", root.display()); + + let asset = write_local_markdown_asset( + &root_uri, + "local-md:docs~2FREADME~2FREADME.md", + "image", + LocalUploadFile { + name: "photo.png".to_string(), + content_type: "image/png".to_string(), + bytes: b"new-image".to_vec(), + }, + ) + .expect("upload asset"); + + assert_eq!(asset["sourcePath"], "photo-1.png"); + assert_eq!(asset["file_url"], "photo-1.png"); + assert_eq!(asset["asset_type"], "image"); + assert_eq!(asset["sourceKind"], "local_folder"); + assert_eq!( + std::fs::read(root.join("docs").join("README").join("photo-1.png")) + .expect("read copied asset"), + b"new-image" + ); + + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn local_rename_markdown_page_renames_nested_bundle() { + let root = temp_root("mnote-local-rename-nested-bundle"); + init_workspace(&root); + let root_uri = format!("file://{}", root.display()); + + // 构造 Old/Old.md + Old/Child.md。 + std::fs::create_dir_all(root.join("Old")).expect("create Old dir"); + std::fs::write(root.join("Old").join("Old.md"), "# Old\n").expect("write Old.md"); + std::fs::write(root.join("Old").join("Child.md"), "# Child\n").expect("write Child.md"); + + // 将 Old 重命名为 New。 + let old_document_id = "local-md:Old~2FOld.md"; + let result = + execute_local_tree_command(&root_uri, "rename", old_document_id, None, Some("New")) + .expect("rename page"); + + // New/New.md 应存在。 + assert!( + root.join("New").join("New.md").is_file(), + "New/New.md should exist" + ); + // New/Child.md 应存在。 + assert!( + root.join("New").join("Child.md").is_file(), + "New/Child.md should exist" + ); + // Old/ 不应再存在。 + assert!( + !root.join("Old").exists(), + "Old/ directory should not exist" + ); + + assert_eq!(result["action"], "rename"); + assert_eq!(result["sourceKind"], "local_folder"); + assert_eq!(result["documentId"].as_str(), Some("local-md:New~2FNew.md")); + + let _ = std::fs::remove_dir_all(&root); + } } diff --git a/rust/crates/mnote-web/src/routes/local_markdown_parser.rs b/rust/crates/mnote-web/src/routes/local_markdown_parser.rs index 9395d8dd..84fd0a33 100644 --- a/rust/crates/mnote-web/src/routes/local_markdown_parser.rs +++ b/rust/crates/mnote-web/src/routes/local_markdown_parser.rs @@ -5,7 +5,6 @@ use serde_json::{json, Map, Value}; #[derive(Debug, Clone)] pub struct ParsedLocalMarkdownPage { pub title: String, - pub mnote_id: Option, pub body: String, } @@ -37,6 +36,14 @@ enum MarkdownBlock { alignments: Vec, rows: Vec, }, + Image { + alt: String, + source_path: String, + }, + Mindmap { + name: String, + source_path: String, + }, Media { name: String, source_path: String, @@ -71,18 +78,10 @@ struct MarkdownInlineStyles { } pub fn parse_markdown_page(markdown: &str, file_name: &str) -> ParsedLocalMarkdownPage { - let (frontmatter, body) = split_frontmatter(markdown); - let title = frontmatter - .as_deref() - .and_then(|content| read_frontmatter_field(content, "title")) - .or_else(|| extract_first_h1_title(body)) - .unwrap_or_else(|| file_stem_title(file_name)); - let mnote_id = frontmatter - .as_deref() - .and_then(|content| read_frontmatter_field(content, "mnote_id")); + let (_, body) = split_frontmatter(markdown); + let title = file_stem_title(file_name); ParsedLocalMarkdownPage { title, - mnote_id, body: body.to_string(), } } @@ -97,7 +96,12 @@ pub fn parse_markdown_attachment_link(trimmed: &str) -> Option<(String, String)> .unwrap_or(trimmed) .strip_prefix('[')?; let (label, rest) = value.split_once("](")?; - let target = rest.strip_suffix(')')?.trim(); + let raw_target = rest.strip_suffix(')')?.trim(); + let target = raw_target + .strip_prefix('<') + .and_then(|value| value.strip_suffix('>')) + .unwrap_or(raw_target) + .trim(); if target.is_empty() || target.starts_with("http://") || target.starts_with("https://") @@ -179,6 +183,14 @@ fn append_ast_block<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) } fn append_ast_paragraph<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) { + if let Some((alt, source_path)) = paragraph_image(node) { + blocks.push(MarkdownBlock::Image { alt, source_path }); + return; + } + if let Some((name, source_path)) = paragraph_mindmap(node) { + blocks.push(MarkdownBlock::Mindmap { name, source_path }); + return; + } if let Some((name, source_path)) = paragraph_attachment_media(node) { blocks.push(MarkdownBlock::Media { name, source_path }); return; @@ -350,6 +362,28 @@ fn paragraph_attachment_media<'a>(node: &'a AstNode<'a>) -> Option<(String, Stri link_attachment_media(first) } +fn paragraph_mindmap<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> { + let mut children = node.children(); + let first = children.next()?; + if children.next().is_some() { + return None; + } + link_mindmap(first) +} + +fn paragraph_image<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> { + let mut children = node.children(); + let first = children.next()?; + if children.next().is_some() { + return None; + } + let NodeValue::Image(link) = &first.data.borrow().value else { + return None; + }; + let alt = collect_plain_text(first).trim().to_string(); + Some((alt, link.url.clone())) +} + fn paragraph_leading_attachment_media<'a>( node: &'a AstNode<'a>, ) -> Option<(String, String, Vec)> { @@ -377,6 +411,33 @@ fn link_attachment_media<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> parse_markdown_attachment_link(&format!("[{}]({})", collect_plain_text(node), link.url)) } +fn link_mindmap<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> { + let NodeValue::Link(link) = &node.data.borrow().value else { + return None; + }; + let target = link.url.trim(); + let file_name = std::path::Path::new(target) + .file_name() + .and_then(|value| value.to_str()) + .unwrap_or(target) + .trim(); + let lower = file_name.to_ascii_lowercase(); + let is_mindmap = lower.ends_with(".mindmap.json") + || (file_name.starts_with("思维导图") && lower.ends_with(".json")); + if !is_mindmap { + return None; + } + let name = collect_plain_text(node).trim().to_string(); + Some(( + if name.is_empty() { + "思维导图".to_string() + } else { + name + }, + target.to_string(), + )) +} + fn markdown_ast_document_to_blocks(document: &MarkdownAstDocument) -> Value { Value::Array( document @@ -438,6 +499,29 @@ fn markdown_block_to_json(block: &MarkdownBlock, block_number: usize) -> Value { MarkdownBlock::Table { alignments, rows } => { table_block_to_json(alignments, rows, block_number) } + MarkdownBlock::Image { alt, source_path } => json!({ + "id": format!("local-block-{block_number}"), + "type": "image", + "props": { + "src": source_path, + "alt": alt, + "title": alt, + }, + "content": [], + "children": [], + }), + MarkdownBlock::Mindmap { name, source_path } => json!({ + "id": format!("local-block-{block_number}"), + "type": "mindmap", + "props": { + "name": name, + "sourcePath": source_path, + "mindmapId": source_path, + "rootNodeId": "root", + }, + "content": [], + "children": [], + }), MarkdownBlock::Media { name, source_path } => json!({ "id": format!("local-block-{block_number}"), "type": "media", @@ -634,32 +718,6 @@ pub(crate) fn split_frontmatter(markdown: &str) -> (Option, &str) { (None, normalized) } -fn read_frontmatter_field(frontmatter: &str, key: &str) -> Option { - frontmatter.lines().find_map(|line| { - let (candidate_key, value) = line.split_once(':')?; - if candidate_key.trim() != key { - return None; - } - let trimmed = value.trim().trim_matches('"').trim_matches('\'').trim(); - if trimmed.is_empty() { - None - } else { - Some(trimmed.to_string()) - } - }) -} - -fn extract_first_h1_title(body: &str) -> Option { - body.lines().find_map(|line| { - let trimmed = line.trim(); - trimmed - .strip_prefix("# ") - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(ToOwned::to_owned) - }) -} - pub(crate) fn file_stem_title(file_name: &str) -> String { std::path::Path::new(file_name) .file_stem() @@ -669,3 +727,21 @@ pub(crate) fn file_stem_title(file_name: &str) -> String { .unwrap_or(file_name) .to_string() } + +#[cfg(test)] +mod tests { + use super::markdown_to_blocks; + + #[test] + fn markdown_image_parses_as_image_block() { + let blocks = markdown_to_blocks("![示例图片](assets/photo.jpg)\n"); + let first = blocks + .as_array() + .and_then(|items| items.first()) + .expect("first block"); + + assert_eq!(first["type"].as_str(), Some("image")); + assert_eq!(first["props"]["src"].as_str(), Some("assets/photo.jpg")); + assert_eq!(first["props"]["alt"].as_str(), Some("示例图片")); + } +} diff --git a/rust/crates/mnote-web/src/routes/local_search_index.rs b/rust/crates/mnote-web/src/routes/local_search_index.rs index bf5873c5..9f4c1611 100644 --- a/rust/crates/mnote-web/src/routes/local_search_index.rs +++ b/rust/crates/mnote-web/src/routes/local_search_index.rs @@ -1,4 +1,5 @@ use crate::error::WebError; +use crate::routes::local_folder_source::encode_local_id_segment; use crate::routes::local_markdown_parser::{ parse_markdown_attachment_link, parse_markdown_page, split_frontmatter, }; @@ -411,11 +412,7 @@ fn index_markdown_file(root_path: &Path, path: &Path) -> Result, pub root_node_id: Option, pub workspace_id: Option, + pub source_kind: Option, + pub root_uri: Option, } #[derive(Debug, Deserialize)] @@ -38,6 +44,8 @@ pub struct MindmapCommandRequest { pub workspace_id: Option, pub data: Option, pub create_only: Option, + pub source_kind: Option, + pub root_uri: Option, } fn default_mindmap_data() -> Value { @@ -81,6 +89,25 @@ fn read_workspace_id_from_meta(meta: &Value) -> Option { .map(ToOwned::to_owned) } +fn is_local_folder_source(source_kind: Option<&str>) -> bool { + source_kind + .map(str::trim) + .is_some_and(|value| value == "local_folder") +} + +fn local_root_uri<'a>( + query_root_uri: Option<&'a str>, + body_root_uri: Option<&'a str>, +) -> Result<&'a str, WebError> { + query_root_uri + .or(body_root_uri) + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + WebError::bad_request_code("local_folder_root_required", "缺少本地思维导图 rootUri") + }) +} + async fn resolve_mindmap_workspace_id( state: &AppState, context: &RequestContext, @@ -122,6 +149,29 @@ pub async fn get_mindmap( ) .with_context(&context)); } + if is_local_folder_source(params.source_kind.as_deref()) { + let root_uri = local_root_uri(params.root_uri.as_deref(), None)?; + ensure_local_workspace_read_access(&context, root_uri) + .map_err(|error| error.with_context(&context))?; + let data = read_local_mindmap_data(root_uri, document_id, mindmap_id) + .map_err(|error| error.with_context(&context))?; + let query_name = resolve_query_name(¶ms); + let result = execute_runtime_query_against_data( + &context, + params.workspace_id.as_deref(), + RuntimeQueryEnvelopeWire { + name: query_name.into(), + payload: json!({ + "documentId": document_id, + "mindmapId": mindmap_id, + "rootNodeId": params.root_node_id, + "workspaceId": params.workspace_id, + }), + }, + data, + )?; + return Ok((StatusCode::OK, response_headers(), Json(result))); + } let effective_workspace_id = resolve_effective_workspace_id(&context, params.workspace_id.as_deref(), false)?; let query_name = resolve_query_name(¶ms); @@ -148,6 +198,7 @@ pub async fn apply_mindmap_command( State(state): State, Extension(context): Extension, Path((document_id, mindmap_id)): Path<(String, String)>, + Query(params): Query, Json(body): Json, ) -> Result<(StatusCode, HeaderMap, Json), WebError> { let document_id = document_id.trim(); @@ -170,6 +221,101 @@ pub async fn apply_mindmap_command( ) .with_context(&context)); } + + let local_source_kind = body + .source_kind + .as_deref() + .or(params.source_kind.as_deref()); + if is_local_folder_source(local_source_kind) { + let root_uri = local_root_uri(params.root_uri.as_deref(), body.root_uri.as_deref())?; + ensure_local_workspace_access(&context, root_uri) + .map_err(|error| error.with_context(&context))?; + if command_name == Some("mindmap.command.apply") { + let current = read_local_mindmap_data(root_uri, document_id, mindmap_id) + .map_err(|error| error.with_context(&context))?; + let applied = apply_mindmap_kernel_commands_to_value(¤t, &body.commands) + .map_err(|error| WebError::bad_request(error.message).with_context(&context))?; + if !applied.errors.is_empty() { + return Err(WebError::bad_request_code( + "mindmap_command_failed", + applied.errors.join("; "), + ) + .with_context(&context) + .with_header("x-error-phase", "local_mindmap_command_apply")); + } + let write_result = write_local_mindmap_data( + root_uri, + document_id, + mindmap_id, + applied.data.clone(), + false, + ) + .map_err(|error| error.with_context(&context))?; + let projection = execute_runtime_query_against_data( + &context, + body.workspace_id.as_deref(), + RuntimeQueryEnvelopeWire { + name: "mindmap.simple_mind_map_scene.get".into(), + payload: json!({ + "documentId": document_id, + "mindmapId": mindmap_id, + "workspaceId": body.workspace_id, + }), + }, + applied.data, + )?; + return Ok(( + StatusCode::OK, + response_headers(), + Json(json!({ + "ok": true, + "sourceKind": "local_folder", + "commandName": "mindmap.command.apply", + "applied": applied.applied, + "errors": applied.errors, + "projectionRevision": body.projection_revision, + "writeResult": write_result, + "kernelRevision": projection.get("kernelRevision").cloned().unwrap_or(json!(1)), + "root": projection.get("root").cloned().unwrap_or(Value::Null), + })), + )); + } + + let data = body.data.unwrap_or_else(default_mindmap_data); + let write_result = write_local_mindmap_data( + root_uri, + document_id, + mindmap_id, + data.clone(), + body.create_only.unwrap_or(false), + ) + .map_err(|error| error.with_context(&context))?; + let projection = execute_runtime_query_against_data( + &context, + body.workspace_id.as_deref(), + RuntimeQueryEnvelopeWire { + name: "mindmap.simple_mind_map_scene.get".into(), + payload: json!({ + "documentId": document_id, + "mindmapId": mindmap_id, + "workspaceId": body.workspace_id, + }), + }, + data, + )?; + return Ok(( + StatusCode::OK, + response_headers(), + Json(json!({ + "ok": true, + "sourceKind": "local_folder", + "commandName": "mindmaps.put", + "writeResult": write_result, + "kernelRevision": projection.get("kernelRevision").cloned().unwrap_or(json!(1)), + "root": projection.get("root").cloned().unwrap_or(Value::Null), + })), + )); + } let effective_workspace_id = resolve_mindmap_workspace_id(&state, &context, body.workspace_id.as_deref(), document_id) .await?; @@ -323,6 +469,7 @@ mod tests { use axum::body::{to_bytes, Body}; use axum::http::{Request, StatusCode}; use serde_json::{json, Value}; + use std::path::PathBuf; use tower::util::ServiceExt; fn app() -> axum::Router { @@ -354,6 +501,26 @@ mod tests { })) } + fn temp_root(name: &str) -> PathBuf { + let root = std::env::temp_dir().join(format!("{}-{}", name, std::process::id())); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir_all(&root).expect("create temp root"); + root + } + + fn encode_query_value(value: &str) -> String { + value.replace(':', "%3A").replace('/', "%2F") + } + + fn init_local_workspace(root: &PathBuf) -> String { + let root_uri = format!("file://{}", root.display()); + crate::routes::local_folder_source::initialize_local_workspace_for_actor( + "dev-user", &root_uri, + ) + .expect("init local workspace"); + root_uri + } + #[tokio::test] async fn mindmap_put_derives_workspace_and_returns_tree_artifacts() { let response = app() @@ -450,4 +617,142 @@ mod tests { }) ); } + + #[tokio::test] + async fn local_folder_mindmap_put_writes_file_and_get_reads_back() { + let root = temp_root("mnote-local-mindmap-api-put-get"); + let root_uri = init_local_workspace(&root); + std::fs::create_dir_all(root.join("Page")).expect("create page dir"); + std::fs::write(root.join("Page").join("Page.md"), "").expect("write page"); + let encoded_root = encode_query_value(&root_uri); + let encoded_mindmap_id = encode_query_value("思维导图123456.json"); + + let put_response = app() + .oneshot( + Request::builder() + .method("POST") + .uri(format!( + "/api/mindmap/local-md:Page~2FPage.md/{encoded_mindmap_id}?sourceKind=local_folder&rootUri={encoded_root}" + )) + .header("content-type", "application/json") + .header("x-mnote-actor-id", "dev-user") + .header("x-mnote-actor-type", "user") + .body(Body::from( + json!({ + "data": { + "data": {"text": "本地主题", "uid": "root"}, + "children": [] + }, + "createOnly": false + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(put_response.status(), StatusCode::OK); + + let put_body = to_bytes(put_response.into_body(), usize::MAX) + .await + .expect("body"); + let put_payload: Value = serde_json::from_slice(&put_body).expect("json"); + assert_eq!(put_payload["sourceKind"], "local_folder"); + assert_eq!(put_payload["commandName"], "mindmaps.put"); + assert_eq!( + put_payload["writeResult"]["relativePath"], + "Page/思维导图123456.json" + ); + assert!( + root.join("Page").join("思维导图123456.json").is_file(), + "mindmap json should be persisted next to the page markdown" + ); + + let get_response = app() + .oneshot( + Request::builder() + .uri(format!( + "/api/mindmap/local-md:Page~2FPage.md/{encoded_mindmap_id}?sourceKind=local_folder&rootUri={encoded_root}&view=simple_mind_map_scene" + )) + .header("x-mnote-actor-id", "dev-user") + .header("x-mnote-actor-type", "user") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(get_response.status(), StatusCode::OK); + let get_body = to_bytes(get_response.into_body(), usize::MAX) + .await + .expect("body"); + let get_payload: Value = serde_json::from_slice(&get_body).expect("json"); + assert_eq!(get_payload["root"]["data"]["text"], "本地主题"); + + let _ = std::fs::remove_dir_all(&root); + } + + #[tokio::test] + async fn local_folder_mindmap_command_apply_updates_persisted_file() { + let root = temp_root("mnote-local-mindmap-api-command-apply"); + let root_uri = init_local_workspace(&root); + std::fs::create_dir_all(root.join("Page")).expect("create page dir"); + std::fs::write(root.join("Page").join("Page.md"), "").expect("write page"); + std::fs::write( + root.join("Page").join("思维导图123456.json"), + json!({ + "data": {"text": "旧主题", "uid": "root"}, + "children": [] + }) + .to_string(), + ) + .expect("write mindmap"); + let encoded_root = encode_query_value(&root_uri); + let encoded_mindmap_id = encode_query_value("思维导图123456.json"); + + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri(format!( + "/api/mindmap/local-md:Page~2FPage.md/{encoded_mindmap_id}?sourceKind=local_folder&rootUri={encoded_root}" + )) + .header("content-type", "application/json") + .header("x-mnote-actor-id", "dev-user") + .header("x-mnote-actor-type", "user") + .body(Body::from( + json!({ + "commandName": "mindmap.command.apply", + "commands": [ + { + "type": "updateText", + "mindmapId": "思维导图123456.json", + "nodeId": "root", + "text": "新主题" + } + ], + "projectionRevision": 1 + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["sourceKind"], "local_folder"); + assert_eq!(payload["commandName"], "mindmap.command.apply"); + assert_eq!(payload["root"]["data"]["text"], "新主题"); + + let saved = std::fs::read_to_string(root.join("Page").join("思维导图123456.json")) + .expect("read saved mindmap"); + let saved_json: Value = serde_json::from_str(&saved).expect("saved json"); + assert_eq!(saved_json["data"]["text"], "新主题"); + + let _ = std::fs::remove_dir_all(&root); + } } diff --git a/rust/crates/mnote-web/src/routes/mindmap_shell.rs b/rust/crates/mnote-web/src/routes/mindmap_shell.rs index 77865f26..967c3d7a 100644 --- a/rust/crates/mnote-web/src/routes/mindmap_shell.rs +++ b/rust/crates/mnote-web/src/routes/mindmap_shell.rs @@ -3,27 +3,46 @@ use crate::context::RequestContext; use crate::error::WebError; use crate::routes::web_shell::{ build_page_aggregate_snapshot, load_file_tree_html, load_sidebar_tree_html, - load_workspace_shell_projection, + load_workspace_shell_projection, render_local_file_tree_html, render_local_sidebar_tree_html, }; use crate::ssr::pages::mindmap::MindmapPage; use crate::workspace_shell::render_workspace_shell_sidebar_html; -use axum::extract::{Extension, Path, State}; +use axum::extract::{Extension, Path, Query, State}; use axum::http::{HeaderMap, HeaderName, HeaderValue}; use axum::response::{Html, IntoResponse, Response}; +use serde::Deserialize; use serde_json::json; const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner"; const HEADER_MNOTE_WEB_SHELL: &str = "x-mnote-web-shell"; +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MindmapShellQuery { + pub source_kind: Option, + pub root_uri: Option, + pub workspace_id: Option, +} + pub async fn mindmap_object_shell( State(state): State, Extension(context): Extension, Path((doc_id, mindmap_id)): Path<(String, String)>, + Query(query): Query, ) -> Result { let default_workspace_name = format!("{} 的空间", state.config().dev_user_name); - let aggregate = build_page_aggregate_snapshot(&state, &context, &doc_id, None, None, None) - .await - .ok(); + let source_kind = query.source_kind.as_deref(); + let root_uri = query.root_uri.as_deref(); + let aggregate = build_page_aggregate_snapshot( + &state, + &context, + &doc_id, + query.workspace_id.as_deref(), + source_kind, + root_uri, + ) + .await + .ok(); let workspace_id = aggregate .as_ref() .map(|value| value.identity.workspace_id.clone()) @@ -33,44 +52,69 @@ pub async fn mindmap_object_shell( .map(|value| value.head.title.trim().to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| "思维导图".to_string()); - let (workspace_name, sidebar_tree_html, workspace_sidebar_html) = - if let Some(workspace_id) = workspace_id.as_deref() { - let workspace_projection = load_workspace_shell_projection( - state.config(), - &context, - workspace_id, - Some(&doc_id), - &default_workspace_name, - ) - .await; - let sidebar_tree_html = - load_sidebar_tree_html(state.config(), &context, workspace_id, Some(&doc_id)) - .await - .unwrap_or_default(); - let active_filetree_row_id = format!("asset:{mindmap_id}"); - let file_tree_html = load_file_tree_html( - state.config(), - &context, - workspace_id, - Some(&doc_id), - Some(active_filetree_row_id.as_str()), - ) - .await - .unwrap_or_default(); - let workspace_name = workspace_projection.workspace_name.clone(); - let workspace_sidebar_html = render_workspace_shell_sidebar_html( - &workspace_projection, - Some(sidebar_tree_html.as_str()), - Some(file_tree_html.as_str()), - ); - ( - Some(workspace_name), - Some(sidebar_tree_html), - Some(workspace_sidebar_html), - ) - } else { - (None, None, None) - }; + let is_local_folder = source_kind.map(str::trim) == Some("local_folder"); + let (workspace_name, sidebar_tree_html, workspace_sidebar_html) = if is_local_folder { + let root_uri = root_uri.unwrap_or_default(); + let sidebar_tree_html = + render_local_sidebar_tree_html(root_uri, Some(&doc_id)).unwrap_or_default(); + let file_tree_html = + render_local_file_tree_html(root_uri, Some(&doc_id)).unwrap_or_default(); + let workspace_projection = load_workspace_shell_projection( + state.config(), + &context, + workspace_id.as_deref().unwrap_or("local-folder"), + Some(&doc_id), + &default_workspace_name, + ) + .await; + let workspace_name = workspace_projection.workspace_name.clone(); + let workspace_sidebar_html = render_workspace_shell_sidebar_html( + &workspace_projection, + Some(sidebar_tree_html.as_str()), + Some(file_tree_html.as_str()), + ); + ( + Some(workspace_name), + Some(sidebar_tree_html), + Some(workspace_sidebar_html), + ) + } else if let Some(workspace_id) = workspace_id.as_deref() { + let workspace_projection = load_workspace_shell_projection( + state.config(), + &context, + workspace_id, + Some(&doc_id), + &default_workspace_name, + ) + .await; + let sidebar_tree_html = + load_sidebar_tree_html(state.config(), &context, workspace_id, Some(&doc_id)) + .await + .unwrap_or_default(); + let active_filetree_row_id = format!("asset:{mindmap_id}"); + let file_tree_html = load_file_tree_html( + state.config(), + &context, + workspace_id, + Some(&doc_id), + Some(active_filetree_row_id.as_str()), + ) + .await + .unwrap_or_default(); + let workspace_name = workspace_projection.workspace_name.clone(); + let workspace_sidebar_html = render_workspace_shell_sidebar_html( + &workspace_projection, + Some(sidebar_tree_html.as_str()), + Some(file_tree_html.as_str()), + ); + ( + Some(workspace_name), + Some(sidebar_tree_html), + Some(workspace_sidebar_html), + ) + } else { + (None, None, None) + }; let editor_bootstrap = json!({ "documentId": format!("__mindmap_object__:{doc_id}:{mindmap_id}"), "workspaceId": workspace_id @@ -99,6 +143,8 @@ pub async fn mindmap_object_shell( "documentId": doc_id, "mindmapId": mindmap_id }, + "sourceKind": source_kind.unwrap_or("convex_workspace"), + "rootUri": root_uri.unwrap_or(""), "revision": serde_json::Value::Null, "conflictDetectionKey": serde_json::Value::Null, "pageOptions": { @@ -114,6 +160,8 @@ pub async fn mindmap_object_shell( "shell": "mindmap", "documentId": doc_id, "mindmapId": mindmap_id, + "sourceKind": source_kind.unwrap_or("convex_workspace"), + "rootUri": root_uri.unwrap_or(""), "projection": { "schema": "mnote.mindmap.simple_mind_map_scene.v1", "runtime": "simple-mind-map", @@ -151,7 +199,7 @@ pub async fn mindmap_object_shell( {} - + {} @@ -162,6 +210,8 @@ pub async fn mindmap_object_shell( crate::ssr::MNOTE_CSS, escape_html(&doc_id), escape_html(&mindmap_id), + escape_html(source_kind.unwrap_or("convex_workspace")), + escape_html(root_uri.unwrap_or("")), body_content, escape_script_json(&editor_bootstrap_json), escape_script_json(&contract_json), diff --git a/rust/crates/mnote-web/src/routes/tree.rs b/rust/crates/mnote-web/src/routes/tree.rs index 4b6b97be..09d38b56 100644 --- a/rust/crates/mnote-web/src/routes/tree.rs +++ b/rust/crates/mnote-web/src/routes/tree.rs @@ -443,48 +443,8 @@ pub(crate) fn collect_page_tree_render_rows(projection: &Value) -> Vec) -> String { - let source = asset_id - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or(raw.trim()); - let digits = source - .chars() - .rev() - .take_while(|ch| ch.is_ascii_digit()) - .collect::() - .chars() - .rev() - .collect::(); - let suffix = if digits.len() >= 4 { - digits[digits.len().saturating_sub(4)..].to_string() - } else { - source - .trim_start_matches("mindmap") - .trim_start_matches(|ch| ch == '-' || ch == '_') - .chars() - .take(6) - .collect::() - }; - if suffix.trim().is_empty() { - "思维导图.json".to_string() - } else { - format!("思维导图-{suffix}.json") - } -} - -fn normalize_filetree_mindmap_title( - raw_title: &str, - asset_id: Option<&str>, - icon_kind: &str, - resource_kind: Option<&str>, -) -> String { +fn normalize_filetree_mindmap_title(raw_title: &str) -> String { let title = raw_title.trim(); - let is_mindmap = icon_kind == "mindmap" || resource_kind == Some("mindmap"); - let generated = title.starts_with("mindmap-") || title.starts_with("mindmap_"); - if is_mindmap && (generated || title.chars().count() > 24) { - return short_mindmap_file_name(title, asset_id); - } if title.is_empty() { "无标题".to_string() } else { @@ -592,14 +552,7 @@ pub(crate) fn collect_filetree_render_rows( .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned), - title: normalize_filetree_mindmap_title( - raw_title, - asset_id.as_deref(), - &icon_kind, - resource_meta - .and_then(|meta| meta.get("resourceKind")) - .and_then(Value::as_str), - ), + title: normalize_filetree_mindmap_title(raw_title), depth: item.get("depth").and_then(Value::as_u64).unwrap_or(0) as u32, expandable: item .get("expandable") @@ -7336,8 +7289,10 @@ mod tests { .expect("body"); let html = String::from_utf8(body.to_vec()).expect("utf8"); assert!(html.contains("local_folder")); - assert!(html.contains("Frontmatter Title")); - assert!(html.contains("Child H1")); + assert!(html.contains("README")); + assert!(html.contains("child")); + assert!(!html.contains("Frontmatter Title")); + assert!(!html.contains("Child H1")); assert!(html.contains(">docs<") || html.contains("docs")); assert!(!html.contains("image.png")); } @@ -7380,8 +7335,23 @@ mod tests { .as_str() .expect("document id") .to_string(); - assert!(root.join("新页面.md").exists()); - assert!(root.join(".mnote").join("page-ids.json").exists()); + let created_relative_path = payload["result"]["execution"]["relativePath"] + .as_str() + .expect("created relative path"); + let (created_dir, created_file) = created_relative_path + .split_once('/') + .expect("created nested bundle path"); + assert!(created_dir.starts_with("新页面")); + assert_eq!(created_file, format!("{created_dir}.md")); + assert!(root.join(created_dir).join(created_file).exists()); + assert_eq!( + document_id, + format!( + "local-md:{}", + crate::routes::local_folder_source::encode_local_id_segment(created_relative_path) + ) + ); + assert!(!root.join(".mnote").join("page-ids.json").exists()); let rename_response = app() .oneshot( @@ -7406,8 +7376,18 @@ mod tests { "{}", String::from_utf8_lossy(&rename_body) ); - assert!(!root.join("新页面.md").exists()); - assert!(root.join("重命名页面.md").exists()); + assert!(!root.join(created_dir).exists()); + assert!(root.join("重命名页面").join("重命名页面.md").exists()); + let rename_payload: Value = serde_json::from_slice(&rename_body).expect("rename json"); + let renamed_document_id = rename_payload["result"]["documentId"] + .as_str() + .expect("renamed document id") + .to_string(); + assert_eq!( + renamed_document_id, + "local-md:~E9~87~8D~E5~91~BD~E5~90~8D~E9~A1~B5~E9~9D~A2~2F~E9~87~8D~E5~91~BD~E5~90~8D~E9~A1~B5~E9~9D~A2.md" + ); + assert!(!root.join(".mnote").join("page-ids.json").exists()); std::fs::create_dir_all(root.join("docs")).expect("create docs dir"); let move_response = app() @@ -7417,15 +7397,38 @@ mod tests { .uri("/api/tree/commands") .header("content-type", "application/json") .body(Body::from(format!( - r#"{{"action":"move","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{document_id}","parentId":"local-dir:docs","sortOrder":0}}"# + r#"{{"action":"move","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{renamed_document_id}","parentId":"local-dir:docs","sortOrder":0}}"# ))) .expect("request"), ) .await .expect("response"); - assert_eq!(move_response.status(), StatusCode::OK); - assert!(!root.join("重命名页面.md").exists()); - assert!(root.join("docs").join("重命名页面.md").exists()); + let move_status = move_response.status(); + let move_body = axum::body::to_bytes(move_response.into_body(), usize::MAX) + .await + .expect("move body"); + assert_eq!( + move_status, + StatusCode::OK, + "{}", + String::from_utf8_lossy(&move_body) + ); + assert!(!root.join("重命名页面").exists()); + assert!(root + .join("docs") + .join("重命名页面") + .join("重命名页面.md") + .exists()); + let move_payload: Value = serde_json::from_slice(&move_body).expect("move json"); + let moved_document_id = move_payload["result"]["documentId"] + .as_str() + .expect("moved document id") + .to_string(); + assert_eq!( + moved_document_id, + "local-md:docs~2F~E9~87~8D~E5~91~BD~E5~90~8D~E9~A1~B5~E9~9D~A2~2F~E9~87~8D~E5~91~BD~E5~90~8D~E9~A1~B5~E9~9D~A2.md" + ); + assert!(!root.join(".mnote").join("page-ids.json").exists()); let copy_response = app() .oneshot( @@ -7434,7 +7437,7 @@ mod tests { .uri("/api/tree/commands") .header("content-type", "application/json") .body(Body::from(format!( - r#"{{"action":"copy","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{document_id}","parentId":"local-dir:docs"}}"# + r#"{{"action":"copy","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{moved_document_id}","parentId":"local-dir:docs"}}"# ))) .expect("request"), ) @@ -7455,7 +7458,11 @@ mod tests { .as_str() .expect("copied document id") .to_string(); - assert!(root.join("docs").join("重命名页面 2.md").exists()); + assert!(root + .join("docs") + .join("重命名页面 2") + .join("重命名页面 2.md") + .exists()); let folder_response = app() .oneshot( @@ -7480,20 +7487,22 @@ mod tests { .uri("/api/tree/commands") .header("content-type", "application/json") .body(Body::from(format!( - r#"{{"action":"delete","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{document_id}"}}"# + r#"{{"action":"delete","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{moved_document_id}"}}"# ))) .expect("request"), ) .await .expect("response"); assert_eq!(delete_response.status(), StatusCode::OK); - assert!(!root.join("docs").join("重命名页面.md").exists()); + assert!(!root.join("docs").join("重命名页面").exists()); assert!(root .join(".mnote") .join("trash") + .join("重命名页面") .join("重命名页面.md") .exists()); assert!(root.join(".mnote").join("trash-index.json").exists()); + assert!(!root.join(".mnote").join("page-ids.json").exists()); let restore_response = app() .oneshot( @@ -7502,14 +7511,33 @@ mod tests { .uri("/api/tree/commands") .header("content-type", "application/json") .body(Body::from(format!( - r#"{{"action":"restore","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{document_id}"}}"# + r#"{{"action":"restore","sourceKind":"local_folder","rootUri":"{root_uri}","documentId":"{moved_document_id}"}}"# ))) .expect("request"), ) .await .expect("response"); - assert_eq!(restore_response.status(), StatusCode::OK); - assert!(root.join("docs").join("重命名页面.md").exists()); + let restore_status = restore_response.status(); + let restore_body = axum::body::to_bytes(restore_response.into_body(), usize::MAX) + .await + .expect("restore body"); + assert_eq!( + restore_status, + StatusCode::OK, + "{}", + String::from_utf8_lossy(&restore_body) + ); + assert!(root + .join("docs") + .join("重命名页面") + .join("重命名页面.md") + .exists()); + let restore_payload: Value = serde_json::from_slice(&restore_body).expect("restore json"); + assert_eq!( + restore_payload["result"]["documentId"].as_str(), + Some(moved_document_id.as_str()) + ); + assert!(!root.join(".mnote").join("page-ids.json").exists()); let purge_response = app() .oneshot( @@ -7525,7 +7553,7 @@ mod tests { .await .expect("response"); assert_eq!(purge_response.status(), StatusCode::OK); - assert!(!root.join("docs").join("重命名页面 2.md").exists()); + assert!(!root.join("docs").join("重命名页面 2").exists()); let _ = std::fs::remove_dir_all(&root); } diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index de98f24a..1bef3ea0 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -213,7 +213,7 @@ pub async fn document_page_shell( {} - + {} @@ -228,6 +228,8 @@ pub async fn document_page_shell( escape_html(title), crate::ssr::MNOTE_CSS, escape_html(&document_id), + escape_html(primary_source_kind.unwrap_or("convex_workspace")), + escape_html(primary_root_uri.unwrap_or("")), secondary_requested, secondary_invalid, body_content, @@ -557,11 +559,24 @@ pub(crate) fn render_document_title_controller_script() -> &'static str { if (!response.ok || !payload || payload.ok !== true) { throw new Error(payload?.error?.message || payload?.message || `title_failed_${response.status}`); } - writeLastSavedTitle(title); - updateVisibleTitle(input, title, currentTarget.documentId); + const result = payload?.result || {}; + const nextDocumentId = String(result.documentId || result.id || currentTarget.documentId || '').trim(); + const previousDocumentId = currentTarget.documentId; + const nextTitle = String(result.title || title || '无标题').trim() || '无标题'; + if (nextDocumentId) { + input.setAttribute('data-document-id', nextDocumentId); + } + writeLastSavedTitle(nextTitle); + updateVisibleTitle(input, nextTitle, nextDocumentId || currentTarget.documentId); setStatus(input, 'saved'); window.dispatchEvent(new CustomEvent('tree:title-updated', { - detail: { documentId: currentTarget.documentId, workspaceId: currentTarget.workspaceId || null, title, payload }, + detail: { + documentId: nextDocumentId || currentTarget.documentId, + previousDocumentId, + workspaceId: currentTarget.workspaceId || null, + title: nextTitle, + payload, + }, })); } catch (error) { setStatus(input, 'error', error instanceof Error ? error.message : String(error)); @@ -896,6 +911,8 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { const mindmapId = firstNonEmptyText( block?.props?.mindmapId, block?.props?.mindmap_id, + block?.props?.sourcePath, + block?.props?.source_path, block?.mindmapId, block?.mindmap_id, data?.mindmapId, @@ -1023,10 +1040,84 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { return textToTiptapDocument(fallbackText); }; - const pageBodyTiptapDocument = (body, fallbackText = '') => { + const decodeLocalIdSegment = (segment) => { + const encoded = String(segment || '').replace(/~([0-9a-fA-F]{2})/g, '%$1'); + try { + return decodeURIComponent(encoded); + } catch (_) { + return String(segment || '').replace(/~2F/g, '/').replace(/~20/g, ' '); + } + }; + + const localMarkdownRelativePathFromDocumentId = (documentId) => { + const raw = String(documentId || '').trim(); + const segment = raw.startsWith('local-md:') ? raw.slice('local-md:'.length) : raw; + return decodeLocalIdSegment(segment).replace(/^\/+/, ''); + }; + + const localMarkdownDirectoryFromDocumentId = (documentId) => { + const relativePath = localMarkdownRelativePathFromDocumentId(documentId); + const slash = relativePath.lastIndexOf('/'); + return slash >= 0 ? relativePath.slice(0, slash) : ''; + }; + + const isExternalOrSpecialUrl = (value) => { + const text = String(value || '').trim(); + return !text + || text.startsWith('#') + || text.startsWith('data:') + || text.startsWith('blob:') + || text.startsWith('mailto:') + || text.startsWith('http://') + || text.startsWith('https://') + || text.startsWith('/api/'); + }; + + const normalizeLocalAssetRelativePath = (value, context) => { + const text = String(value || '').trim(); + if (!text || isExternalOrSpecialUrl(text)) return text; + if (text.startsWith('/')) return text.replace(/^\/+/, ''); + const baseDir = localMarkdownDirectoryFromDocumentId(context?.documentId); + return (baseDir ? `${baseDir}/${text}` : text) + .split('/') + .filter((part) => part && part !== '.') + .join('/'); + }; + + const localFileOpenUrlForTiptap = (value, context) => { + if (!context || context.sourceKind !== 'local_folder' || !context.rootUri) return value; + const relativePath = normalizeLocalAssetRelativePath(value, context); + if (!relativePath || isExternalOrSpecialUrl(relativePath)) return value; + const url = new URL('/api/local-folder/files/open', window.location.origin); + url.searchParams.set('rootUri', context.rootUri); + url.searchParams.set('path', relativePath); + return url.toString(); + }; + + const localizeTiptapAssetUrls = (node, context) => { + if (!node || typeof node !== 'object') return node; + if (node.type === 'image' && node.attrs && typeof node.attrs.src === 'string') { + node.attrs = { ...node.attrs, src: localFileOpenUrlForTiptap(node.attrs.src, context) }; + } + if (Array.isArray(node.marks)) { + node.marks = node.marks.map((mark) => { + if (!mark || mark.type !== 'link' || !mark.attrs || typeof mark.attrs.href !== 'string') return mark; + return { ...mark, attrs: { ...mark.attrs, href: localFileOpenUrlForTiptap(mark.attrs.href, context) } }; + }); + } + if (Array.isArray(node.content)) { + node.content = node.content.map((child) => localizeTiptapAssetUrls(child, context)); + } + return node; + }; + + const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => { + if (String(body?.projectionSource || body?.projection_source || '') === 'local_markdown.content' && body?.content) { + return localizeTiptapAssetUrls(toTiptapDocument(body.content, fallbackText), context); + } const blockDocument = body?.blockDocument || body?.block_document || body?.editorDocument || body?.editor_document || null; - if (blockDocument) return toTiptapDocument(blockDocument, fallbackText); - return toTiptapDocument(body?.content, fallbackText); + if (blockDocument) return localizeTiptapAssetUrls(toTiptapDocument(blockDocument, fallbackText), context); + return localizeTiptapAssetUrls(toTiptapDocument(body?.content, fallbackText), context); }; const inlineTextNodes = (node) => { @@ -1089,6 +1180,8 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { const mindmapId = firstNonEmptyText( attrs?.mindmapId, attrs?.mindmap_id, + attrs?.sourcePath, + attrs?.source_path, data?.mindmapId, data?.mindmap_id, data?.id, @@ -1106,10 +1199,20 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { const tiptapNodeToEditorBlock = (node, index) => { const blockId = blockIdOf(node, index); if (node?.type === 'paragraph' && node?.attrs?.mnoteBlockType === 'mindmap') { + const mindmapId = firstNonEmptyText( + node?.attrs?.mindmapId, + node?.attrs?.mindmap_id, + node?.attrs?.sourcePath, + node?.attrs?.source_path, + blockId + ); return { blockId, blockType: 'mindmap', - props: mindmapPropsFromAttrs(node?.attrs, blockId), + props: { + ...mindmapPropsFromAttrs(node?.attrs, blockId), + sourcePath: mindmapId, + }, contentNodes: [], childBlockIds: [], }; @@ -1577,6 +1680,15 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { return flattenText(pageBodyTiptapDocument(body)).replace(/\n{3,}/g, '\n\n').trim(); }; + const shouldRetryTransientEmptyLocalAggregate = (session, nextAggregate) => { + if (!session || session.sourceKind !== 'local_folder') return false; + if (!sessionHasRecentExternalSignal(session)) return false; + if (!sessionPlainText(session)) return false; + return !aggregatePlainText(nextAggregate); + }; + + const delay = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms)); + const conflictEnvelopeFromResponse = (payload) => ( payload?.error?.details?.conflict || payload?.details?.conflict @@ -1610,7 +1722,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { const nextBody = nextAggregate?.body || {}; const nextPermissions = nextAggregate?.head?.permissions || {}; const nextConflictKey = conflictDetectionKeyFromBody(nextBody); - const nextTiptapDocument = pageBodyTiptapDocument(nextBody); + const nextTiptapDocument = pageBodyTiptapDocument(nextBody, '', session); const nextSerialized = JSON.stringify(nextTiptapDocument); const nextRevision = Number.isInteger(nextBody.revision) ? nextBody.revision : revisionFromConflictKey(nextConflictKey); session.latestAggregate = nextAggregate; @@ -1937,6 +2049,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { }; const scheduleSessionExternalRefresh = (session, source) => { + if (!session || session.views.size === 0) return; if (session.externalRefreshTimer) return; session.externalRefreshSource = source || session.externalRefreshSource || 'mnote-web-external-change'; session.externalRefreshTimer = window.setTimeout(() => { @@ -1949,6 +2062,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { const refreshSessionFromExternalChange = async (session, source) => { if (document.hidden) return; + if (!session || session.views.size === 0) return; if (session.sourceKind === 'local_folder' && !session.rootUri) return; try { const response = await fetch(pageAggregateUrl({ @@ -1973,11 +2087,26 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { markSessionExternalConflict(session, conflictMessageFromEnvelope(session, conflictEnvelope)); return; } - const nextAggregate = payload?.result; + let nextAggregate = payload?.result; + if (shouldRetryTransientEmptyLocalAggregate(session, nextAggregate)) { + await delay(500); + try { + const retryAggregate = await fetchLatestSessionAggregate(session); + if (aggregatePlainText(retryAggregate)) { + nextAggregate = retryAggregate; + } else { + markSessionExternalConflict(session, '检测到外部编辑器正在写入空内容,已暂停自动刷新以保护当前编辑区。'); + return; + } + } catch (_retryError) { + markSessionExternalConflict(session, externalConflictMessage); + return; + } + } const nextBody = nextAggregate?.body || {}; const nextPermissions = nextAggregate?.head?.permissions || {}; const nextConflictKey = conflictDetectionKeyFromBody(nextBody); - const nextTiptapDocument = pageBodyTiptapDocument(nextBody); + const nextTiptapDocument = pageBodyTiptapDocument(nextBody, '', session); const nextSerialized = JSON.stringify(nextTiptapDocument); const contentChanged = nextSerialized !== session.currentSerialized; session.externalChangePending = false; @@ -2036,11 +2165,17 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { const payload = parseLocalFolderEventPayload(event); if (!payload) return; Array.from(channel.sessions.values()).forEach((targetSession) => { + if (!targetSession || targetSession.views.size === 0) return; const documentId = typeof payload.documentId === 'string' ? payload.documentId.trim() : ''; const eventKind = String(payload.eventKind || ''); const mayAffectMissingDocument = eventKind.includes('Remove') || eventKind.includes('Name'); const targetsCurrentDocument = Boolean(documentId && documentId === targetSession.documentId); if (documentId && !targetsCurrentDocument && !mayAffectMissingDocument) return; + if (targetsCurrentDocument && targetSession.saving) { + targetSession.externalChangePending = false; + targetSession.lastSelfSaveSignalAt = Date.now(); + return; + } targetSession.lastExternalChangeSignalAt = Date.now(); targetSession.externalChangePending = true; if (targetsCurrentDocument && (targetSession.hasExternalConflict || sessionHasRecentLocalInput(targetSession) || targetSession.dirty || targetSession.saveTimer || targetSession.saving)) { @@ -2256,8 +2391,8 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { const conflictDetectionKey = conflictDetectionKeyFromBody(pageBody); const pageBodyRevision = Number.isInteger(pageBody.revision) ? pageBody.revision : null; const keyRevision = revisionFromConflictKey(conflictDetectionKey); - const tiptapDocument = pageBodyTiptapDocument(pageBody); const sourceKind = normalizeSessionSourceKind(runtimeDescriptor.bootstrap); + const tiptapDocument = pageBodyTiptapDocument(pageBody, '', runtimeDescriptor.bootstrap); const session = { key: buildDocumentSessionKey(runtimeDescriptor.bootstrap), documentId: runtimeDescriptor.bootstrap.documentId, @@ -2282,6 +2417,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { externalChangePending: false, externalRefreshSource: '', lastExternalChangeSignalAt: 0, + lastSelfSaveSignalAt: 0, lastUserInputAt: 0, status: 'booting', error: null, @@ -3498,10 +3634,16 @@ mod tests { assert!(html.contains("/api/tree/events")); assert!(html.contains("data-mnote-tree-live-transport")); assert!(html.contains("syncPageAggregateScript(session, nextAggregate);")); - assert!(html.contains("const pageBodyTiptapDocument = (body, fallbackText = '') => {")); + assert!(html.contains( + "const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => {" + )); assert!(html.contains("body?.blockDocument || body?.block_document")); - assert!(html.contains("const nextTiptapDocument = pageBodyTiptapDocument(nextBody);")); - assert!(html.contains("const tiptapDocument = pageBodyTiptapDocument(pageBody);")); + assert!(html.contains("localizeTiptapAssetUrls(toTiptapDocument(body.content")); + assert!(html + .contains("const nextTiptapDocument = pageBodyTiptapDocument(nextBody, '', session);")); + assert!(html.contains( + "const tiptapDocument = pageBodyTiptapDocument(pageBody, '', runtimeDescriptor.bootstrap);" + )); assert!(!html.contains("mnote-web-document-shell")); } @@ -3655,10 +3797,10 @@ mod tests { let root = std::env::temp_dir().join(format!("mnote-local-page-aggregate-{}", std::process::id())); let _ = std::fs::remove_dir_all(&root); - std::fs::create_dir_all(&root).expect("create local root"); + std::fs::create_dir_all(root.join("Local Aggregate")).expect("create local page bundle"); std::fs::write( - root.join("README.md"), - "---\ntitle: Local Aggregate\n---\n# Local Heading\n正文内容\n", + root.join("Local Aggregate").join("Local Aggregate.md"), + "# Local Heading\n正文内容\n", ) .expect("write local md"); @@ -3668,7 +3810,7 @@ mod tests { .oneshot( Request::builder() .uri(format!( - "/api/page-aggregate/local-md:README.md?sourceKind=local_folder&rootUri={root_uri}" + "/api/page-aggregate/local-md:Local~20Aggregate~2FLocal~20Aggregate.md?sourceKind=local_folder&rootUri={root_uri}" )) .header("x-mnote-actor-id", "user_test") .header("x-mnote-actor-type", "user") @@ -3695,7 +3837,7 @@ mod tests { assert_eq!(payload["schema"], "mnote.page_aggregate.v1"); assert_eq!( payload["result"]["identity"]["documentId"], - "local-md:README.md" + "local-md:Local~20Aggregate~2FLocal~20Aggregate.md" ); assert_eq!(payload["result"]["head"]["title"], "Local Aggregate"); assert_eq!(payload["result"]["head"]["permissions"]["readOnly"], false); @@ -3710,10 +3852,19 @@ mod tests { let root = std::env::temp_dir().join(format!("mnote-local-document-shell-{}", std::process::id())); let _ = std::fs::remove_dir_all(&root); - std::fs::create_dir_all(root.join("docs")).expect("create local docs"); - std::fs::write(root.join("README.md"), "# Local Shell\n正文\n").expect("write root md"); - std::fs::write(root.join("docs").join("child.md"), "# Child Page\n") - .expect("write child md"); + std::fs::create_dir_all(root.join("Local Shell")).expect("create local page bundle"); + std::fs::create_dir_all(root.join("docs").join("Child Page")) + .expect("create local child bundle"); + std::fs::write( + root.join("Local Shell").join("Local Shell.md"), + "# Local Shell\n正文\n", + ) + .expect("write root md"); + std::fs::write( + root.join("docs").join("Child Page").join("Child Page.md"), + "# Child Page\n", + ) + .expect("write child md"); std::fs::write(root.join("asset.png"), b"png").expect("write asset"); let root_uri = format!("file://{}", root.display()); @@ -3722,7 +3873,7 @@ mod tests { .oneshot( Request::builder() .uri(format!( - "/documents/local-md:README.md?sourceKind=local_folder&rootUri={root_uri}&treeView=filetree" + "/documents/local-md:Local~20Shell~2FLocal~20Shell.md?sourceKind=local_folder&rootUri={root_uri}&treeView=filetree" )) .header("x-mnote-actor-id", "user_test") .header("x-mnote-actor-type", "user") @@ -3771,6 +3922,10 @@ mod tests { .contains("if (!response.ok) {\n if (session.sourceKind === 'local_folder')")); assert!(html.contains("mayAffectMissingDocument")); assert!(html.contains("eventKind.includes('Remove') || eventKind.includes('Name')")); + assert!(html.contains("targetSession.views.size === 0")); + assert!(html.contains("session.views.size === 0")); + assert!(html.contains("targetSession.saving")); + assert!(html.contains("lastSelfSaveSignalAt")); assert!(html.contains("command: 'replaceContent'")); assert!(html.contains("external-change-conflict")); assert!(html.contains("mnote-editor-conflict-panel")); @@ -3875,6 +4030,8 @@ mod tests { let html = String::from_utf8(body.to_vec()).expect("html"); assert!(html.contains("Spec")); assert!(html.contains("assets/spec.pdf")); + assert!(html.contains(r#"data-mnote-source-kind="local_folder""#)); + assert!(html.contains(r#"data-mnote-root-uri="file://"#)); } #[tokio::test] diff --git a/rust/crates/mnote-web/src/ssr/pages/document.rs b/rust/crates/mnote-web/src/ssr/pages/document.rs index 47f1198d..405e326f 100644 --- a/rust/crates/mnote-web/src/ssr/pages/document.rs +++ b/rust/crates/mnote-web/src/ssr/pages/document.rs @@ -191,6 +191,9 @@ pub fn DocumentPage( /// 是否显示管理员授权入口 #[prop(optional)] show_admin_access_policy: bool, + /// 是否启用树实时流 + #[prop(optional, default = true)] + enable_tree_live: bool, ) -> impl IntoView { let has_page_subtree = page_subtree_json .as_deref() @@ -288,7 +291,7 @@ pub fn DocumentPage( visible: secondary_visible, }; view! { - +
+ {move || if has_active_page { view! {
@@ -81,8 +81,7 @@ pub fn HomePage( type="button" class="mnote-empty-create-page" data-testid="mnote-empty-create-page" - data-mnote-action="create-page" - data-workspace-id={workspace_id_value.clone()} + data-mnote-action="create-local-workspace" >"新建页面" }.into_any() diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index 33b9be45..a2e5c9ea 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -625,10 +625,16 @@ const SIDEBAR_TREE_JS: &str = r##" return (new URLSearchParams(window.location.search).get('workspaceId') || 'default').trim() || 'default'; } + function currentWorkspaceId() { + return resolveWorkspaceId(document.body); + } + function currentSourceKind() { var params = new URLSearchParams(window.location.search); var fromUrl = (params.get('sourceKind') || '').trim(); if (fromUrl) return fromUrl; + var fromBody = document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-source-kind') || '').trim() : ''; + if (fromBody) return fromBody; if ( window.location.pathname === '/' && !(params.get('workspaceId') || '').trim() && @@ -641,7 +647,10 @@ const SIDEBAR_TREE_JS: &str = r##" } function currentRootUri() { - return (new URLSearchParams(window.location.search).get('rootUri') || '').trim(); + var params = new URLSearchParams(window.location.search); + var fromUrl = (params.get('rootUri') || '').trim(); + if (fromUrl) return fromUrl; + return document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-root-uri') || '').trim() : ''; } function rememberCloudWorkspaceId(workspaceId) { @@ -691,6 +700,10 @@ const SIDEBAR_TREE_JS: &str = r##" var value = (params.get(name) || '').trim(); if (value) targetUrl.searchParams.set(name, value); }); + var sourceKind = currentSourceKind(); + var rootUri = currentRootUri(); + if (sourceKind && !targetUrl.searchParams.get('sourceKind')) targetUrl.searchParams.set('sourceKind', sourceKind); + if (rootUri && !targetUrl.searchParams.get('rootUri')) targetUrl.searchParams.set('rootUri', rootUri); } function currentWorkspaceSourcePayload() { @@ -700,6 +713,10 @@ const SIDEBAR_TREE_JS: &str = r##" var value = (params.get(name) || '').trim(); if (value) payload[name] = value; }); + var sourceKind = currentSourceKind(); + var rootUri = currentRootUri(); + if (sourceKind && !payload.sourceKind) payload.sourceKind = sourceKind; + if (rootUri && !payload.rootUri) payload.rootUri = rootUri; return payload; } @@ -838,13 +855,19 @@ const SIDEBAR_TREE_JS: &str = r##" document.documentElement.setAttribute('data-mnote-trash-modal-open', 'true'); var content = overlay.querySelector('[data-testid="mnote-trash-modal-content"]'); var sourceKind = currentSourceKind(); - if (sourceKind === 'local_folder') { - renderLocalFolderTrashPlaceholder(content, new URLSearchParams(window.location.search).get('rootUri') || ''); - return; - } var workspaceId = resolveWorkspaceId(trigger || document.body); var url = new URL('/trash', window.location.origin); - if (workspaceId) url.searchParams.set('workspaceId', workspaceId); + if (sourceKind === 'local_folder') { + var rootUri = currentRootUri(); + if (!rootUri) { + renderLocalFolderTrashPlaceholder(content, ''); + return; + } + url.searchParams.set('sourceKind', 'local_folder'); + url.searchParams.set('rootUri', rootUri); + } else if (workspaceId) { + url.searchParams.set('workspaceId', workspaceId); + } fetch(url.toString(), { headers: { 'x-mnote-trash-modal': '1' } }).then(function(response) { return response.text().then(function(html) { if (!response.ok) throw new Error('trash_modal_load_failed_' + response.status); @@ -1294,7 +1317,7 @@ const SIDEBAR_TREE_JS: &str = r##" title: '新页面' }); var nextWorkspaceId = result.workspaceId || workspaceId; - navigateToDocument(result.documentId, nextWorkspaceId, { treeView: activeSidebarTreeMode() }); + navigateToDocument(commandDocumentId(result, ''), nextWorkspaceId, { treeView: activeSidebarTreeMode() }); } function applySidebarTreeTab(mode, shell) { @@ -1450,6 +1473,7 @@ const SIDEBAR_TREE_JS: &str = r##" function commandDocumentId(result, fallback) { var value = result && ( + (result.execution && (result.execution.documentId || result.execution.id || result.execution.nodeId)) || result.documentId || result.id || result.nodeId || @@ -1462,6 +1486,7 @@ const SIDEBAR_TREE_JS: &str = r##" function commandDocumentTitle(result, fallback) { var value = result && ( + (result.execution && result.execution.title) || result.title || (result.document && result.document.title) || (result.node && result.node.title) || @@ -1581,6 +1606,17 @@ const SIDEBAR_TREE_JS: &str = r##" return pageChanged || fileChanged; } + function localCommandNeedsProjectionRefresh(action, result) { + if (currentSourceKind() !== 'local_folder') return false; + if (action === 'create' || action === 'rename' || action === 'move' || action === 'delete' || action === 'archive' || action === 'trash' || action === 'purge') { + return true; + } + if (result && (result.previousDocumentId || result.previousRelativePath || result.relativePath || result.trashPath)) { + return true; + } + return false; + } + function sortOrderFromDelta(data) { var raw = data && (data.sortOrder ?? data.sort_order); var value = Number(raw); @@ -1798,9 +1834,8 @@ const SIDEBAR_TREE_JS: &str = r##" function normalizeMindmapFileTreeTitle(rawTitle, assetId, iconKind, objectIdentity) { var title = String(rawTitle || '').trim(); var isMindmap = iconKind === 'mindmap' || String(objectIdentity && objectIdentity.objectKind || '') === 'mindmap'; - var generated = /^mindmap[-_]/i.test(title); - if (!isMindmap || (!generated && title.length <= 24)) return title || '无标题'; - return shortMindmapFileName(assetId || title); + if (!isMindmap) return title || '无标题'; + return title || shortMindmapFileName(assetId); } function renderFileRows(parentId, grouped, activeId, activeRowId) { @@ -1865,9 +1900,8 @@ const SIDEBAR_TREE_JS: &str = r##" } async function refreshLocalFolderSidebarSnapshot() { - var params = new URLSearchParams(window.location.search); var workspaceId = currentWorkspaceId(); - var rootUri = (params.get('rootUri') || '').trim(); + var rootUri = currentRootUri(); var currentId = currentDocumentId(); if (!workspaceId || !rootUri) return false; @@ -1900,9 +1934,8 @@ const SIDEBAR_TREE_JS: &str = r##" } function startLocalFolderSidebarWatch() { - var params = new URLSearchParams(window.location.search); - if ((params.get('sourceKind') || '').trim() !== 'local_folder') return; - var rootUri = (params.get('rootUri') || '').trim(); + if (currentSourceKind() !== 'local_folder') return; + var rootUri = currentRootUri(); if (!rootUri) return; var revision = ''; var refreshTimer = 0; @@ -2029,11 +2062,29 @@ const SIDEBAR_TREE_JS: &str = r##" return '/onlyoffice?' + params.toString(); } + function buildLocalOnlyOfficeOpenUrl(relativePath, fileName, documentId, assetId) { + var fileType = inferOnlyOfficeFileType(fileName || relativePath || '', ''); + if (!fileType) return ''; + var fileUrl = buildLocalFileOpenUrl(relativePath, false); + if (!fileUrl) return ''; + return buildOnlyOfficeOpenUrl({ + fileUrl: fileUrl, + fileName: fileName || relativePath || '未命名资源', + fileType: fileType, + assetId: assetId || ('local-file:' + relativePath), + documentId: documentId || currentDocumentId() || '', + userId: '', + mode: 'edit' + }); + } + function buildMindmapOpenPath(documentId, assetId) { var doc = String(documentId || '').trim(); var map = String(assetId || '').trim(); if (!doc || !map) return ''; - return '/mindmap/' + encodeURIComponent(doc) + '/' + encodeURIComponent(map); + var targetUrl = new URL('/mindmap/' + encodeURIComponent(doc) + '/' + encodeURIComponent(map), window.location.origin); + copyWorkspaceSourceParams(targetUrl); + return targetUrl.pathname + targetUrl.search; } function navigateToMindmapObject(documentId, assetId, workspaceId) { @@ -2077,7 +2128,11 @@ const SIDEBAR_TREE_JS: &str = r##" var assetId = String(detail && detail.assetId || '').trim(); var assetType = String(detail && detail.assetType || '').trim(); if (assetType === 'mindmap') return true; - return assetId.indexOf('mindmap_') === 0 || assetId.indexOf('mindmap-') === 0; + var fileName = assetId.indexOf('/') >= 0 ? assetId.split('/').pop() : assetId; + return assetId.indexOf('mindmap_') === 0 + || assetId.indexOf('mindmap-') === 0 + || /\.mindmap\.json$/i.test(fileName) + || (/^思维导图/i.test(fileName) && /\.json$/i.test(fileName)); } function localFilePathFromAssetId(assetId) { @@ -2126,6 +2181,18 @@ const SIDEBAR_TREE_JS: &str = r##" if (!assetId) return; var localFilePath = localFilePathFromAssetId(assetId); if (localFilePath) { + var localFileName = localFilePath.split('/').pop() || localFilePath; + if (isMindmapAssetDetail(detail) || String(detail && detail.assetType || '').trim() === 'mindmap') { + document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'local-mindmap-object-shell'); + document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', assetId); + navigateToMindmapObject(String(detail && detail.documentId || currentDocumentId() || '').trim(), assetId, String(detail.workspaceId || '').trim()); + return; + } + var localOfficeUrl = buildLocalOnlyOfficeOpenUrl(localFilePath, localFileName, String(detail && detail.documentId || currentDocumentId() || '').trim(), assetId); + if (localOfficeUrl) { + window.open(localOfficeUrl, '_blank', 'noopener,noreferrer'); + return; + } var localFileUrl = buildLocalFileOpenUrl(localFilePath, false); if (localFileUrl) window.open(localFileUrl, '_blank', 'noopener,noreferrer'); return; @@ -2267,6 +2334,18 @@ const SIDEBAR_TREE_JS: &str = r##" return String(asset && (asset.sourcePath || asset.file_url || asset.signedUrl || asset.signed_url || asset.thumbnail_url) || '').trim(); } + function localAssetOpenUrl(asset, download) { + if (!isLocalUploadedAsset(asset)) return ''; + var rootUri = String(asset && (asset.rootUri || asset.root_uri) || '').trim() || currentRootUri(); + var rootRelativePath = String(asset && (asset.rootRelativePath || asset.root_relative_path) || '').trim(); + if (!rootUri || !rootRelativePath) return ''; + var url = new URL('/api/local-folder/files/open', window.location.origin); + url.searchParams.set('rootUri', rootUri); + url.searchParams.set('path', rootRelativePath); + if (download) url.searchParams.set('download', 'true'); + return url.toString(); + } + function uploadedAssetType(asset) { return String(asset && (asset.asset_type || asset.assetType || asset.mime_type || '') || '').trim(); } @@ -2360,11 +2439,23 @@ const SIDEBAR_TREE_JS: &str = r##" } function buildOnlyOfficeAssetOpenUrl(asset, userId) { - if (isLocalUploadedAsset(asset)) return ''; var title = uploadedAssetTitle(asset); var fileType = inferOnlyOfficeFileType(title, asset && asset.mime_type); if (!fileType) return ''; var assetId = String(asset && asset.id || '').trim(); + if (isLocalUploadedAsset(asset)) { + var localUrl = localAssetOpenUrl(asset, false); + if (!localUrl) return ''; + return buildOnlyOfficeOpenUrl({ + fileUrl: localUrl, + fileName: title, + fileType: fileType, + assetId: assetId, + documentId: String(asset && (asset.document_id || asset.documentId) || currentDocumentId() || '').trim(), + userId: userId || '', + mode: 'edit' + }); + } return buildOnlyOfficeOpenUrl({ fileUrl: assetId ? '' : uploadedAssetUrl(asset), fileName: title, @@ -2402,14 +2493,17 @@ const SIDEBAR_TREE_JS: &str = r##" legacyOfficeAttachmentIndex = Object.create(null); return legacyOfficeAttachmentIndex; } - legacyOfficeAttachmentIndexPending = fetch( - '/api/tree/projections/file?documentId=' + encodeURIComponent(documentId) + '&workspaceId=' + encodeURIComponent(workspaceId), - { - method: 'GET', - credentials: 'include', - cache: 'no-store' - } - ).then(function(response) { + var projectionUrl = new URL('/api/tree/projections/file', window.location.origin); + projectionUrl.searchParams.set('documentId', documentId); + projectionUrl.searchParams.set('workspaceId', workspaceId); + var sourcePayload = currentWorkspaceSourcePayload(); + if (sourcePayload.sourceKind) projectionUrl.searchParams.set('sourceKind', sourcePayload.sourceKind); + if (sourcePayload.rootUri) projectionUrl.searchParams.set('rootUri', sourcePayload.rootUri); + legacyOfficeAttachmentIndexPending = fetch(projectionUrl.toString(), { + method: 'GET', + credentials: 'include', + cache: 'no-store' + }).then(function(response) { return response.json().catch(function() { return null; }).then(function(payload) { var items = payload && payload.ok && payload.result && Array.isArray(payload.result.items) ? payload.result.items @@ -2581,11 +2675,22 @@ const SIDEBAR_TREE_JS: &str = r##" var container = children || root; var li = document.createElement('li'); li.className = 'tree-node'; + var objectKind = String(asset && (asset.objectKind || asset.resourceKind || '') || '').trim(); + if (!objectKind && (String(asset && (asset.asset_type || asset.assetType) || '').trim() === 'mindmap' || /\.mindmap\.json$/i.test(assetId))) { + objectKind = 'mindmap'; + } + var objectIdentity = { + objectKind: objectKind || 'attachment', + documentId: targetDocumentId || null, + blockId: null, + assetId: assetId + }; li.setAttribute('data-node-id', 'asset:' + assetId); var title = uploadedAssetTitle(asset); var iconKind = uploadedAssetType(asset) || 'file'; + if (objectKind === 'mindmap') iconKind = 'mindmap'; li.innerHTML = - '