@dimina-kit/devtools 0.4.0-dev.20260828092243 → 0.4.0-dev.20260831134112

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 (56) hide show
  1. package/dist/main/app/app.js +4 -0
  2. package/dist/main/index.bundle.js +99 -11
  3. package/dist/main/ipc/projects.d.ts +1 -1
  4. package/dist/main/ipc/projects.js +25 -1
  5. package/dist/main/services/projects/create-project-service.js +9 -2
  6. package/dist/main/services/projects/local-provider.js +6 -0
  7. package/dist/main/services/projects/project-repository.d.ts +25 -0
  8. package/dist/main/services/projects/project-repository.js +88 -12
  9. package/dist/main/services/projects/types.d.ts +13 -2
  10. package/dist/main/services/workbench-context.d.ts +18 -4
  11. package/dist/main/services/workbench-context.js +1 -0
  12. package/dist/main/services/workspace/workspace-service.d.ts +2 -0
  13. package/dist/main/services/workspace/workspace-service.js +14 -0
  14. package/dist/preload/windows/simulator.cjs.map +1 -1
  15. package/dist/render-host/pageFrame.html +1 -1
  16. package/dist/render-host/preload.cjs +16 -4
  17. package/dist/renderer/assets/{button-DsNqZm7o.js → button-D5Q2wkXq.js} +2 -2
  18. package/dist/renderer/assets/{createLucideIcon-CYJeT1ji.js → createLucideIcon-Bqi8hpdi.js} +2 -2
  19. package/dist/renderer/assets/{dialog-pT4qWmka.js → dialog-e1TecTOY.js} +3 -3
  20. package/dist/renderer/assets/{dist-o70xmpc1.js → dist-DKWdE1TK.js} +2 -2
  21. package/dist/renderer/assets/{hostSidebarDefault-NvGZlE9O.js → hostSidebarDefault-B3yLfy8j.js} +2 -2
  22. package/dist/renderer/assets/index-CsPD68N_.js +5 -0
  23. package/dist/renderer/assets/{input-4QxTXMZN.js → input-Ct3alvkq.js} +2 -2
  24. package/dist/renderer/assets/ipc-channels-overlays-LsDzGAtV.js +2 -0
  25. package/dist/renderer/assets/jsx-runtime-2vXHouwr.css +1 -0
  26. package/dist/renderer/assets/{jsx-runtime-CqVdW3eg.js → jsx-runtime-DGLHmmOq.js} +1 -1
  27. package/dist/renderer/assets/{popover-CJiSWwuE.js → popover-Dix5cm_b.js} +2 -2
  28. package/dist/renderer/assets/project-api-k7Oq9iaq.js +2 -0
  29. package/dist/renderer/assets/{projectCreateDialog-DMWNhw_1.js → projectCreateDialog-Bbjz75bP.js} +2 -2
  30. package/dist/renderer/assets/{select-C86pNcsu.js → select-acGb0AmJ.js} +2 -2
  31. package/dist/renderer/assets/{settings-BoI8mLT1.js → settings-CQrTMr43.js} +2 -2
  32. package/dist/renderer/assets/{settings-api-Bs_g4NHs.js → settings-api-CrUQ9rYV.js} +2 -2
  33. package/dist/renderer/assets/{settings-tab-bar-DHLLygqv.js → settings-tab-bar-Cw1kVIf2.js} +2 -2
  34. package/dist/renderer/assets/{tooltip-BhAyY0T4.js → tooltip-B_-_YVbw.js} +2 -2
  35. package/dist/renderer/assets/{types-YYjkvane.js → types-CsEKtFyr.js} +2 -2
  36. package/dist/renderer/assets/{updateDialog-DPum3wx1.js → updateDialog-NsF8223A.js} +2 -2
  37. package/dist/renderer/assets/{view-api-DFhb2R8-.js → view-api-CLn3rcQE.js} +2 -2
  38. package/dist/renderer/assets/{workbenchSettings-DrajRQUk.js → workbenchSettings-CV4LVA9b.js} +2 -2
  39. package/dist/renderer/entries/host-sidebar-default/index.html +5 -5
  40. package/dist/renderer/entries/main/index.html +14 -12
  41. package/dist/renderer/entries/popover/index.html +8 -8
  42. package/dist/renderer/entries/project-create-dialog/index.html +11 -11
  43. package/dist/renderer/entries/settings/index.html +9 -9
  44. package/dist/renderer/entries/tooltip/index.html +5 -5
  45. package/dist/renderer/entries/update-dialog/index.html +9 -9
  46. package/dist/renderer/entries/workbench-settings/index.html +9 -9
  47. package/dist/shared/ipc-channels.d.ts +4 -0
  48. package/dist/shared/ipc-channels.js +4 -0
  49. package/dist/shared/ipc-schemas.d.ts +11 -0
  50. package/dist/shared/ipc-schemas.js +14 -0
  51. package/dist/shared/types.d.ts +61 -0
  52. package/package.json +8 -8
  53. package/dist/renderer/assets/index-zErhG3ta.js +0 -5
  54. package/dist/renderer/assets/ipc-channels-overlays-Blr5TNoB.js +0 -2
  55. package/dist/renderer/assets/jsx-runtime-B-3rq1y5.css +0 -1
  56. package/dist/renderer/assets/project-api-DZ1Zi6EC.js +0 -2
