march-control-cli 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +220 -0
  2. package/core/apply.js +152 -0
  3. package/core/backup.js +53 -0
  4. package/core/constants.js +55 -0
  5. package/core/desktop-service.js +219 -0
  6. package/core/desktop-state.js +511 -0
  7. package/core/index.js +1293 -0
  8. package/core/paths.js +71 -0
  9. package/core/presets.js +171 -0
  10. package/core/probe.js +70 -0
  11. package/core/store.js +218 -0
  12. package/core/utils.js +178 -0
  13. package/core/writers/codex.js +102 -0
  14. package/core/writers/index.js +16 -0
  15. package/core/writers/openclaw.js +93 -0
  16. package/core/writers/opencode.js +91 -0
  17. package/desktop/assets/march-mark.svg +21 -0
  18. package/desktop/main.js +192 -0
  19. package/desktop/preload.js +49 -0
  20. package/desktop/renderer/app.js +327 -0
  21. package/desktop/renderer/index.html +130 -0
  22. package/desktop/renderer/styles.css +413 -0
  23. package/package.json +106 -0
  24. package/scripts/desktop-dev.mjs +90 -0
  25. package/scripts/postinstall.mjs +28 -0
  26. package/scripts/serve-site.mjs +51 -0
  27. package/site/app.js +10 -0
  28. package/site/assets/march-mark.svg +22 -0
  29. package/site/index.html +286 -0
  30. package/site/styles.css +566 -0
  31. package/src/App.tsx +1186 -0
  32. package/src/components/layout/app-sidebar.tsx +103 -0
  33. package/src/components/layout/top-toolbar.tsx +44 -0
  34. package/src/components/layout/workspace-tabs.tsx +32 -0
  35. package/src/components/providers/inspector-panel.tsx +84 -0
  36. package/src/components/providers/metric-strip.tsx +26 -0
  37. package/src/components/providers/provider-editor.tsx +87 -0
  38. package/src/components/providers/provider-table.tsx +85 -0
  39. package/src/components/ui/logo-mark.tsx +16 -0
  40. package/src/features/mcp/mcp-view.tsx +45 -0
  41. package/src/features/prompts/prompts-view.tsx +40 -0
  42. package/src/features/providers/providers-view.tsx +40 -0
  43. package/src/features/providers/types.ts +8 -0
  44. package/src/features/skills/skills-view.tsx +44 -0
  45. package/src/hooks/use-control-workspace.ts +184 -0
  46. package/src/index.css +22 -0
  47. package/src/lib/client.ts +944 -0
  48. package/src/lib/query-client.ts +3 -0
  49. package/src/lib/workspace-sections.ts +34 -0
  50. package/src/main.tsx +14 -0
  51. package/src/types.ts +76 -0
  52. package/src/vite-env.d.ts +56 -0
  53. package/src-tauri/README.md +11 -0
