setup-command-system
Install the Command System architecture in the project
- License 许可
- MIT
- 最近更新
- 2026-10-28
- Stars
- ★ 1
- Forks
- 0
01 前言 Front Matter
- name
- setup-command-system
- description
- Install the Command System architecture in the project
03 SKILL.md 正文
Setup Command System
Install the Command System architecture in the current project by creating the files listed below exactly as provided.
What this skill does
The files below are the reference implementation of the Command System (V2). They are correct and define the architecture and public API.
- Create the files listed below with the provided content.
- Adapt them so they compile and run in this project. Allowed and expected:
- Fix imports, aliases and paths.
- Adjust references to the project structure (e.g. env access, file locations).
- Fix type or compilation errors caused by the project setup (TypeScript config, strictness, lint rules).
- Format with the project formatter.
- Keep the design: the public API (
command,actions,Action,ScopedAction,useTransition,InstanceRegistry,TransitionStore), the file roles and the behavior stay the same. Do not redesign, add features or switch to another version (V3).
Decide these adaptations yourself and continue. Never stop to ask the user about them; list them in the final output.
Before copying
The files import through the #/ and @/ aliases. Prefer configuring both to resolve to src/; if the project uses another alias convention, change the imports instead:
tsconfig.json→compilerOptions.paths:"#/*": ["./src/*"]and"@/*": ["./src/*"].package.json→"imports": { "#/*": "./src/*" }, or the bundler alias equivalent.
The architecture rules for using the Command System live in the command-bus-architecture skill. Do not create a rule file.
Files to create
Create the following files in the project, using EXACTLY the content provided below for each file:
1. src/types/helpers.ts
export type ActionPaths<T> = T extends (...args: any[]) => any
? never
: {
[K in keyof T & string]: T[K] extends (...args: any[]) => any
? K
: `${K}.${ActionPaths<T[K]>}`;
}[keyof T & string];
export type DeepKeyPaths<T> = T extends object
? {
[K in keyof T & string]: K | `${K}.${DeepKeyPaths<T[K]>}`;
}[keyof T & string]
: never;
export type DeepKeys<T> = T extends object
? {
[K in keyof T]: K | DeepKeys<T[K]>;
}[keyof T]
: never;
export type PathValue<
T,
P extends string,
> = P extends `${infer K}.${infer Rest}`
? K extends keyof T
? PathValue<T[K], Rest>
: never
: P extends keyof T
? T[P]
: never;
export type PayloadFromAction<T> = T extends (
payload: infer P,
...args: any[]
) => any
? P
: never;
export type ReturnFromAction<T> = T extends (...args: any[]) => infer R
? Awaited<R>
: never;
2. src/utils/dev/dev.ts
export const dev = {
isDev: import.meta.env?.MODE === 'development' || process.env.NODE_ENV === 'development',
run: (fn: () => void) => {
if (dev.isDev) fn();
}
};
3. src/lib/command/subject.ts
export class Subject {
private observers = new Set<() => void>();
subscribe(observer: () => void) {
this.observers.add(observer);
return () => this.observers.delete(observer);
}
notify() {
for (const observer of this.observers) observer();
}
}
4. src/lib/command/types.ts
import type {
ActionPaths,
PathValue,
PayloadFromAction,
ReturnFromAction,
} from "#/types/helpers";
import type { Actions } from "./global";
// #region Transitions
export type TransitionKey = unknown | unknown[];
// #endregion
// #region Command Bus
export type Handler<TPayload = unknown, TResult = void> = (
payload: TPayload,
) => Promise<TResult>;
export type Dispose = () => void;
export type DispatchConfig = {
transition?: TransitionKey;
};
// #endregion
// #region Instance Registry
export type Instance = {
id: string;
label?: string;
};
// #endregion
// #region Actions
export type Config = DispatchConfig & {
instanceId: string;
};
export type Action<TPayload = undefined, TResult = void> = [TPayload] extends [
undefined,
]
? (payload?: TPayload, config?: DispatchConfig) => Promise<TResult>
: (payload: TPayload, config?: DispatchConfig) => Promise<TResult>;
export type ScopedAction<TPayload = undefined, TResult = void> = [
TPayload,
] extends [undefined]
? (payload: undefined, config: Config) => Promise<TResult>
: (payload: TPayload, config: Config) => Promise<TResult>;
// #endregion
// #region Aliases
export type ActionPath = ActionPaths<Actions>;
export type ActionValue<TCommand extends ActionPath> = PathValue<
Actions,
TCommand
>;
export type CommandMeta = { label: string };
// #endregion
// #region Derived
export type ActionPayload<TCommand extends ActionPath> = PayloadFromAction<
ActionValue<TCommand>
>;
export type ActionReturn<TCommand extends ActionPath> = ReturnFromAction<
ActionValue<TCommand>
>;
export type IsScopedCommand<TCommand extends ActionPath> =
ActionValue<TCommand> extends (
payload: any,
config: Config,
) => Promise<any>
? true
: false;
export type HandleConfig<TCommand extends ActionPath> =
IsScopedCommand<TCommand> extends true
? { instanceId: string; meta?: CommandMeta }
: { instanceId?: string; meta?: CommandMeta };
export type SecondArg<T> = T extends (a: any, b: infer B) => any ? B : never;
export type ScopedCommands = {
[K in ActionPath]: SecondArg<ActionValue<K>> extends Config ? K : never;
}[ActionPath];
export type UnscopedCommands = Exclude<ActionPath, ScopedCommands>;
// #endregion
5. src/lib/command/command-bus.ts
import { dev } from "#/utils/dev/dev";
import type { TransitionStore } from "./transitions-store";
import type { DispatchConfig, Dispose, Handler } from "./types";
export class CommandBus {
private handlers: Map<string, Handler> = new Map();
constructor(private readonly transition: TransitionStore) {}
handle<TPayload = unknown, TResult = void>(
command: string,
handler: Handler<TPayload, TResult>,
): Dispose {
if (this.handlers.has(command)) {
dev.run(() =>
console.error(
`[command]: already exist a handle registered for ${command}`,
),
);
return () => null;
}
this.handlers.set(command, handler as Handler);
return () => this.dispose(command);
}
async dispatch<TResult = void>(
command: string,
payload?: unknown,
config?: DispatchConfig,
): Promise<TResult | undefined> {
dev.run(() => console.info(`[command dispatched]: ${command}`, payload));
const handler = this.handlers.get(command);
if (!handler) {
const message = `[command]: no handler registered for ${command}`;
dev.run(() => console.error(message));
return;
}
const transitionKey = config?.transition ? config.transition : [command];
this.transition.start(transitionKey);
try {
return (await handler(payload)) as TResult;
} finally {
this.transition.done(transitionKey);
}
}
dispose(command: string): void {
this.handlers.delete(command);
}
}
6. src/lib/command/transitions-store.ts
import { Subject } from "./subject";
import type { TransitionKey } from "./types";
export class TransitionStore extends Subject {
private static instance: TransitionStore;
private counts: Map<string, number> = new Map();
private constructor() {
super();
}
start(key: TransitionKey) {
const serializedKey = TransitionStore.serializeKey(key);
const count = this.counts.get(serializedKey) ?? 0;
this.counts.set(serializedKey, count + 1);
this.notify();
}
done(key: TransitionKey) {
const serializedKey = TransitionStore.serializeKey(key);
const count = (this.counts.get(serializedKey) ?? 0) - 1;
if (count <= 0) this.counts.delete(serializedKey);
else this.counts.set(serializedKey, count);
this.notify();
}
isExecuting(key: TransitionKey) {
const serializedKey = TransitionStore.serializeKey(key);
return (this.counts.get(serializedKey) ?? 0) > 0;
}
static serializeKey(key: TransitionKey) {
return JSON.stringify(key);
}
static getInstance() {
if (!TransitionStore.instance) {
TransitionStore.instance = new TransitionStore();
}
return TransitionStore.instance;
}
}
7. src/lib/command/instance-registry.ts
import { Subject } from "./subject";
import type { Instance } from "./types";
export class InstanceRegistry extends Subject {
private static instance: InstanceRegistry;
private domains: Map<string, Map<string, Instance>> = new Map();
private instancesCache: Map<string, readonly Instance[]> = new Map();
private constructor() {
super();
}
add(domain: string, instance: Instance) {
if (!this.domains.has(domain)) {
this.domains.set(domain, new Map());
}
this.domains.get(domain)?.set(instance.id, instance);
this.instancesCache.delete(domain);
this.notify();
}
getInstances(domain: string): readonly Instance[] {
const cached = this.instancesCache.get(domain);
if (cached) return cached;
const fresh = Array.from(this.domains.get(domain)?.values() ?? []);
this.instancesCache.set(domain, fresh);
return fresh;
}
remove(domain: string, instanceId: string) {
if (!this.domains.has(domain)) return;
this.domains.get(domain)?.delete(instanceId);
this.instancesCache.delete(domain);
this.notify();
}
static getInstance() {
if (!InstanceRegistry.instance) {
InstanceRegistry.instance = new InstanceRegistry();
}
return InstanceRegistry.instance;
}
}
8. src/hooks/use-transition.ts
import { useSyncExternalStore } from "react";
import { TransitionStore } from "@/lib/command/transitions-store";
export function useTransition(key: unknown[]) {
return useSyncExternalStore(
(callback) => TransitionStore.getInstance().subscribe(callback),
() => TransitionStore.getInstance().isExecuting(key),
);
}
9. src/lib/command/index.ts
import type { DeepKeys } from "#/types/helpers";
import { CommandBus } from "./command-bus";
import { InstanceRegistry } from "./instance-registry";
import { TransitionStore } from "./transitions-store";
import type { Actions } from "./global";
import type {
ActionPath,
ActionPayload,
ActionReturn,
CommandMeta,
Config,
Handler,
ScopedCommands,
UnscopedCommands,
} from "./types";
export type {
Action,
Actions,
HandleConfig,
IsScopedCommand,
ScopedAction,
UnscopedCommands,
} from "./types";
export class Command {
private $commandBus: CommandBus;
private $transitions: TransitionStore;
private $instanceRegistry: InstanceRegistry;
constructor() {
this.$transitions = TransitionStore.getInstance();
this.$commandBus = new CommandBus(this.$transitions);
this.$instanceRegistry = InstanceRegistry.getInstance();
}
handle<TCommand extends ScopedCommands>(
command: TCommand,
handler: Handler<ActionPayload<TCommand>, ActionReturn<TCommand>>,
config: {
instanceId: string;
meta?: CommandMeta;
},
): () => void;
handle<TCommand extends UnscopedCommands>(
command: TCommand,
handler: Handler<ActionPayload<TCommand>, ActionReturn<TCommand>>,
config?: {
meta?: CommandMeta;
},
): () => void;
handle<TCommand extends ActionPath>(
command: TCommand,
handler: Handler<ActionPayload<TCommand>, ActionReturn<TCommand>>,
config?: {
instanceId?: string;
meta?: CommandMeta;
},
) {
const result = this.parseCommand(command, config?.instanceId);
const { domain, key, instanceId } = result;
if (domain && instanceId) {
this.$instanceRegistry.add(domain, {
id: instanceId,
label: config?.meta?.label,
});
}
const dispose = this.$commandBus.handle<
ActionPayload<TCommand>,
ActionReturn<TCommand>
>(key, handler);
return () => {
if (domain && instanceId)
this.$instanceRegistry.remove(domain, instanceId);
dispose();
};
}
dispatch<TCommand extends ActionPath>(
command: TCommand,
payload?: ActionPayload<TCommand>,
config?: Config,
) {
const result = this.parseCommand(command, config?.instanceId);
return this.$commandBus.dispatch<ActionPayload<TCommand>>(
result.key,
payload,
config,
);
}
getActionsProxy(path: DeepKeys<Actions>[] = []): Actions {
const self = this;
return new Proxy(() => {}, {
get(_target, prop: DeepKeys<Actions>) {
return self.getActionsProxy([...path, prop]);
},
apply(_target, _thisArg, args: [ActionPayload<ActionPath>, Config?]) {
const commandName = path.join(".") as ActionPath;
return self.dispatch(commandName, args[0], args[1]);
},
}) as unknown as Actions;
}
private parseCommand(command: ActionPath, instanceId?: string) {
const parts = command.split(".");
const hasDomain = parts.length > 1;
if (!hasDomain) {
return { domain: undefined, instanceId, key: command };
}
const domain = parts[0];
const path = parts.slice(1).join(".");
const key = instanceId ? `${instanceId}:${domain}.${path}` : command;
return { domain, instanceId, key };
}
}
export const command = new Command();
export const actions = command.getActionsProxy();
10. src/lib/command/global.ts
import type { Action, ScopedAction } from "./types";
export interface Actions {
// Define your global actions here
// Example:
// counter: {
// increment: Action;
// decrement: Action;
// reset: Action;
// };
//
// content: {
// show: ScopedAction<string>;
// };
}
Final output
Return:
- Files created
- Adaptations made, with the reason for each
Return this output to the calling skill and continue its flow. Do not end the run here unless this skill was invoked directly by the user.
04 同仓库更多 Skills
utils-implementation
Write and organize utils (pure helpers for formatting, mapping, filtering, math, parsing, ids, storage) as one namespace object per domain with clear, readable names. Use before creating or changing any util.
test-writer
Write tests for the current implementation
tech-lead
Analyze a project, turn the user's goal into an implementation-ready GitHub Epic and Issues (following project rules, the Command Bus architecture and Fragiola UI), then start epic-implementer automatically (use the no-implement flag to stop after planning).
tech-lead-publish
Publish a final Epic and its Issues to GitHub, linked and labeled, keeping dependency and execution information. Sub-skill of tech-lead.
setup-command-system
Install the Command System architecture in the project
reviewer
Review code changes and provide feedback