@@ -169,6 +169,10 @@ function createContext(config, mainWindow, rendererDir) {
169
169
  projectTemplates: config.projectTemplates,
170
170
  builtinTemplates: config.builtinTemplates,
171
171
  customCreateProjectDialog: config.customCreateProjectDialog,
172
+ // No cast needed here: the hook receives the project record, so the
173
+ // main-process `Project` the context hands it satisfies the structural
174
+ // `EditableProject` the config declares.
175
+ customEditProjectDialog: config.customEditProjectDialog,
172
176
  onBeforeOpenProject: config.onBeforeOpenProject,
173
177
  });
174
178
  }
@@ -485,10 +485,14 @@ var ProjectsChannel = {
485
485
  List: "projects:list",
486
486
  Add: "projects:add",
487
487
  Remove: "projects:remove",
488
+ /** Edit an existing record's name / icon; its path stays the identity. */
489
+ Update: "projects:update",
488
490
  /** Merged + sanitized template catalog for the create-project dialog. */
489
491
  ListTemplates: "projects:listTemplates",
490
492
  /** Host-supplied create-project dialog hook (returns input or null). */
491
493
  OpenCreateDialog: "projects:openCreateDialog",
494
+ /** Host-supplied edit-project dialog hook; `null` means no hook is configured. */
495
+ OpenEditDialog: "projects:openEditDialog",
492
496
  /** Server-side scaffold + register (delegates to create-project-service). */
493
497
  Create: "projects:create",
494
498
  /** Default values used to pre-fill the create-project dialog (baseDir). */
@@ -6391,6 +6395,7 @@ __export(project_repository_exports, {
6391
6395
  removeProject: () => removeProject,
6392
6396
  saveCompileConfig: () => saveCompileConfig,
6393
6397
  updateLastOpened: () => updateLastOpened,
6398
+ updateProject: () => updateProject,
6394
6399
  updateProjectSettings: () => updateProjectSettings,
6395
6400
  validateProjectDir: () => validateProjectDir
6396
6401
  });
@@ -6411,6 +6416,30 @@ function readProjectConfig(dirPath) {
6411
6416
  }
6412
6417
  return merged;
6413
6418
  }
