@yanglaofish/fishtools 0.5.1 → 0.5.2

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 (66) hide show
  1. package/README.md +4 -2
  2. package/manual/README.md +4 -2
  3. package/manual/docs/deployment.md +2 -2
  4. package/manual/docs/modules.md +2 -0
  5. package/manual/docs/plugin-development.md +3 -1
  6. package/manual/docs/releases.md +32 -6
  7. package/manual/docs/superpowers/plans/2026-10-13-command-shortcuts.md +107 -0
  8. package/manual/docs/superpowers/reviews/2026-10-13-0.5.0-followup-review.md +4 -1
  9. package/manual/docs/superpowers/reviews/2026-10-13-command-shortcuts-review.md +63 -0
  10. package/manual/docs/superpowers/specs/2026-10-12-workflow-variation-design.md +1 -1
  11. package/manual/docs/superpowers/specs/2026-10-13-command-shortcuts-design.md +56 -0
  12. package/manual/docs/superpowers/specs/2026-10-13-issue-4-workflow-menu.md +28 -0
  13. package/manual/docs/terminal-commands.md +64 -0
  14. package/manual/docs/verification.md +26 -1
  15. package/manual/packages/core/README.md +3 -1
  16. package/manual/packages/core/package.json +1 -0
  17. package/manual/packages/core/src/application.js +114 -17
  18. package/manual/packages/core/src/export-snapshot.js +20 -1
  19. package/manual/packages/core/src/version.js +1 -1
  20. package/manual/packages/fst-cli/README.md +2 -0
  21. package/manual/packages/fst-cli/src/terminal/command-menu.js +186 -0
  22. package/manual/packages/fst-cli/src/terminal/navigation.js +4 -3
  23. package/manual/packages/fst-cli/src/terminal/session.js +35 -4
  24. package/manual/packages/fst-cli/src/terminal/view.js +43 -8
  25. package/manual/packages/fst-cli/src/terminal/workflow-menu.js +1 -6
  26. package/manual/packages/fst-devtools/src/documentation.js +2 -1
  27. package/manual/packages/fst-platform/README.md +11 -0
  28. package/manual/packages/fst-platform/package.json +1 -0
  29. package/manual/packages/fst-platform/src/process.js +11 -7
  30. package/manual/packages/fst-platform/src/shell.js +58 -0
  31. package/manual/packages/fst-plugin/src/contracts.js +6 -2
  32. package/manual/packages/fst-workspace/README.md +3 -0
  33. package/manual/packages/fst-workspace/package.json +1 -0
  34. package/manual/packages/fst-workspace/src/shortcuts.js +119 -0
  35. package/manual/packages/pack/index.js +1 -1
  36. package/manual/plugins/commands/LICENSE +21 -0
  37. package/manual/plugins/commands/README.md +41 -0
  38. package/manual/plugins/commands/index.js +94 -0
  39. package/manual/plugins/commands/package.json +17 -0
  40. package/node_modules/@mytools/commands/LICENSE +21 -0
  41. package/node_modules/@mytools/commands/README.md +41 -0
  42. package/node_modules/@mytools/commands/index.js +94 -0
  43. package/node_modules/@mytools/commands/package.json +17 -0
  44. package/node_modules/@mytools/core/README.md +3 -1
  45. package/node_modules/@mytools/core/package.json +1 -0
  46. package/node_modules/@mytools/core/src/application.js +114 -17
  47. package/node_modules/@mytools/core/src/export-snapshot.js +20 -1
  48. package/node_modules/@mytools/core/src/version.js +1 -1
  49. package/node_modules/@mytools/fst-cli/README.md +2 -0
  50. package/node_modules/@mytools/fst-cli/src/terminal/command-menu.js +186 -0
  51. package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +4 -3
  52. package/node_modules/@mytools/fst-cli/src/terminal/session.js +35 -4
  53. package/node_modules/@mytools/fst-cli/src/terminal/view.js +43 -8
  54. package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +1 -6
  55. package/node_modules/@mytools/fst-devtools/src/documentation.js +2 -1
  56. package/node_modules/@mytools/fst-platform/README.md +11 -0
  57. package/node_modules/@mytools/fst-platform/package.json +1 -0
  58. package/node_modules/@mytools/fst-platform/src/process.js +11 -7
  59. package/node_modules/@mytools/fst-platform/src/shell.js +58 -0
  60. package/node_modules/@mytools/fst-plugin/src/contracts.js +6 -2
  61. package/node_modules/@mytools/fst-workspace/README.md +3 -0
  62. package/node_modules/@mytools/fst-workspace/package.json +1 -0
  63. package/node_modules/@mytools/fst-workspace/src/shortcuts.js +119 -0
  64. package/node_modules/@mytools/pack/index.js +1 -1
  65. package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1447 -1423
  66. package/package.json +3 -1