@@ -0,0 +1,511 @@
1
+ import { SUPPORTED_PLATFORMS } from "./constants.js";
2
+ import { getMarchDesktopStatePath } from "./paths.js";
3
+ import { readJson, writeJson } from "./utils.js";
4
+
5
+ const SUPPORTED_APP_TYPES = ["global", ...SUPPORTED_PLATFORMS];
6
+
7
+ function now() {
8
+ return new Date().toISOString();
9
+ }
10
+
11
+ function normalizeText(value, fallback = "") {
12
+ const text = `${value ?? ""}`.trim();
13
+ return text || fallback;
14
+ }
15
+
16
+ function uniqueStrings(values) {
17
+ const result = [];
18
+ const seen = new Set();
19
+
20
+ for (const value of values) {
21
+ const text = normalizeText(value);
22
+ if (!text) {
23
+ continue;
24
+ }
25
+
26
+ const key = text.toLowerCase();
27
+ if (seen.has(key)) {
28
+ continue;
29
+ }
30
+
31
+ seen.add(key);
32
+ result.push(text);
33
+ }
34
+
35
+ return result;
36
+ }
37
+
38
+ function createId(prefix, seed) {
39
+ const slug = normalizeText(seed)
40
+ .toLowerCase()
41
+ .replace(/[^a-z0-9]+/g, "-")
42
+ .replace(/^-+|-+$/g, "")
43
+ .slice(0, 24);
44
+
45
+ const rand = Math.random().toString(36).slice(2, 8);
46
+ return `${prefix}-${slug || "item"}-${rand}`;
47
+ }
48
+
49
+ function sanitizeEnabledPlatforms(raw, fallbackPlatforms) {
50
+ const base = Array.isArray(fallbackPlatforms) && fallbackPlatforms.length > 0 ? fallbackPlatforms : SUPPORTED_PLATFORMS;
51
+ const values = Array.isArray(raw) ? raw : base;
52
+ const seen = new Set();
53
+ const result = [];
54
+
55
+ for (const value of values) {
56
+ if (!SUPPORTED_PLATFORMS.includes(value)) {
57
+ continue;
58
+ }
59
+
60
+ if (seen.has(value)) {
61
+ continue;
62
+ }
63
+
64
+ seen.add(value);
65
+ result.push(value);
66
+ }
67
+
68
+ return result.length > 0 ? result : [...base];
69
+ }
70
+
71
+ function sanitizeTags(raw, fallback = []) {
72
+ const values = Array.isArray(raw) ? raw : fallback;
73
+ const tags = uniqueStrings(values);
74
+ return tags.length > 0 ? tags : [];
75
+ }
76
+
77
+ function sanitizeAppType(raw, fallback = "global") {
78
+ const value = normalizeText(raw, fallback);
79
+ return SUPPORTED_APP_TYPES.includes(value) ? value : fallback;
80
+ }
81
+
82
+ function createDefaultDesktopState() {
83
+ return {
84
+ mcpServers: [
85
+ {
86
+ id: "filesystem",
87
+ name: "文件系统",
88
+ description: "已挂载工作区根目录,可读写",
89
+ tags: ["files", "storage"],
90
+ enabledPlatforms: ["codex", "opencode", "openclaw"]
91
+ },
92
+ {
93
+ id: "browser",
94
+ name: "浏览器",
95
+ description: "可访问文档、版本发布和参考资料",
96
+ tags: ["web", "docs"],
97
+ enabledPlatforms: ["codex", "openclaw"]
98
+ },
99
+ {
100
+ id: "git",
101
+ name: "Git",
102
+ description: "可读取仓库元数据与分支关系",
103
+ tags: ["code", "history"],
104
+ enabledPlatforms: ["codex", "opencode", "openclaw"]
105
+ },
106
+ {
107
+ id: "shell",
108
+ name: "命令行",
109
+ description: "可在工作区边界内执行本地命令",
110
+ tags: ["terminal"],
111
+ enabledPlatforms: ["codex", "opencode"]
112
+ }
113
+ ],
114
+ prompts: [
115
+ {
116
+ id: "default-coding",
117
+ appType: "global",
118
+ name: "默认编码",
119
+ description: "用于开发实现、调试和仓库级改动。",
120
+ content: "优先做可落地实现、Provider 流程和直接代码修改,减少空泛规划。",
121
+ enabled: true,
122
+ updatedAt: now()
123
+ },
124
+ {
125
+ id: "ui-review",
126
+ appType: "codex",
127
+ name: "界面评审",
128
+ description: "用于视觉比对、布局评估和细节打磨。",
129
+ content: "按目标参考评审当前界面,识别间距、密度、交互和品牌表达差距。",
130
+ enabled: false,
131
+ updatedAt: now()
132
+ },
133
+ {
134
+ id: "provider-setup",
135
+ appType: "openclaw",
136
+ name: "Provider 配置",
137
+ description: "用于中转配置、路由切换和安装流程。",
138
+ content: "重点检查 Provider 连通性、路由健康度、代理设置和目标配置文件。",
139
+ enabled: true,
140
+ updatedAt: now()
141
+ }
142
+ ],
143
+ skills: [
144
+ {
145
+ id: "openai-docs",
146
+ name: "openai-docs",
147
+ description: "查询 OpenAI 官方文档",
148
+ directory: "~/.codex/skills/.system/openai-docs",
149
+ enabledPlatforms: ["codex"]
150
+ },
151
+ {
152
+ id: "plugin-creator",
153
+ name: "plugin-creator",
154
+ description: "创建并生成 Codex 插件骨架",
155
+ directory: "~/.codex/skills/.system/plugin-creator",
156
+ enabledPlatforms: ["codex"]
157
+ },
158
+ {
159
+ id: "skill-installer",
160
+ name: "skill-installer",
161
+ description: "从精选来源安装技能",
162
+ directory: "~/.codex/skills/.system/skill-installer",
163
+ enabledPlatforms: ["codex"]
164
+ },
165
+ {
166
+ id: "xianyu-keyword-research",
167
+ name: "xianyu-keyword-research",
168
+ description: "构建闲鱼关键词结构化词池",
169
+ directory: "~/.codex/skills/xianyu-keyword-research",
170
+ enabledPlatforms: ["codex", "openclaw"]
171
+ }
172
+ ],
173
+ skillRepos: [
174
+ {
175
+ id: "anthropics-skills",
176
+ owner: "anthropics",
177
+ name: "skills",
178
+ branch: "main",
179
+ enabled: true
180
+ },
181
+ {
182
+ id: "composio-awesome-claude-skills",
183
+ owner: "ComposioHQ",
184
+ name: "awesome-claude-skills",
185
+ branch: "master",
186
+ enabled: true
187
+ },
188
+ {
189
+ id: "cexll-myclaude",
190
+ owner: "cexll",
191
+ name: "myclaude",
192
+ branch: "master",
193
+ enabled: true
194
+ },
195
+ {
196
+ id: "jimliu-baoyu-skills",
197
+ owner: "JimLiu",
198
+ name: "baoyu-skills",
199
+ branch: "main",
200
+ enabled: true
201
+ }
202
+ ]
203
+ };
204
+ }
205
+
206
+ function normalizeState(raw, fallback) {
207
+ const mcpServers = Array.isArray(raw?.mcpServers)
208
+ ? raw.mcpServers
209
+ .map((server, index) => {
210
+ const fallbackServer = fallback.mcpServers[index] || fallback.mcpServers[0];
211
+ const id = normalizeText(server?.id);
212
+ if (!id) {
213
+ return null;
214
+ }
215
+
216
+ return {
217
+ id,
218
+ name: normalizeText(server?.name, fallbackServer.name),
219
+ description: normalizeText(server?.description, fallbackServer.description),
220
+ tags: sanitizeTags(server?.tags, fallbackServer.tags),
221
+ homepage: normalizeText(server?.homepage) || undefined,
222
+ enabledPlatforms: sanitizeEnabledPlatforms(server?.enabledPlatforms, fallbackServer.enabledPlatforms)
223
+ };
224
+ })
225
+ .filter(Boolean)
226
+ : fallback.mcpServers;
227
+
228
+ const prompts = Array.isArray(raw?.prompts)
229
+ ? raw.prompts
230
+ .map((prompt, index) => {
231
+ const fallbackPrompt = fallback.prompts[index] || fallback.prompts[0];
232
+ const id = normalizeText(prompt?.id);
233
+ if (!id) {
234
+ return null;
235
+ }
236
+
237
+ return {
238
+ id,
239
+ appType: sanitizeAppType(prompt?.appType, fallbackPrompt.appType),
240
+ name: normalizeText(prompt?.name, fallbackPrompt.name),
241
+ description: normalizeText(prompt?.description, fallbackPrompt.description),
242
+ content: normalizeText(prompt?.content, fallbackPrompt.content),
243
+ enabled: typeof prompt?.enabled === "boolean" ? prompt.enabled : fallbackPrompt.enabled,
244
+ updatedAt: normalizeText(prompt?.updatedAt, now())
245
+ };
246
+ })
247
+ .filter(Boolean)
248
+ : fallback.prompts;
249
+
250
+ const skills = Array.isArray(raw?.skills)
251
+ ? raw.skills
252
+ .map((skill, index) => {
253
+ const fallbackSkill = fallback.skills[index] || fallback.skills[0];
254
+ const id = normalizeText(skill?.id);
255
+ if (!id) {
256
+ return null;
257
+ }
258
+
259
+ return {
260
+ id,
261
+ name: normalizeText(skill?.name, fallbackSkill.name),
262
+ description: normalizeText(skill?.description, fallbackSkill.description),
263
+ directory: normalizeText(skill?.directory, fallbackSkill.directory),
264
+ enabledPlatforms: sanitizeEnabledPlatforms(skill?.enabledPlatforms, fallbackSkill.enabledPlatforms)
265
+ };
266
+ })
267
+ .filter(Boolean)
268
+ : fallback.skills;
269
+
270
+ const skillRepos = Array.isArray(raw?.skillRepos)
271
+ ? raw.skillRepos
272
+ .map((repo, index) => {
273
+ const fallbackRepo = fallback.skillRepos[index] || fallback.skillRepos[0];
274
+ const id = normalizeText(repo?.id);
275
+ if (!id) {
276
+ return null;
277
+ }
278
+
279
+ return {
280
+ id,
281
+ owner: normalizeText(repo?.owner, fallbackRepo.owner),
282
+ name: normalizeText(repo?.name, fallbackRepo.name),
283
+ branch: normalizeText(repo?.branch, fallbackRepo.branch),
284
+ enabled: typeof repo?.enabled === "boolean" ? repo.enabled : fallbackRepo.enabled
285
+ };
286
+ })
287
+ .filter(Boolean)
288
+ : fallback.skillRepos;
289
+
290
+ return {
291
+ mcpServers: mcpServers.length > 0 ? mcpServers : fallback.mcpServers,
292
+ prompts: prompts.length > 0 ? prompts : fallback.prompts,
293
+ skills: skills.length > 0 ? skills : fallback.skills,
294
+ skillRepos: skillRepos.length > 0 ? skillRepos : fallback.skillRepos
295
+ };
296
+ }
297
+
298
+ function loadDesktopState() {
299
+ const fallback = createDefaultDesktopState();
300
+ const stored = readJson(getMarchDesktopStatePath(), null);
301
+ const normalized = normalizeState(stored, fallback);
302
+
303
+ if (!stored) {
304
+ writeJson(getMarchDesktopStatePath(), normalized);
305
+ }
306
+
307
+ return normalized;
308
+ }
309
+
310
+ function saveDesktopState(state) {
311
+ writeJson(getMarchDesktopStatePath(), state);
312
+ }
313
+
314
+ export function getDesktopState() {
315
+ return loadDesktopState();
316
+ }
317
+
318
+ export function toggleMcpServerForPlatform(serverId, platform) {
319
+ if (!SUPPORTED_PLATFORMS.includes(platform)) {
320
+ throw new Error(`Unsupported platform: ${platform}`);
321
+ }
322
+
323
+ const state = loadDesktopState();
324
+ const server = state.mcpServers.find((item) => item.id === serverId);
325
+ if (!server) {
326
+ throw new Error(`MCP server not found: ${serverId}`);
327
+ }
328
+
329
+ server.enabledPlatforms = server.enabledPlatforms.includes(platform)
330
+ ? server.enabledPlatforms.filter((item) => item !== platform)
331
+ : [...server.enabledPlatforms, platform];
332
+
333
+ saveDesktopState(state);
334
+ return state;
335
+ }
336
+
337
+ export function upsertMcpServerFromDesktop(input) {
338
+ const state = loadDesktopState();
339
+ const id = normalizeText(input?.id);
340
+ const name = normalizeText(input?.name);
341
+ const description = normalizeText(input?.description);
342
+ if (!name) {
343
+ throw new Error("MCP 名称不能为空");
344
+ }
345
+ if (!description) {
346
+ throw new Error("MCP 描述不能为空");
347
+ }
348
+
349
+ const payload = {
350
+ id: id || createId("mcp", name),
351
+ name,
352
+ description,
353
+ tags: sanitizeTags(input?.tags),
354
+ homepage: normalizeText(input?.homepage) || undefined,
355
+ enabledPlatforms: sanitizeEnabledPlatforms(input?.enabledPlatforms, SUPPORTED_PLATFORMS)
356
+ };
357
+
358
+ const index = state.mcpServers.findIndex((item) => item.id === payload.id);
359
+ if (index >= 0) {
360
+ state.mcpServers[index] = payload;
361
+ } else {
362
+ state.mcpServers.unshift(payload);
363
+ }
364
+
365
+ saveDesktopState(state);
366
+ return state;
367
+ }
368
+
369
+ export function deleteMcpServerFromDesktop(serverId) {
370
+ const id = normalizeText(serverId);
371
+ if (!id) {
372
+ throw new Error("MCP ID 不能为空");
373
+ }
374
+
375
+ const state = loadDesktopState();
376
+ const next = state.mcpServers.filter((item) => item.id !== id);
377
+ if (next.length === state.mcpServers.length) {
378
+ throw new Error(`MCP server not found: ${id}`);
379
+ }
380
+
381
+ state.mcpServers = next;
382
+ saveDesktopState(state);
383
+ return state;
384
+ }
385
+
386
+ export function togglePromptFromDesktop(promptId) {
387
+ const state = loadDesktopState();
388
+ const prompt = state.prompts.find((item) => item.id === promptId);
389
+ if (!prompt) {
390
+ throw new Error(`Prompt not found: ${promptId}`);
391
+ }
392
+
393
+ prompt.enabled = !prompt.enabled;
394
+ prompt.updatedAt = now();
395
+ saveDesktopState(state);
396
+ return state;
397
+ }
398
+
399
+ export function upsertPromptFromDesktop(input) {
400
+ const state = loadDesktopState();
401
+ const id = normalizeText(input?.id);
402
+ const name = normalizeText(input?.name);
403
+ const description = normalizeText(input?.description);
404
+ const content = normalizeText(input?.content);
405
+
406
+ if (!name) {
407
+ throw new Error("提示词名称不能为空");
408
+ }
409
+ if (!content) {
410
+ throw new Error("提示词内容不能为空");
411
+ }
412
+
413
+ const payload = {
414
+ id: id || createId("prompt", name),
415
+ appType: sanitizeAppType(input?.appType, "global"),
416
+ name,
417
+ description,
418
+ content,
419
+ enabled: typeof input?.enabled === "boolean" ? input.enabled : true,
420
+ updatedAt: now()
421
+ };
422
+
423
+ const index = state.prompts.findIndex((item) => item.id === payload.id);
424
+ if (index >= 0) {
425
+ state.prompts[index] = payload;
426
+ } else {
427
+ state.prompts.unshift(payload);
428
+ }
429
+
430
+ saveDesktopState(state);
431
+ return state;
432
+ }
433
+
434
+ export function deletePromptFromDesktop(promptId) {
435
+ const id = normalizeText(promptId);
436
+ if (!id) {
437
+ throw new Error("提示词 ID 不能为空");
438
+ }
439
+
440
+ const state = loadDesktopState();
441
+ const next = state.prompts.filter((item) => item.id !== id);
442
+ if (next.length === state.prompts.length) {
443
+ throw new Error(`Prompt not found: ${id}`);
444
+ }
445
+
446
+ state.prompts = next;
447
+ saveDesktopState(state);
448
+ return state;
449
+ }
450
+
451
+ export function upsertSkillFromDesktop(input) {
452
+ const state = loadDesktopState();
453
+ const id = normalizeText(input?.id);
454
+ const name = normalizeText(input?.name);
455
+ const description = normalizeText(input?.description);
456
+ const directory = normalizeText(input?.directory);
457
+
458
+ if (!name) {
459
+ throw new Error("技能名称不能为空");
460
+ }
461
+ if (!directory) {
462
+ throw new Error("技能目录不能为空");
463
+ }
464
+
465
+ const payload = {
466
+ id: id || createId("skill", name),
467
+ name,
468
+ description,
469
+ directory,
470
+ enabledPlatforms: sanitizeEnabledPlatforms(input?.enabledPlatforms, SUPPORTED_PLATFORMS)
471
+ };
472
+
473
+ const index = state.skills.findIndex((item) => item.id === payload.id);
474
+ if (index >= 0) {
475
+ state.skills[index] = payload;
476
+ } else {
477
+ state.skills.unshift(payload);
478
+ }
479
+
480
+ saveDesktopState(state);
481
+ return state;
482
+ }
483
+
484
+ export function deleteSkillFromDesktop(skillId) {
485
+ const id = normalizeText(skillId);
486
+ if (!id) {
487
+ throw new Error("技能 ID 不能为空");
488
+ }
489
+
490
+ const state = loadDesktopState();
491
+ const next = state.skills.filter((item) => item.id !== id);
492
+ if (next.length === state.skills.length) {
493
+ throw new Error(`Skill not found: ${id}`);
494
+ }
495
+
496
+ state.skills = next;
497
+ saveDesktopState(state);
498
+ return state;
499
+ }
500
+
501
+ export function toggleSkillRepoFromDesktop(repoId) {
502
+ const state = loadDesktopState();
503
+ const repo = state.skillRepos.find((item) => item.id === repoId);
504
+ if (!repo) {
505
+ throw new Error(`Skill repo not found: ${repoId}`);
506
+ }
507
+
508
+ repo.enabled = !repo.enabled;
509
+ saveDesktopState(state);
510
+ return state;
511
+ }