6419
+ function readProjectName(dirPath) {
6420
+ const raw = readProjectConfig(dirPath).projectname;
6421
+ if (typeof raw !== "string" || !raw.trim()) return null;
6422
+ try {
6423
+ return decodeURIComponent(raw);
6424
+ } catch {
6425
+ return raw;
6426
+ }
6427
+ }
6428
+ function writeProjectName(dirPath, name) {
6429
+ const configPath = path9.join(dirPath, "project.private.config.json");
6430
+ let config = {};
6431
+ if (fs5.existsSync(configPath)) {
6432
+ try {
6433
+ config = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
6434
+ } catch {
6435
+ throw new Error(`\u65E0\u6CD5\u91CD\u547D\u540D\uFF1A${configPath} \u4E0D\u662F\u5408\u6CD5 JSON`);
6436
+ }
6437
+ }
6438
+ fs5.writeFileSync(
6439
+ configPath,
6440
+ JSON.stringify({ ...config, projectname: encodeURIComponent(name) }, null, 2)
6441
+ );
6442
+ }
6414
6443
  function detectRuntimeType(dirPath) {
6415
6444
  const compileType = readProjectConfig(dirPath).compileType;
6416
6445
  if (compileType === "game") return "minigame";
@@ -6472,16 +6501,7 @@ function hasProject(dirPath) {
6472
6501
  }
6473
6502
  function addProject(dirPath) {
6474
6503
  const projects = load();
6475
- let name = path9.basename(dirPath);
6476
- try {
6477
- const configPath = path9.join(dirPath, "project.config.json");
6478
- if (fs5.existsSync(configPath)) {
6479
- const cfg = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
6480
- if (cfg.projectname) name = cfg.projectname;
6481
- }
6482
- } catch (err2) {
6483
- log2.warn("Failed to read project name from config", err2);
6484
- }
6504
+ const name = readProjectName(dirPath) ?? path9.basename(dirPath);
6485
6505
  const type = detectRuntimeType(dirPath);
6486
6506
  const project = { name, path: dirPath, lastOpened: null, type };
6487
6507
  const idx = projects.findIndex((p) => p.path === dirPath);
@@ -6496,6 +6516,26 @@ function addProject(dirPath) {
6496
6516
  function removeProject(dirPath) {
6497
6517
  save(load().filter((p) => p.path !== dirPath));
6498
6518
  }
6519
+ function updateProject(dirPath, patch) {
6520
+ const projects = load();
6521
+ const idx = projects.findIndex((p) => p.path === dirPath);
6522
+ if (idx < 0) return null;
6523
+ const next = { ...projects[idx] };
6524
+ if (patch.name !== void 0) {
6525
+ const name = patch.name.trim();
6526
+ if (!name) throw new Error("Project name cannot be empty");
6527
+ next.name = name;
6528
+ }
6529
+ if (patch.iconUrl !== void 0) {
6530
+ const iconUrl = patch.iconUrl.trim();
6531
+ if (iconUrl) next.iconUrl = iconUrl;
6532
+ else delete next.iconUrl;
6533
+ }
6534
+ if (next.name !== projects[idx].name) writeProjectName(dirPath, next.name);
6535
+ projects[idx] = next;
6536
+ save(projects);
6537
+ return next;
6538
+ }
6499
6539
  function updateLastOpened(dirPath) {
6500
6540
  const projects = load();
6501
6541
  const idx = projects.findIndex((p) => p.path === dirPath);
@@ -6725,6 +6765,15 @@ function createWorkspaceService(ctx) {
6725
6765
  removeProject: async (dirPath) => {
6726
6766
  await provider.removeProject(dirPath);
6727
6767
  },
6768
+ updateProject: async (dirPath, patch) => {
6769
+ const projects = await provider.listProjects();
6770
+ const existing = projects.find((p) => p.path === dirPath);
6771
+ if (!existing) throw new Error(`No such project: ${dirPath}`);
6772
+ if (!provider.updateProject) {
6773
+ throw new Error("\u5F53\u524D\u5BBF\u4E3B\u4E0D\u652F\u6301\u7F16\u8F91\u9879\u76EE");
6774
+ }
6775
+ return provider.updateProject(dirPath, patch);
6776
+ },
6728
6777
  hasProject: async (dirPath) => {
6729
6778
  const projects = await provider.listProjects();
6730
6779
  return projects.some((p) => p.path === dirPath);
@@ -6895,6 +6944,11 @@ function createLocalProjectsProvider() {
6895
6944
  listProjects: () => listProjects(),
6896
6945
  addProject: (dirPath) => addProject(dirPath),
6897
6946
  removeProject: (dirPath) => removeProject(dirPath),
6947
+ updateProject: (dirPath, patch) => {
6948
+ const updated = updateProject(dirPath, patch);
6949
+ if (!updated) throw new Error(`No such project: ${dirPath}`);
6950
+ return updated;
6951
+ },
6898
6952
  validateProjectDir: (dirPath) => validateProjectDir(dirPath),
6899
6953
  updateLastOpened: (dirPath) => updateLastOpened(dirPath),
6900
6954
  getCompileConfig: (dirPath) => getCompileConfig(dirPath),
@@ -7167,6 +7221,7 @@ function createWorkbenchContext(opts) {
7167
7221
  opts.builtinTemplates ?? "all"
7168
7222
  );
7169
7223
  ctx.customCreateProjectDialog = opts.customCreateProjectDialog;
7224
+ ctx.customEditProjectDialog = opts.customEditProjectDialog;
7170
7225
  ctx.onBeforeOpenProject = opts.onBeforeOpenProject;
7171
7226
  ctx.workspace = createWorkspaceService(ctx);
7172
7227
  ctx.senderPolicy = createWorkbenchSenderPolicy(ctx);
@@ -7443,7 +7498,15 @@ var SimulatorCustomApiInvokeSchema = z3.tuple([
7443
7498
  ]);
7444
7499
  var ProjectGetPagesSchema = z3.tuple([AbsolutePath]);
7445
7500
  var ProjectGetCompileConfigSchema = z3.tuple([AbsolutePath]);
7501
+ var ProjectsUpdateSchema = z3.tuple([
7502
+ AbsolutePath,
7503
+ z3.object({
7504
+ name: z3.string().min(1).max(200).optional(),
7505
+ iconUrl: z3.string().max(2048).optional()
7506
+ })
7507
+ ]);
7446
7508
  var ProjectsRemoveSchema = z3.tuple([AbsolutePath]);
7509
+ var ProjectsOpenEditDialogSchema = z3.tuple([AbsolutePath]);
7447
7510
  var WorkbenchSettingsSaveSchema = z3.tuple([
7448
7511
  z3.looseObject({
7449
7512
  cdp: z3.looseObject({
@@ -7625,7 +7688,7 @@ function writeProjectConfig(target, name) {
7625
7688
  cfg = {};
7626
7689
  }
7627
7690
  }
7628
- cfg.projectname = name;
7691
+ cfg.projectname = encodeURIComponent(name);
7629
7692
  fs7.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
7630
7693
  }
7631
7694
  async function createProject(input, ctx) {
@@ -7680,6 +7743,13 @@ function registerProjectsIpc(ctx) {
7680
7743
  }).handle(ProjectsChannel.Remove, (_event, ...args) => {
7681
7744
  const [dirPath] = validate(ProjectsChannel.Remove, ProjectsRemoveSchema, args);
7682
7745
  return ctx.workspace.removeProject(dirPath);
7746
+ }).handle(ProjectsChannel.Update, (_event, ...args) => {
7747
+ const [dirPath, patch] = validate(
7748
+ ProjectsChannel.Update,
7749
+ ProjectsUpdateSchema,
7750
+ args
7751
+ );
7752
+ return ctx.workspace.updateProject(dirPath, patch);
7683
7753
  }).handle(ProjectsChannel.ListTemplates, () => {
7684
7754
  return sanitizeTemplates(ctx.projectTemplates ?? []);
7685
7755
  }).handle(ProjectsChannel.OpenCreateDialog, async () => {
@@ -7689,6 +7759,20 @@ function registerProjectsIpc(ctx) {
7689
7759
  parentWindow: ctx.windows.mainWindow,
7690
7760
  templates: sanitized
7691
7761
  });
7762
+ }).handle(ProjectsChannel.OpenEditDialog, async (_event, ...args) => {
7763
+ if (!ctx.customEditProjectDialog) return null;
7764
+ const [dirPath] = validate(
7765
+ ProjectsChannel.OpenEditDialog,
7766
+ ProjectsOpenEditDialogSchema,
7767
+ args
7768
+ );
7769
+ const project = (await ctx.workspace.listProjects()).find((p) => p.path === dirPath);
7770
+ if (!project) throw new Error(`No such project: ${dirPath}`);
7771
+ const result = await ctx.customEditProjectDialog({
7772
+ parentWindow: ctx.windows.mainWindow,
7773
+ project
7774
+ });
7775
+ return { result };
7692
7776
  }).handle(ProjectsChannel.Create, async (_event, ...args) => {
7693
7777
  const [raw] = args;
7694
7778
  if (!raw || typeof raw !== "object") {
@@ -11108,6 +11192,10 @@ function createContext(config, mainWindow, rendererDir2) {
11108
11192
  projectTemplates: config.projectTemplates,
11109
11193
  builtinTemplates: config.builtinTemplates,
11110
11194
  customCreateProjectDialog: config.customCreateProjectDialog,
11195
+ // No cast needed here: the hook receives the project record, so the
11196
+ // main-process `Project` the context hands it satisfies the structural
11197
+ // `EditableProject` the config declares.
11198
+ customEditProjectDialog: config.customEditProjectDialog,
11111
11199
  onBeforeOpenProject: config.onBeforeOpenProject
11112
11200
  });
11113
11201
  }
@@ -1,7 +1,7 @@
1
1
  import type { WorkbenchContext } from '../services/workbench-context.js';
2
2
  import type { WorkbenchModule } from '../services/module.js';
3
3
  import type { Disposable } from '@dimina-kit/electron-deck/main';
4
- type ProjectsIpcCtx = Pick<WorkbenchContext, 'workspace' | 'windows' | 'senderPolicy' | 'projectsProvider' | 'projectTemplates' | 'customCreateProjectDialog' | 'notify' | 'views'>;
4
+ type ProjectsIpcCtx = Pick<WorkbenchContext, 'workspace' | 'windows' | 'senderPolicy' | 'projectsProvider' | 'projectTemplates' | 'customCreateProjectDialog' | 'customEditProjectDialog' | 'notify' | 'views'>;
5
5
  export declare function registerProjectsIpc(ctx: ProjectsIpcCtx): Disposable;
6
6
  export declare const projectsModule: WorkbenchModule;
7
7
  export {};
@@ -1,7 +1,7 @@
1
1
  import { app, dialog } from 'electron';
2
2
  import path from 'path';
3
3
  import { ProjectsChannel, DialogChannel } from '../../shared/ipc-channels.js';
4
- import { ProjectsAddSchema, ProjectsRemoveSchema, } from '../../shared/ipc-schemas.js';
4
+ import { ProjectsAddSchema, ProjectsOpenEditDialogSchema, ProjectsRemoveSchema, ProjectsUpdateSchema, } from '../../shared/ipc-schemas.js';
5
5
  import { validate } from '../utils/ipc-schema.js';
6
6
  import { IpcRegistry } from '../utils/ipc-registry.js';
7
7
  import { sanitizeTemplates } from '../services/projects/templates.js';
@@ -59,6 +59,10 @@ export function registerProjectsIpc(ctx) {
59
59
  .handle(ProjectsChannel.Remove, (_event, ...args) => {
60
60
  const [dirPath] = validate(ProjectsChannel.Remove, ProjectsRemoveSchema, args);
61
61
  return ctx.workspace.removeProject(dirPath);
62
+ })
63
+ .handle(ProjectsChannel.Update, (_event, ...args) => {
64
+ const [dirPath, patch] = validate(ProjectsChannel.Update, ProjectsUpdateSchema, args);
65
+ return ctx.workspace.updateProject(dirPath, patch);
62
66
  })
63
67
  // ── template catalog + create flow ──
64
68
  .handle(ProjectsChannel.ListTemplates, () => {
@@ -75,6 +79,26 @@ export function registerProjectsIpc(ctx) {
75
79
  parentWindow: ctx.windows.mainWindow,
76
80
  templates: sanitized,
77
81
  });
82
+ })
83
+ .handle(ProjectsChannel.OpenEditDialog, async (_event, ...args) => {
84
+ if (!ctx.customEditProjectDialog)
85
+ return null;
86
+ const [dirPath] = validate(ProjectsChannel.OpenEditDialog, ProjectsOpenEditDialogSchema, args);
87
+ // Read from the workspace, not the renderer's args — the host sees
88
+ // devtools' own authoritative record, never a client-supplied one.
89
+ const project = (await ctx.workspace.listProjects()).find((p) => p.path === dirPath);
90
+ if (!project)
91
+ throw new Error(`No such project: ${dirPath}`);
92
+ const result = await ctx.customEditProjectDialog({
93
+ parentWindow: ctx.windows.mainWindow,
94
+ project,
95
+ });
96
+ // Wrapped in `{ result }` so `null` on the wire means ONLY "no hook
97
+ // configured" (renderer falls back to the built-in dialog). A hook that
98
+ // ran and was cancelled resolves `result: null`, which the renderer
99
+ // must treat as "do nothing" — otherwise a cancelled host dialog would
100
+ // fall through into the built-in one right behind it.
101
+ return { result };
78
102
  })
79
103
  .handle(ProjectsChannel.Create, async (_event, ...args) => {
80
104
  // We deliberately don't run this through zod yet — the input shape is
@@ -8,7 +8,8 @@
8
8
  * 3. copies the template `source` directory into `input.path`, or invokes
9
9
  * `template.generate(input.path, { name })` for programmatic templates.
10
10
  * 4. rewrites `<input.path>/project.config.json`'s `projectname` to
11
- * `input.name`.
11
+ * `encodeURIComponent(input.name)` — same encoding project-repository's
12
+ * `readProjectName`/`writeProjectName` use for the edit path.
12
13
  * 5. registers the project with `ctx.projectsProvider.addProject(input.path)`
13
14
  * and returns the resulting Project.
14
15
  */
@@ -81,6 +82,12 @@ async function materializeTemplate(target, template, name) {
81
82
  * Rewrite `<target>/project.config.json`'s `projectname` (best-effort: create
82
83
  * the file if the template didn't ship one — gives the user a working
83
84
  * starting point even for tiny generators).
85
+ *
86
+ * Encodes with `encodeURIComponent`, the same convention project-repository's
87
+ * `readProjectName`/`writeProjectName` use (aligned with WeChat DevTools:
88
+ * non-ASCII names go through URL encoding). Writing the raw name here would
89
+ * make this the one path that disagrees with the rename path, so a name with
90
+ * `%` or non-ASCII characters would come back different after an edit.
84
91
  */
85
92
  function writeProjectConfig(target, name) {
86
93
  const cfgPath = path.join(target, 'project.config.json');
@@ -93,7 +100,7 @@ function writeProjectConfig(target, name) {
93
100
  cfg = {};
94
101
  }
95
102
  }
96
- cfg.projectname = name;
103
+ cfg.projectname = encodeURIComponent(name);
97
104
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
98
105
  }
99
106
  export async function createProject(input, ctx) {
@@ -14,6 +14,12 @@ export function createLocalProjectsProvider() {
14
14
  listProjects: () => repo.listProjects(),
15
15
  addProject: (dirPath) => repo.addProject(dirPath),
16
16
  removeProject: (dirPath) => repo.removeProject(dirPath),
17
+ updateProject: (dirPath, patch) => {
18
+ const updated = repo.updateProject(dirPath, patch);
19
+ if (!updated)
20
+ throw new Error(`No such project: ${dirPath}`);
21
+ return updated;
22
+ },
17
23
  validateProjectDir: (dirPath) => repo.validateProjectDir(dirPath),
18
24
  updateLastOpened: (dirPath) => repo.updateLastOpened(dirPath),
19
25
  getCompileConfig: (dirPath) => repo.getCompileConfig(dirPath),
@@ -7,6 +7,22 @@ export interface Project {
7
7
  compileConfig?: CompileConfig;
8
8
  /** Absent on projects added before mini-game support — treat as 'miniprogram'. */
9
9
  type?: ProjectType;
10
+ /**
11
+ * User-supplied icon for the project card. Absent means the card falls back
12
+ * to the first character of `name`.
13
+ */
14
+ iconUrl?: string;
15
+ }
16
+ /**
17
+ * The subset of a project record the user may edit after import. `path` is the
18
+ * record's identity (it keys every other per-project store — compile config,
19
+ * thumbnail, watcher) and is deliberately not patchable: pointing an existing
20
+ * record at another directory is an import, not an edit.
21
+ */
22
+ export interface ProjectPatch {
23
+ name?: string;
24
+ /** Empty string clears the icon and restores the name-initial fallback. */
25
+ iconUrl?: string;
10
26
  }
11
27
  export interface ProjectPages {
12
28
  pages: string[];
@@ -20,6 +36,15 @@ export declare function validateProjectDir(dirPath: string): string | null;
20
36
  export declare function hasProject(dirPath: string): boolean;
21
37
  export declare function addProject(dirPath: string): Project;
22
38
  export declare function removeProject(dirPath: string): void;
39
+ /**
40
+ * Apply a user edit to the record at `dirPath`. Returns the updated record, or
41
+ * `null` when no record matches — callers decide whether a missing project is
42
+ * an error (the IPC path) or a no-op.
43
+ *
44
+ * A name change is written through to the project's own config (see
45
+ * `writeProjectName`); the icon lives only in our list.
46
+ */
47
+ export declare function updateProject(dirPath: string, patch: ProjectPatch): Project | null;
23
48
  export declare function updateLastOpened(dirPath: string): void;
24
49
  export declare function getCompileConfig(dirPath: string): CompileConfig;
25
50
  export declare function getProjectPages(dirPath: string): ProjectPages;
@@ -23,6 +23,54 @@ function readProjectConfig(dirPath) {
23
23
  }
24
24
  return merged;
25
25
  }
26
+ /**
27
+ * The project's display name as the project itself carries it.
28
+ *
29
+ * `projectname` lives in the merged config with the private file winning, and
30
+ * is URL-encoded when it leaves ASCII — WeChat DevTools 36.6.0 writes
31
+ * `"projectname": "%E6%BD%AE%E7%8E%A9%E6%97%8F"` and its own project list only
32
+ * mirrors that value. ASCII names encode to themselves, so plain values that
33
+ * were written by hand round-trip unchanged.
34
+ *
35
+ * Returns null when the config carries no usable name, leaving the caller to
36
+ * fall back to the directory name.
37
+ */
38
+ function readProjectName(dirPath) {
39
+ const raw = readProjectConfig(dirPath).projectname;
40
+ if (typeof raw !== 'string' || !raw.trim())
41
+ return null;
42
+ try {
43
+ return decodeURIComponent(raw);
44
+ }
45
+ catch {
46
+ // A literal '%' that isn't a valid escape — keep the author's text.
47
+ return raw;
48
+ }
49
+ }
50
+ /**
51
+ * Persist the display name into `project.private.config.json`, the per-
52
+ * developer half of the project config (WeChat DevTools writes the name there
53
+ * too, leaving the shared `project.config.json` alone).
54
+ *
55
+ * This is what makes the config — not our list file — the single owner of the
56
+ * name: `addProject` re-reads it, so re-importing a directory restores the
57
+ * user's rename instead of resetting it.
58
+ */
59
+ function writeProjectName(dirPath, name) {
60
+ const configPath = path.join(dirPath, 'project.private.config.json');
61
+ let config = {};
62
+ if (fs.existsSync(configPath)) {
63
+ try {
64
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
65
+ }
66
+ catch {
67
+ // Refuse rather than overwrite: this file also holds the user's IDE
68
+ // settings and compile conditions, which a blind rewrite would drop.
69
+ throw new Error(`无法重命名:${configPath} 不是合法 JSON`);
70
+ }
71
+ }
72
+ fs.writeFileSync(configPath, JSON.stringify({ ...config, projectname: encodeURIComponent(name) }, null, 2));
73
+ }
26
74
  /**
27
75
  * Mirrors `dimina/fe/packages/compiler/src/env.js`'s `detectRuntimeType` —
28
76
  * an explicit `compileType` wins, else a mini-game is inferred from
@@ -95,18 +143,9 @@ export function hasProject(dirPath) {
95
143
  }
96
144
  export function addProject(dirPath) {
97
145
  const projects = load();
98
- let name = path.basename(dirPath);
99
- try {
100
- const configPath = path.join(dirPath, 'project.config.json');
101
- if (fs.existsSync(configPath)) {
102
- const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
103
- if (cfg.projectname)
104
- name = cfg.projectname;
105
- }
106
- }
107
- catch (err) {
108
- log.warn('Failed to read project name from config', err);
109
- }
146
+ // The config is the name's owner, so re-adding a directory picks up a rename
147
+ // the user made here rather than reverting it.
148
+ const name = readProjectName(dirPath) ?? path.basename(dirPath);
110
149
  const type = detectRuntimeType(dirPath);
111
150
  const project = { name, path: dirPath, lastOpened: null, type };
112
151
  const idx = projects.findIndex((p) => p.path === dirPath);
@@ -122,6 +161,43 @@ export function addProject(dirPath) {
122
161
  export function removeProject(dirPath) {
123
162
  save(load().filter((p) => p.path !== dirPath));
124
163
  }
164
+ /**
165
+ * Apply a user edit to the record at `dirPath`. Returns the updated record, or
166
+ * `null` when no record matches — callers decide whether a missing project is
167
+ * an error (the IPC path) or a no-op.
168
+ *
169
+ * A name change is written through to the project's own config (see
170
+ * `writeProjectName`); the icon lives only in our list.
171
+ */
172
+ export function updateProject(dirPath, patch) {
173
+ const projects = load();
174
+ const idx = projects.findIndex((p) => p.path === dirPath);
175
+ if (idx < 0)
176
+ return null;
177
+ const next = { ...projects[idx] };
178
+ if (patch.name !== undefined) {
179
+ const name = patch.name.trim();
180
+ if (!name)
181
+ throw new Error('Project name cannot be empty');
182
+ next.name = name;
183
+ }
184
+ if (patch.iconUrl !== undefined) {
185
+ const iconUrl = patch.iconUrl.trim();
186
+ if (iconUrl)
187
+ next.iconUrl = iconUrl;
188
+ else
189
+ delete next.iconUrl;
190
+ }
191
+ // The config write comes first and is allowed to throw: a rename that only
192
+ // reached our list would look applied until the next re-import silently
193
+ // reverted it. The icon has no counterpart in the project config — it is
194
+ // ours alone — so it never touches the project directory.
195
+ if (next.name !== projects[idx].name)
196
+ writeProjectName(dirPath, next.name);
197
+ projects[idx] = next;
198
+ save(projects);
199
+ return next;
200
+ }
125
201
  export function updateLastOpened(dirPath) {
126
202
  const projects = load();
127
203
  const idx = projects.findIndex((p) => p.path === dirPath);
@@ -5,8 +5,8 @@
5
5
  * template catalog, and the "新建项目" dialog.
6
6
  */
7
7
  import type { CompileConfig } from '../../../shared/types.js';
8
- import type { Project } from './project-repository.js';
9
- export type { Project };
8
+ import type { Project, ProjectPatch } from './project-repository.js';
9
+ export type { Project, ProjectPatch };
10
10
  export type { ProjectTemplate, CreateProjectInput } from '../../../shared/types.js';
11
11
  /**
12
12
  * Default compile config returned by WorkspaceService when the injected
@@ -40,6 +40,17 @@ export interface ProjectsProvider {
40
40
  validateProjectDir?(dirPath: string): string | null | Promise<string | null>;
41
41
  addProject(dirPath: string): Project | Promise<Project>;
42
42
  removeProject(dirPath: string): void | Promise<void>;
43
+ /**
44
+ * Apply a user edit (name / icon) to an existing record. `dirPath` is the
45
+ * record's identity, so it is never part of the patch — see `ProjectPatch`.
46
+ *
47
+ * Default when omitted: throws, and the renderer shows that error in the
48
+ * edit dialog. A silent no-op would report success (dialog closes, list
49
+ * reloads) while discarding the user's input, since the caller has no way
50
+ * to tell "saved" apart from "provider doesn't support this". Implement
51
+ * this if your UI exposes the project-edit dialog.
52
+ */
53
+ updateProject?(dirPath: string, patch: ProjectPatch): Project | Promise<Project>;
43
54
  /**
44
55
  * Record that the user just opened a project (drives "recent" ordering).
45
56
  *
@@ -21,8 +21,8 @@ import { type WorkspaceService } from './workspace/workspace-service.js';
21
21
  import { type SimulatorApiRegistry } from './simulator/custom-apis.js';
22
22
  import { type SimulatorUiExtensionRegistry } from './simulator/ui-extensions.js';
23
23
  import { sanitizeTemplates } from './projects/templates.js';
24
- import type { ProjectsProvider, ProjectTemplate } from './projects/types.js';
25
- import type { CustomCreateProjectDialogResult } from '../../shared/types.js';
24
+ import type { Project, ProjectsProvider, ProjectTemplate } from './projects/types.js';
25
+ import type { CustomCreateProjectDialogResult, CustomEditProjectDialogResult } from '../../shared/types.js';
26
26
  /**
27
27
  * Shared mutable state for the workbench application.
28
28
  * Passed to each IPC module so they can read/write shared state without closures.
@@ -96,6 +96,17 @@ export interface WorkbenchContext extends RuntimeContext {
96
96
  parentWindow: BrowserWindow;
97
97
  templates: ProjectTemplate[];
98
98
  }) => Promise<CustomCreateProjectDialogResult>;
99
+ /**
100
+ * Optional host hook for the "编辑项目" dialog. When present, the
101
+ * `projects:openEditDialog` IPC runs it and wraps whatever it resolves to
102
+ * as `{ result }`; when absent, the IPC returns `null` and the renderer
103
+ * shows the built-in dialog. `project` comes from `workspace.listProjects`,
104
+ * not the renderer's IPC args.
105
+ */
106
+ customEditProjectDialog?: (ctx: {
107
+ parentWindow: BrowserWindow;
108
+ project: Project;
109
+ }) => Promise<CustomEditProjectDialogResult>;
99
110
  /**
100
111
  * Host permission gate consulted by `workspace.openProject` BEFORE any side
101
112
  * effect. Throwing vetoes the open (see `WorkbenchAppConfig.onBeforeOpenProject`).
@@ -239,8 +250,9 @@ export interface WorkbenchContext extends RuntimeContext {
239
250
  * because their shapes intentionally differ from the config:
240
251
  * - `preloadPath` / `rendererDir` are REQUIRED here (the caller resolves the
241
252
  * defaults before constructing the context) but optional in the config;
242
- * - `projectsProvider` / `projectTemplates` / `customCreateProjectDialog` use
243
- * the main-process types, not the structural mirrors in `shared/types`.
253
+ * - `projectsProvider` / `projectTemplates` / `customCreateProjectDialog` /
254
+ * `customEditProjectDialog` use the main-process types, not the structural
255
+ * mirrors in `shared/types`.
244
256
  */
245
257
  export interface CreateContextOptions extends Pick<WorkbenchConfig, 'adapter' | 'apiNamespaces' | 'appName' | 'fileTypes'> {
246
258
  mainWindow: BrowserWindow;
@@ -255,6 +267,8 @@ export interface CreateContextOptions extends Pick<WorkbenchConfig, 'adapter' |
255
267
  builtinTemplates?: 'all' | 'none' | readonly string[];
256
268
  /** Host-supplied "新建项目" dialog hook. */
257
269
  customCreateProjectDialog?: WorkbenchContext['customCreateProjectDialog'];
270
+ /** Host-supplied "编辑项目" dialog hook. */
271
+ customEditProjectDialog?: WorkbenchContext['customEditProjectDialog'];
258
272
  /** Host permission gate run before a project opens (throw to veto). */
259
273
  onBeforeOpenProject?: WorkbenchContext['onBeforeOpenProject'];
260
274
  }
@@ -74,6 +74,7 @@ export function createWorkbenchContext(opts) {
74
74
  ctx.projectsProvider = opts.projectsProvider ?? createLocalProjectsProvider();
75
75
  ctx.projectTemplates = resolveTemplates(BUILTIN_TEMPLATES, opts.projectTemplates ?? [], opts.builtinTemplates ?? 'all');
76
76
  ctx.customCreateProjectDialog = opts.customCreateProjectDialog;
77
+ ctx.customEditProjectDialog = opts.customEditProjectDialog;
77
78
  ctx.onBeforeOpenProject = opts.onBeforeOpenProject;
78
79
  ctx.workspace = createWorkspaceService(ctx);
79
80
  ctx.senderPolicy = createWorkbenchSenderPolicy(ctx);
@@ -1,6 +1,7 @@
1
1
  import type { AppInfo, CompileConfig, ProjectSession } from '../../../shared/types.js';
2
2
  import type { WorkbenchContext } from '../workbench-context.js';
3
3
  import type { Project, ProjectPages, ProjectSettings } from '../projects/project-repository.js';
4
+ import type { ProjectPatch } from '../projects/types.js';
4
5
  /**
5
6
  * Result returned to the renderer after `project:open` finishes.
6
7
  * Mirrors the previous inline shape in ipc/session.ts so no
@@ -28,6 +29,7 @@ export interface WorkspaceService {
28
29
  listProjects(): Promise<Project[]>;
29
30
  addProject(dirPath: string): Promise<Project>;
30
31
  removeProject(dirPath: string): Promise<void>;
32
+ updateProject(dirPath: string, patch: ProjectPatch): Promise<Project>;
31
33
  hasProject(dirPath: string): Promise<boolean>;
32
34
  validateProjectDir(dirPath: string): Promise<string | null>;
33
35
  openProject(projectPath: string): Promise<OpenProjectResult>;
@@ -158,6 +158,20 @@ export function createWorkspaceService(ctx) {
158
158
  removeProject: async (dirPath) => {
159
159
  await provider.removeProject(dirPath);
160
160
  },
161
+ updateProject: async (dirPath, patch) => {
162
+ const projects = await provider.listProjects();
163
+ const existing = projects.find((p) => p.path === dirPath);
164
+ if (!existing)
165
+ throw new Error(`No such project: ${dirPath}`);
166
+ // A provider without `updateProject` cannot persist the edit. Returning
167
+ // the record unchanged would look like a successful save to the
168
+ // renderer (dialog closes, list reloads) while silently discarding the
169
+ // user's input, so this must fail loudly instead.
170
+ if (!provider.updateProject) {
171
+ throw new Error('当前宿主不支持编辑项目');
172
+ }
173
+ return provider.updateProject(dirPath, patch);
174
+ },
161
175
  hasProject: async (dirPath) => {
162
176
  const projects = await provider.listProjects();
163
177
  return projects.some((p) => p.path === dirPath);