@@ -17,12 +17,15 @@
17
17
  | `@mytools/fst-workspace/plugins` | `./src/plugins.js` |
18
18
  | `@mytools/fst-workspace/installations` | `./src/plugin-installations.js` |
19
19
  | `@mytools/fst-workspace/bundles` | `./src/bundle-workspace.js` |
20
+ | `@mytools/fst-workspace/shortcuts` | `./src/shortcuts.js` |
20
21
  | `@mytools/fst-workspace/package.json` | `./package.json` |
21
22
 
22
23
  仅依赖 exports 声明的入口,禁止消费者跨包读取 src/bin 私有文件。入口路径由本模块维护,修改协议时同步消费方回归。
23
24
 
24
25
  ## 维护与验证
25
26
 
27
+ `/shortcuts` 提供 `validateShortcuts` 校验、`bindShortcutCommands` 在用户配置时补齐命令提供者身份,以及 `describeShortcuts` 生成可用或失效状态。绑定函数可传入 previous 保留旧项:未绑定命令修改参数或重命名仍保持失效,需重新选择目标。命令项使用 command 与 plugin 同时匹配,动作项使用稳定 action ID;不执行其他插件的同名能力。实际保存、备份与宿主装配由 core 协调。
28
+
26
29
  本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
27
30
 
28
31
  在仓库根目录执行:
@@ -8,6 +8,7 @@
8
8
  ".": "./src/index.js",
9
9
  "./workflows": "./src/workflows.js",
10
10
  "./project": "./src/project.js",
11
+ "./shortcuts": "./src/shortcuts.js",
11
12
  "./plugins": "./src/plugins.js",
12
13
  "./installations": "./src/plugin-installations.js",
13
14
  "./bundles": "./src/bundle-workspace.js",
@@ -0,0 +1,119 @@
1
+ import {FlowError} from '@mytools/sdk';
2
+ import {builtinCommands,isCommandName} from '@mytools/fst-plugin/contracts';
3
+ import {isBuiltin} from 'node:module';
4
+
5
+ const dangerousFields=new Set(['__proto__','constructor','prototype']);
6
+ const shortcutFields=new Set(['action','command','plugin','input']);
7
+
8
+ function validPluginId(value){
9
+ if(typeof value!=='string'||!value.length||value.length>214||value!==value.toLowerCase()||isBuiltin(value)||['node_modules','favicon.ico'].includes(value))return false;
10
+ const parts=/^(?:@([^/]+)\/)?([^/]+)$/.exec(value);
11
+ if(!parts||/^[._-]/.test(value)||parts[2].startsWith('.'))return false;
12
+ return (!parts[1]||/^[a-z0-9_.~'!()*-]+$/.test(parts[1]))&&/^[a-z0-9_.-]+$/.test(parts[2]);
13
+ }
14
+
15
+ function invalid(location,message){
16
+ throw new FlowError(message,{code:'VALIDATION',details:{path:location}});
17
+ }
18
+
19
+ function plainObject(value){
20
+ return value!==null&&typeof value==='object'&&!Array.isArray(value)&&[Object.prototype,null].includes(Object.getPrototypeOf(value));
21
+ }
22
+
23
+ function entries(value,location){
24
+ if(!plainObject(value))invalid(location,`${location} 必须为普通 JSON 对象`);
25
+ return Reflect.ownKeys(value).map(key=>{
26
+ if(typeof key!=='string'||dangerousFields.has(key))invalid(location,`${location} 包含危险或非 JSON 字段`);
27
+ const field=Object.getOwnPropertyDescriptor(value,key);
28
+ if(!field.enumerable||!Object.hasOwn(field,'value'))invalid(`${location}.${key}`,`${location}.${key} 必须为普通 JSON 字段`);
29
+ return [key,field.value];
30
+ });
31
+ }
32
+
33
+ function cloneJson(value,location,ancestors=new Set()){
34
+ if(value===null||typeof value==='string'||typeof value==='boolean')return value;
35
+ if(typeof value==='number'&&Number.isFinite(value))return value;
36
+ if(typeof value!=='object')invalid(location,`${location} 必须为可序列化的 JSON 值`);
37
+ if(ancestors.has(value))invalid(location,`${location} 不能包含循环引用`);
38
+ ancestors.add(value);
39
+ try{
40
+ if(Array.isArray(value)){
41
+ if(Object.getPrototypeOf(value)!==Array.prototype||Reflect.ownKeys(value).length!==value.length+1)invalid(location,`${location} 必须为普通且完整的 JSON 数组`);
42
+ const copy=[];
43
+ for(let index=0;index<value.length;index++){
44
+ const field=Object.getOwnPropertyDescriptor(value,String(index));
45
+ if(!field?.enumerable||!Object.hasOwn(field,'value'))invalid(`${location}[${index}]`,`${location} 必须为普通且完整的 JSON 数组`);
46
+ copy.push(cloneJson(field.value,`${location}[${index}]`,ancestors));
47
+ }
48
+ return copy;
49
+ }
50
+ return Object.fromEntries(entries(value,location).map(([key,nested])=>[key,cloneJson(nested,`${location}.${key}`,ancestors)]));
51
+ }finally{ancestors.delete(value);}
52
+ }
53
+
54
+ export function validateShortcuts(shortcuts,{reservedNames=[]}={}){
55
+ if(!Array.isArray(reservedNames)||reservedNames.some(name=>typeof name!=='string'))invalid('reservedNames','保留名称必须为字符串数组');
56
+ const reserved=new Set([...builtinCommands.map(command=>command.name),...reservedNames].map(name=>name.normalize('NFC')));
57
+ const copy={};
58
+ for(const [name,shortcut] of entries(shortcuts,'shortcuts')){
59
+ const location=`shortcuts.${name}`;
60
+ // 明确拒绝未规范化名称,避免两个视觉相同的入口指向不同能力。
61
+ if(name!==name.normalize('NFC'))invalid(location,`快捷名称必须使用 NFC 规范形式:${name}`);
62
+ if(!isCommandName(name))invalid(location,`快捷名称无效:/${name}`);
63
+ if(reserved.has(name))invalid(location,`快捷名称与现有命令冲突:/${name}`);
64
+ reserved.add(name);
65
+ const fields=entries(shortcut,location);
66
+ if(fields.some(([key])=>!shortcutFields.has(key)))invalid(location,`快捷项 ${name} 包含未声明字段`);
67
+ const hasAction=Object.hasOwn(shortcut,'action'),hasCommand=Object.hasOwn(shortcut,'command');
68
+ if(hasAction===hasCommand)invalid(location,`快捷项 ${name} 必须声明 action 或 command 之一`);
69
+ const field=hasAction?'action':'command',target=shortcut[field];
70
+ if(hasAction){
71
+ const separator=typeof target==='string'?target.lastIndexOf(':'):-1;
72
+ if(separator<=0||separator===target.length-1||/[\s\p{C}]/u.test(target))invalid(`${location}.action`,`快捷项 ${name} 的 action 必须为稳定动作 ID`);
73
+ }else if(!isCommandName(target))invalid(`${location}.command`,`快捷项 ${name} 的 command 名称无效`);
74
+ copy[name]={[field]:target};
75
+ if(Object.hasOwn(shortcut,'plugin')){
76
+ if(hasAction)invalid(`${location}.plugin`,`动作快捷项 ${name} 不能声明 plugin`);
77
+ if(!validPluginId(shortcut.plugin))invalid(`${location}.plugin`,`快捷项 ${name} 的 plugin 必须为合法插件包 ID`);
78
+ copy[name].plugin=shortcut.plugin;
79
+ }
80
+ if(Object.hasOwn(shortcut,'input')){
81
+ if(!plainObject(shortcut.input))invalid(`${location}.input`,`快捷项 ${name} 的 input 必须为普通 JSON 对象`);
82
+ copy[name].input=cloneJson(shortcut.input,`${location}.input`);
83
+ }
84
+ }
85
+ return copy;
86
+ }
87
+
88
+ export function bindShortcutCommands(shortcuts,{commands=[],previous={}}={}){
89
+ const copy=validateShortcuts(shortcuts);
90
+ const old=validateShortcuts(previous),unbound=new Set(Object.values(old).filter(shortcut=>shortcut.command&&shortcut.plugin===undefined).map(shortcut=>shortcut.command));
91
+ for(const [name,shortcut] of Object.entries(copy)){
92
+ // 已有未绑定项即使改参数或重命名也需明确选择目标,不能被整组保存顺带启用。
93
+ if(!Object.hasOwn(shortcut,'command')||Object.hasOwn(shortcut,'plugin'))continue;
94
+ if(old[name]?.command===shortcut.command||unbound.has(shortcut.command))continue;
95
+ const command=commands.find(command=>command.name===shortcut.command);
96
+ if(command?.plugin!==undefined)shortcut.plugin=command.plugin;
97
+ }
98
+ return validateShortcuts(copy);
99
+ }
100
+
101
+ export function describeShortcuts(shortcuts,{actions=[],commands=[]}={}){
102
+ const actionIndex=new Map(actions.map(action=>[action.id,action]));
103
+ return Object.entries(validateShortcuts(shortcuts)).map(([name,shortcut])=>{
104
+ const field=Object.hasOwn(shortcut,'action')?'action':'command';
105
+ const id=shortcut[field];
106
+ let definition;
107
+ if(field==='action')definition=actionIndex.get(id);
108
+ else if(shortcut.plugin!==undefined)definition=commands.find(command=>command.name===id&&command.plugin===shortcut.plugin);
109
+ const descriptions=field==='action'?[definition?.title,definition?.description]:[definition?.description];
110
+ const target={[field]:id};
111
+ if(shortcut.plugin!==undefined)target.plugin=shortcut.plugin;
112
+ const entry={name,target,input:shortcut.input??{},available:Boolean(definition),description:descriptions.find(value=>typeof value==='string'&&value.trim())??id};
113
+ if(!definition){
114
+ if(field==='command'&&shortcut.plugin===undefined)entry.reason='尚未绑定目标插件,请重新选择目标';
115
+ else entry.reason=`${field==='action'?'动作':'命令'}不可用或尚未安装:${shortcut.plugin?`${shortcut.plugin} → `:''}${id}`;
116
+ }
117
+ return entry;
118
+ });
119
+ }
@@ -78,7 +78,7 @@ export async function packApplication(app,{entries,target,outDir='dist',includeW
78
78
  for(const w of closure.workflows)await writeFile(path.join(workflowDir,`${w.id}.yaml`),YAML.stringify(w));
79
79
  const config={};
80
80
  for(const p of plugins)if(p.defaultConfig)config[p.id]=p.defaultConfig;
81
- await writeFile(path.join(directory,'flow.project.json'),JSON.stringify({readOnly,plugins:plugins.map(p=>`./app/node_modules/${p.id}`),config,entries,environments:overrides,network:snapshot.project.network,logs:snapshot.project.logs},null,2));
81
+ await writeFile(path.join(directory,'flow.project.json'),JSON.stringify({readOnly,plugins:plugins.map(p=>`./app/node_modules/${p.id}`),config,entries,environments:overrides,network:snapshot.project.network,logs:snapshot.project.logs,commandPresets:snapshot.project.commandPresets,shortcuts:snapshot.project.shortcuts},null,2));
82
82
  const bundleId=randomUUID();
83
83
  await writeFile(path.join(directory,'bundle.json'),JSON.stringify({format:2,id:bundleId,editable:!readOnly}));
84
84
  await writeFile(path.join(directory,'package.json'),JSON.stringify({private:true,type:'module'},null,2));