@boyingliu01/dsh-plugin-xp-gate 0.19.0
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.
- package/README.md +25 -0
- package/cordis.patch.yml +7 -0
- package/lib/command.d.ts +27 -0
- package/lib/command.d.ts.map +1 -0
- package/lib/command.js +62 -0
- package/lib/command.js.map +1 -0
- package/lib/gate-runner.d.ts +18 -0
- package/lib/gate-runner.d.ts.map +1 -0
- package/lib/gate-runner.js +46 -0
- package/lib/gate-runner.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +79 -0
- package/lib/index.js.map +1 -0
- package/package.json +70 -0
- package/skills/admin-template-guidelines/SKILL.md +113 -0
- package/skills/admin-template-guidelines/references/rule-1.md +91 -0
- package/skills/admin-template-guidelines/references/rule-2.md +155 -0
- package/skills/admin-template-guidelines/references/rule-3.md +70 -0
- package/skills/admin-template-guidelines/references/rule-4.md +154 -0
- package/skills/admin-template-guidelines/references/rule-5.md +126 -0
- package/skills/admin-template-guidelines/references/rule-6.md +180 -0
- package/skills/batch-grill-me/SKILL.md +15 -0
- package/skills/delphi-review/.delphi-config.json.example +70 -0
- package/skills/delphi-review/AGENTS.md +67 -0
- package/skills/delphi-review/INSTALL.md +150 -0
- package/skills/delphi-review/SKILL.md +559 -0
- package/skills/delphi-review/hooks.md +19 -0
- package/skills/delphi-review/opencode.json.delphi.example +52 -0
- package/skills/delphi-review/references/code-walkthrough.md +470 -0
- package/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
- package/skills/delphi-review/references/requirements.md +402 -0
- package/skills/delphi-review/references/round-templates.md +134 -0
- package/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/skills/delphi-review/tools.md +29 -0
- package/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/skills/domain-modeling/SKILL.md +74 -0
- package/skills/grill-with-docs/SKILL.md +7 -0
- package/skills/grilling/SKILL.md +12 -0
- package/skills/improve-codebase-architecture/SKILL.md +156 -0
- package/skills/ralph-loop/SKILL.md +198 -0
- package/skills/ralph-loop/references/components.md +113 -0
- package/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/skills/ralph-loop/references/workflow-details.md +99 -0
- package/skills/ralph-loop/templates/progress-log.md +36 -0
- package/skills/sprint-flow/AGENTS.md +103 -0
- package/skills/sprint-flow/SKILL.md +411 -0
- package/skills/sprint-flow/__tests__/sprint-flow.test.cjs +457 -0
- package/skills/sprint-flow/hooks.md +18 -0
- package/skills/sprint-flow/references/components/memory.md +87 -0
- package/skills/sprint-flow/references/components/middleware.md +75 -0
- package/skills/sprint-flow/references/components/skill-invocations.md +105 -0
- package/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/skills/sprint-flow/references/components/tool-descriptions.md +95 -0
- package/skills/sprint-flow/references/force-levels.md +204 -0
- package/skills/sprint-flow/references/orchestration-rules.md +236 -0
- package/skills/sprint-flow/references/phase-1-prep.md +294 -0
- package/skills/sprint-flow/references/phase-2-design.md +273 -0
- package/skills/sprint-flow/references/phase-3-build.md +348 -0
- package/skills/sprint-flow/references/phase-4-verify.md +225 -0
- package/skills/sprint-flow/references/phase-5-ship.md +222 -0
- package/skills/sprint-flow/references/phase-6-close.md +266 -0
- package/skills/sprint-flow/references/phase-overview.md +486 -0
- package/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
- package/skills/sprint-flow/templates/auto-estimate-output-template.md +110 -0
- package/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/skills/sprint-flow/templates/sprint-progress-template.md +140 -0
- package/skills/sprint-flow/templates/sprint-summary-template.md +121 -0
- package/skills/sprint-flow/tools.md +29 -0
- package/skills/test-driven-development/SKILL.md +371 -0
- package/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/skills/test-specification-alignment/AGENTS.md +56 -0
- package/skills/test-specification-alignment/SKILL.md +205 -0
- package/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- package/skills/test-specification-alignment/references/specification-format.md +348 -0
- package/skills/to-issues/SKILL.md +277 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @boyingliu01/dsh-plugin-xp-gate
|
|
2
|
+
|
|
3
|
+
XP-Gate deterministic quality gates + AI workflow skills as a **DeepSeek Harness (DSH)** native plugin.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
| Tool | Purpose |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| `gate-check` | Run XP-Gate quality gates (`xp-gate check`) on a file or directory |
|
|
10
|
+
| `gate-principles` | Run the 14 Clean Code/SOLID rules (`xp-gate principles`, Gate 4) |
|
|
11
|
+
| `gate-arch` | Run architecture validation (`xp-gate arch`, Gate 6) |
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
dsh plugin --profile <name> add @boyingliu01/dsh-plugin-xp-gate
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The tools require a global `xp-gate` CLI:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g @boyingliu01/xp-gate
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
When `xp-gate` is missing the tools degrade gracefully (tool-missing → SKIP-style message) rather than erroring.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# XP-Gate DSH bundle patch: mounts the xp-gate tool plugin into a dsh profile.
|
|
2
|
+
# Applied as ONE insert over the empty profile root; reconciled by
|
|
3
|
+
# `dsh plugin add @boyingliu01/dsh-plugin-xp-gate`.
|
|
4
|
+
|
|
5
|
+
- insert:
|
|
6
|
+
- id: tool-xp-gate
|
|
7
|
+
name: '@boyingliu01/dsh-plugin-xp-gate'
|
package/lib/command.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Shown when the global `xp-gate` CLI is not on PATH. */
|
|
2
|
+
export declare const FALLBACK_MESSAGE = "[XP-Gate] xp-gate CLI not installed. Install it with: npm install -g @boyingliu01/xp-gate";
|
|
3
|
+
/**
|
|
4
|
+
* Allowlist of xp-gate gate aliases that are safe to invoke standalone
|
|
5
|
+
* (non-preCommitOnly gates + common aliases). Anything outside this set is
|
|
6
|
+
* rejected before reaching the shell.
|
|
7
|
+
*/
|
|
8
|
+
export declare const GATE_WHITELIST: readonly ["duplicates", "complexity", "principles", "arch", "architecture", "iac", "secrets", "sast"];
|
|
9
|
+
export type GateId = (typeof GATE_WHITELIST)[number];
|
|
10
|
+
/** POSIX single-quote shell escaping: `'` becomes `'\''`. */
|
|
11
|
+
export declare function shq(value: string): string;
|
|
12
|
+
export declare function isGateAllowed(gate: string): boolean;
|
|
13
|
+
export interface BuildCommandOptions {
|
|
14
|
+
subcommand: "check" | "principles" | "arch";
|
|
15
|
+
target?: string;
|
|
16
|
+
gates?: readonly string[];
|
|
17
|
+
config?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build a single guarded shell command: runs `xp-gate …` when the global CLI
|
|
21
|
+
* is present, otherwise prints the graceful-degradation hint. A single
|
|
22
|
+
* `if command -v` avoids a separate probe/run race.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildCommand(options: BuildCommandOptions): string;
|
|
25
|
+
/** Resolve a model-supplied path against the session cwd; absolutes pass through. */
|
|
26
|
+
export declare function resolveTarget(rawPath: string, cwd: string): string;
|
|
27
|
+
//# sourceMappingURL=command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAEA,0DAA0D;AAC1D,eAAO,MAAM,gBAAgB,8FACgE,CAAA;AAE7F;;;;GAIG;AACH,eAAO,MAAM,cAAc,uGASjB,CAAA;AAEV,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEpD,6DAA6D;AAC7D,wBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,GAAG,YAAY,GAAG,MAAM,CAAA;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAuBD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAGjE;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAElE"}
|
package/lib/command.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
/** Shown when the global `xp-gate` CLI is not on PATH. */
|
|
3
|
+
export const FALLBACK_MESSAGE = "[XP-Gate] xp-gate CLI not installed. Install it with: npm install -g @boyingliu01/xp-gate";
|
|
4
|
+
/**
|
|
5
|
+
* Allowlist of xp-gate gate aliases that are safe to invoke standalone
|
|
6
|
+
* (non-preCommitOnly gates + common aliases). Anything outside this set is
|
|
7
|
+
* rejected before reaching the shell.
|
|
8
|
+
*/
|
|
9
|
+
export const GATE_WHITELIST = [
|
|
10
|
+
"duplicates",
|
|
11
|
+
"complexity",
|
|
12
|
+
"principles",
|
|
13
|
+
"arch",
|
|
14
|
+
"architecture",
|
|
15
|
+
"iac",
|
|
16
|
+
"secrets",
|
|
17
|
+
"sast",
|
|
18
|
+
];
|
|
19
|
+
/** POSIX single-quote shell escaping: `'` becomes `'\''`. */
|
|
20
|
+
export function shq(value) {
|
|
21
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
22
|
+
}
|
|
23
|
+
export function isGateAllowed(gate) {
|
|
24
|
+
return GATE_WHITELIST.includes(gate);
|
|
25
|
+
}
|
|
26
|
+
function buildInner(options) {
|
|
27
|
+
const { subcommand, target, gates, config } = options;
|
|
28
|
+
if (subcommand === "check") {
|
|
29
|
+
const tokens = ["xp-gate", "check"];
|
|
30
|
+
if (target !== undefined)
|
|
31
|
+
tokens.push(shq(target));
|
|
32
|
+
if (gates !== undefined && gates.length > 0) {
|
|
33
|
+
// Defense-in-depth: the JSON-schema enum already rejects unknown gate ids,
|
|
34
|
+
// but we also whitelist here so a non-whitelisted value can never reach the shell.
|
|
35
|
+
const allowed = gates.filter(isGateAllowed);
|
|
36
|
+
if (allowed.length > 0)
|
|
37
|
+
tokens.push("--gates", shq(allowed.join(",")));
|
|
38
|
+
}
|
|
39
|
+
return tokens.join(" ");
|
|
40
|
+
}
|
|
41
|
+
if (subcommand === "principles") {
|
|
42
|
+
const tokens = ["xp-gate", "principles"];
|
|
43
|
+
if (target !== undefined)
|
|
44
|
+
tokens.push(shq(target));
|
|
45
|
+
return tokens.join(" ");
|
|
46
|
+
}
|
|
47
|
+
return ["xp-gate", "arch", "--config", shq(config ?? "architecture.yaml")].join(" ");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build a single guarded shell command: runs `xp-gate …` when the global CLI
|
|
51
|
+
* is present, otherwise prints the graceful-degradation hint. A single
|
|
52
|
+
* `if command -v` avoids a separate probe/run race.
|
|
53
|
+
*/
|
|
54
|
+
export function buildCommand(options) {
|
|
55
|
+
const inner = buildInner(options);
|
|
56
|
+
return `if command -v xp-gate >/dev/null 2>&1; then ${inner}; else printf '%s\\n' ${shq(FALLBACK_MESSAGE)}; fi`;
|
|
57
|
+
}
|
|
58
|
+
/** Resolve a model-supplied path against the session cwd; absolutes pass through. */
|
|
59
|
+
export function resolveTarget(rawPath, cwd) {
|
|
60
|
+
return isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command.js","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAE/C,0DAA0D;AAC1D,MAAM,CAAC,MAAM,gBAAgB,GAC3B,2FAA2F,CAAA;AAE7F;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,MAAM;IACN,cAAc;IACd,KAAK;IACL,SAAS;IACT,MAAM;CACE,CAAA;AAIV,6DAA6D;AAC7D,MAAM,UAAU,GAAG,CAAC,KAAa;IAC/B,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAQ,cAAoC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;AAC7D,CAAC;AASD,SAAS,UAAU,CAAC,OAA4B;IAC9C,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IACrD,IAAI,UAAU,KAAK,OAAO,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QACnC,IAAI,MAAM,KAAK,SAAS;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;QAClD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5C,2EAA2E;YAC3E,mFAAmF;YACnF,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;YAC3C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QACxE,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACzB,CAAC;IACD,IAAI,UAAU,KAAK,YAAY,EAAE,CAAC;QAChC,MAAM,MAAM,GAAG,CAAC,SAAS,EAAE,YAAY,CAAC,CAAA;QACxC,IAAI,MAAM,KAAK,SAAS;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;QAClD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACzB,CAAC;IACD,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,IAAI,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACtF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,OAA4B;IACvD,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAA;IACjC,OAAO,+CAA+C,KAAK,yBAAyB,GAAG,CAAC,gBAAgB,CAAC,MAAM,CAAA;AACjH,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,GAAW;IACxD,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;AAC9D,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ShellExecRequest, ShellExecSpec, ShellRunResult } from "@deepseek-ai/dsh-shell";
|
|
2
|
+
/**
|
|
3
|
+
* Structural view of `ctx.shell` so the runner is testable with a fake shell
|
|
4
|
+
* without importing the abstract `ShellExecutor` service.
|
|
5
|
+
*/
|
|
6
|
+
export interface XpGateShell {
|
|
7
|
+
resolve(request: ShellExecRequest): ShellExecSpec;
|
|
8
|
+
run(spec: ShellExecSpec): Promise<ShellRunResult>;
|
|
9
|
+
}
|
|
10
|
+
/** Render a completed shell run as a single model-facing text block. */
|
|
11
|
+
export declare function renderGateResult(result: ShellRunResult): string;
|
|
12
|
+
/**
|
|
13
|
+
* Run an xp-gate command through the harness shell seam. Nonzero exits, timeouts,
|
|
14
|
+
* and sandbox denials resolve (and render) rather than reject; only cancellation
|
|
15
|
+
* (the caller's AbortSignal fired first) throws an AbortError with code ABORTED.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runXpGate(shell: XpGateShell, signal: AbortSignal, command: string, workdir: string, timeoutMs: number): Promise<string>;
|
|
18
|
+
//# sourceMappingURL=gate-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-runner.d.ts","sourceRoot":"","sources":["../src/gate-runner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAE7F;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,aAAa,CAAA;IACjD,GAAG,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;CAClD;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAqB/D;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CASjB"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { TOOL_ABORTED } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
import { HarnessError } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
/** Render a completed shell run as a single model-facing text block. */
|
|
4
|
+
export function renderGateResult(result) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
const stdout = result.stdout.text.trim();
|
|
7
|
+
if (stdout.length > 0)
|
|
8
|
+
lines.push(stdout);
|
|
9
|
+
const stderr = result.stderr.text.trim();
|
|
10
|
+
if (stderr.length > 0)
|
|
11
|
+
lines.push(`[stderr] ${stderr}`);
|
|
12
|
+
if (result.timedOut)
|
|
13
|
+
lines.push(`[timed out after ${result.timeoutMs}ms]`);
|
|
14
|
+
if (result.exitCode !== null && result.exitCode !== 0)
|
|
15
|
+
lines.push(`[exit code: ${result.exitCode}]`);
|
|
16
|
+
if (result.signal !== null)
|
|
17
|
+
lines.push(`[signal: ${result.signal}]`);
|
|
18
|
+
if (result.sandbox?.denied === true) {
|
|
19
|
+
lines.push(`[sandbox: file access denied under ${result.sandbox.mode} mode]`);
|
|
20
|
+
}
|
|
21
|
+
if (result.stdout.truncated) {
|
|
22
|
+
const spill = result.stdout.spillPath !== undefined ? `, full output: ${result.stdout.spillPath}` : "";
|
|
23
|
+
lines.push(`[stdout truncated${spill}]`);
|
|
24
|
+
}
|
|
25
|
+
if (result.stderr.truncated) {
|
|
26
|
+
const spill = result.stderr.spillPath !== undefined ? `, full output: ${result.stderr.spillPath}` : "";
|
|
27
|
+
lines.push(`[stderr truncated${spill}]`);
|
|
28
|
+
}
|
|
29
|
+
return lines.length > 0 ? lines.join("\n") : "[XP-Gate] command completed (no output).";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Run an xp-gate command through the harness shell seam. Nonzero exits, timeouts,
|
|
33
|
+
* and sandbox denials resolve (and render) rather than reject; only cancellation
|
|
34
|
+
* (the caller's AbortSignal fired first) throws an AbortError with code ABORTED.
|
|
35
|
+
*/
|
|
36
|
+
export async function runXpGate(shell, signal, command, workdir, timeoutMs) {
|
|
37
|
+
const spec = shell.resolve({ command, workdir, timeoutMs, signal });
|
|
38
|
+
const result = await shell.run(spec);
|
|
39
|
+
if (result.aborted) {
|
|
40
|
+
const error = new HarnessError("tool call aborted", TOOL_ABORTED);
|
|
41
|
+
error.name = "AbortError";
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
return renderGateResult(result);
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=gate-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-runner.js","sourceRoot":"","sources":["../src/gate-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAYnD,wEAAwE;AACxE,MAAM,UAAU,gBAAgB,CAAC,MAAsB;IACrD,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IACxC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACzC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IACxC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,YAAY,MAAM,EAAE,CAAC,CAAA;IACvD,IAAI,MAAM,CAAC,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,oBAAoB,MAAM,CAAC,SAAS,KAAK,CAAC,CAAA;IAC1E,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,eAAe,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAA;IACpG,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,YAAY,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;IACpE,IAAI,MAAM,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EAAE,CAAC;QACpC,KAAK,CAAC,IAAI,CAAC,sCAAsC,MAAM,CAAC,OAAO,CAAC,IAAI,QAAQ,CAAC,CAAA;IAC/E,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,kBAAkB,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QACtG,KAAK,CAAC,IAAI,CAAC,oBAAoB,KAAK,GAAG,CAAC,CAAA;IAC1C,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,kBAAkB,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QACtG,KAAK,CAAC,IAAI,CAAC,oBAAoB,KAAK,GAAG,CAAC,CAAA;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,0CAA0C,CAAA;AACzF,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAAkB,EAClB,MAAmB,EACnB,OAAe,EACf,OAAe,EACf,SAAiB;IAEjB,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAA;IACnE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACpC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,mBAAmB,EAAE,YAAY,CAAC,CAAA;QACjE,KAAK,CAAC,IAAI,GAAG,YAAY,CAAA;QACzB,MAAM,KAAK,CAAA;IACb,CAAC;IACD,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAA;AACjC,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAIlD,eAAO,MAAM,IAAI,iBAAiB,CAAA;AAClC,eAAO,MAAM,MAAM,UAAqB,CAAA;AAIxC,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,OAAY,GAAG,IAAI,CAmF/D"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
import { GATE_WHITELIST, buildCommand, resolveTarget } from "./command.js";
|
|
3
|
+
import { runXpGate } from "./gate-runner.js";
|
|
4
|
+
export const name = "tool-xp-gate";
|
|
5
|
+
export const inject = ["tools", "shell"];
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
7
|
+
export function apply(ctx, _config = {}) {
|
|
8
|
+
const shell = ctx.shell;
|
|
9
|
+
ctx.tools.register(defineTool({
|
|
10
|
+
name: "gate-check",
|
|
11
|
+
description: "Run XP-Gate quality gates (xp-gate check) on a file or directory and return the gate report. Prefers the global xp-gate CLI and degrades to an actionable install hint when it is absent.",
|
|
12
|
+
parameters: {
|
|
13
|
+
path: {
|
|
14
|
+
type: "string",
|
|
15
|
+
required: true,
|
|
16
|
+
description: "File or directory path to check (absolute or relative to the workspace).",
|
|
17
|
+
},
|
|
18
|
+
gates: {
|
|
19
|
+
type: "array",
|
|
20
|
+
items: { type: "string", enum: GATE_WHITELIST },
|
|
21
|
+
description: "Optional gate subset to run (e.g. ['principles', 'arch']).",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
output: {
|
|
25
|
+
schema: { type: "string" },
|
|
26
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
27
|
+
},
|
|
28
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
29
|
+
async execute(args, exec) {
|
|
30
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
31
|
+
const target = resolveTarget(args.path, cwd);
|
|
32
|
+
const command = buildCommand({ subcommand: "check", target, gates: args.gates });
|
|
33
|
+
return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS);
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
ctx.tools.register(defineTool({
|
|
37
|
+
name: "gate-principles",
|
|
38
|
+
description: "Run the 14 Clean Code / SOLID rules (xp-gate principles, Gate 4) on a file or directory and return the principles report.",
|
|
39
|
+
parameters: {
|
|
40
|
+
path: {
|
|
41
|
+
type: "string",
|
|
42
|
+
required: true,
|
|
43
|
+
description: "File or directory path to check (absolute or relative to the workspace).",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
output: {
|
|
47
|
+
schema: { type: "string" },
|
|
48
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
49
|
+
},
|
|
50
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
51
|
+
async execute(args, exec) {
|
|
52
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
53
|
+
const target = resolveTarget(args.path, cwd);
|
|
54
|
+
const command = buildCommand({ subcommand: "principles", target });
|
|
55
|
+
return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS);
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
ctx.tools.register(defineTool({
|
|
59
|
+
name: "gate-arch",
|
|
60
|
+
description: "Run architecture validation (xp-gate arch, Gate 6) using the given config file and return the architecture report. Defaults to architecture.yaml.",
|
|
61
|
+
parameters: {
|
|
62
|
+
config: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Architecture config file path (defaults to architecture.yaml).",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
output: {
|
|
68
|
+
schema: { type: "string" },
|
|
69
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
70
|
+
},
|
|
71
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
72
|
+
async execute(args, exec) {
|
|
73
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
74
|
+
const command = buildCommand({ subcommand: "arch", config: args.config });
|
|
75
|
+
return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS);
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boyingliu01/dsh-plugin-xp-gate",
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"description": "XP-Gate deterministic quality gates + AI workflow skills as a DeepSeek Harness (DSH) native plugin: gate-check / gate-principles / gate-arch tools + 12 SKILL.md skills",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"import": "./lib/index.js",
|
|
12
|
+
"default": "./lib/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/boyingliu01/xp-gate",
|
|
19
|
+
"directory": "plugins/dsh"
|
|
20
|
+
},
|
|
21
|
+
"dsh": {
|
|
22
|
+
"bundle": {
|
|
23
|
+
"patch": "./cordis.patch.yml"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"registry": "https://registry.npmjs.org",
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"lib/",
|
|
32
|
+
"cordis.patch.yml",
|
|
33
|
+
"skills/",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.build.json",
|
|
38
|
+
"check": "tsc -p tsconfig.json",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"prepare-skills": "node scripts/prepack.cjs",
|
|
41
|
+
"prepack": "npm run build && npm run prepare-skills"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
45
|
+
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-shell": "^0.1.1-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
51
|
+
"@deepseek-ai/dsh-llm": "0.1.1-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-shell": "0.1.1-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.2",
|
|
54
|
+
"@types/node": "^25.6.0",
|
|
55
|
+
"typescript": "^5.9.3",
|
|
56
|
+
"vitest": "^1.6.1",
|
|
57
|
+
"yaml": "^2.9.0"
|
|
58
|
+
},
|
|
59
|
+
"keywords": [
|
|
60
|
+
"dsh",
|
|
61
|
+
"deepseek-harness",
|
|
62
|
+
"plugin",
|
|
63
|
+
"xp-gate",
|
|
64
|
+
"quality-gates",
|
|
65
|
+
"code-review",
|
|
66
|
+
"sprint-flow"
|
|
67
|
+
],
|
|
68
|
+
"author": "boyingliu01",
|
|
69
|
+
"license": "MIT"
|
|
70
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: admin-template-guidelines
|
|
3
|
+
description: "6 maintainability rules for AI-generated admin interfaces (Fastify + Nunjucks + HTMX + Alpine.js). Prevents route bloat, test inconsistency, Nunjucks traps, repeated data logic, auth blind spots, and HTMX/Alpine confusion."
|
|
4
|
+
maturity: alpha
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Admin Template Guidelines
|
|
8
|
+
|
|
9
|
+
6 maintainability rules distilled from real rework patterns in interview-bot admin interface development. These constraints MUST be enforced during sprint-flow BUILD phase when generating admin routes, views, or tests.
|
|
10
|
+
|
|
11
|
+
## 核心原则
|
|
12
|
+
|
|
13
|
+
| 原则 | 说明 |
|
|
14
|
+
|------|------|
|
|
15
|
+
| **路由拆分** | Admin routes 按模块拆分,禁止单文件膨胀 |
|
|
16
|
+
| **测试一致性** | 所有测试使用共享 `createAdminTestApp()` helper |
|
|
17
|
+
| **Nunjucks 括号** | 比较表达式与 filter 混用时必须加括号 |
|
|
18
|
+
| **View Model Mapper** | 重复数据转换提取为独立函数 |
|
|
19
|
+
| **Auth 保护** | 所有 Admin 路由(含 GET)必须认证 |
|
|
20
|
+
| **HTMX+Alpine 分离** | HTMX 管服务端交互,Alpine 管客户端状态 |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 规则 1: 路由拆分(Route Splitting)
|
|
25
|
+
|
|
26
|
+
**Constraint:** Admin routes MUST be split by module — never a single monolithic file.
|
|
27
|
+
|
|
28
|
+
### 模块划分
|
|
29
|
+
|
|
30
|
+
| 文件 | 职责 |
|
|
31
|
+
|------|------|
|
|
32
|
+
| `admin/routes/templates.ts` | 模板 CRUD(列表/创建/编辑/删除) |
|
|
33
|
+
| `admin/routes/plans.ts` | 访谈计划管理(创建/启动/暂停/完结) |
|
|
34
|
+
| `admin/routes/reports.ts` | 报告生成与导出 |
|
|
35
|
+
| `admin/routes/analytics.ts` | 数据统计与看板 |
|
|
36
|
+
| `admin/routes/tree.ts` | 技能树/目录管理 |
|
|
37
|
+
| `admin/routes/admin-shared.ts` | 共享中间件、通用 helper、布局渲染 |
|
|
38
|
+
|
|
39
|
+
**Examples:** → see `references/rule-1.md`
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 规则 2: 测试一致性(Test Consistency)
|
|
44
|
+
|
|
45
|
+
**Constraint:** All admin route tests MUST use a shared `createAdminTestApp()` helper — never hand-roll mini Fastify instances per test file.
|
|
46
|
+
|
|
47
|
+
**Examples:** → see `references/rule-2.md`
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 规则 3: Nunjucks 括号陷阱(Nunjucks Parentheses)
|
|
52
|
+
|
|
53
|
+
**Constraint:** Comparison expressions in Nunjucks MUST be wrapped in parentheses when used with filters. The Nunjucks filter operator (`|`) has **higher precedence** than comparison operators, causing silent incorrect output.
|
|
54
|
+
|
|
55
|
+
**Examples & Checklist:** → see `references/rule-3.md`
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 规则 4: View Model Mapper
|
|
60
|
+
|
|
61
|
+
**Constraint:** Repeated data transformation patterns MUST be extracted into dedicated functions. Any data mapping, filtering, or calculation that appears in 3+ route handlers must live in a shared `view-models.ts` module.
|
|
62
|
+
|
|
63
|
+
### 常见重复模式
|
|
64
|
+
|
|
65
|
+
| 模式 | 出现位置 | 提取函数 |
|
|
66
|
+
|------|----------|----------|
|
|
67
|
+
| `_count` → `totalCount` 映射 | templates.ts, plans.ts, reports.ts | `mapCountFields()` |
|
|
68
|
+
| COMPLETED 状态过滤 | plans.ts, reports.ts, analytics.ts | `filterCompletedPlans()` |
|
|
69
|
+
| 完成率计算 | plans.ts, analytics.ts, tree.ts | `calculateCompletionRate()` |
|
|
70
|
+
| 完整 ViewModel 转换 | 所有列表页路由 | `mapTemplateToViewModel()` |
|
|
71
|
+
|
|
72
|
+
**Examples:** → see `references/rule-4.md`
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 规则 5: Auth 保护(Auth Protection)
|
|
77
|
+
|
|
78
|
+
**Constraint:** ALL Admin routes MUST be protected by authentication — including GET routes. No admin endpoint is exempt. Prefer cookie/session validation over API key headers.
|
|
79
|
+
|
|
80
|
+
### 路由豁免规则
|
|
81
|
+
|
|
82
|
+
| 路由类型 | 需要 Auth | 说明 |
|
|
83
|
+
|----------|-----------|------|
|
|
84
|
+
| Admin GET | ✅ 必须 | 管理页面包含敏感数据 |
|
|
85
|
+
| Admin POST/PUT/DELETE | ✅ 必须 | 数据变更操作 |
|
|
86
|
+
| Admin API (HTMX endpoints) | ✅ 必须 | 部分更新接口 |
|
|
87
|
+
| Public GET | ❌ 不需要 | 面向用户的公开页面 |
|
|
88
|
+
|
|
89
|
+
**Examples:** → see `references/rule-5.md`
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 规则 6: HTMX + Alpine 分离
|
|
94
|
+
|
|
95
|
+
**Constraint:** Clear separation of concerns between HTMX (server-driven content replacement) and Alpine.js (client-side UI state). HTMX handles server interactions; Alpine handles local component state.
|
|
96
|
+
|
|
97
|
+
### 职责矩阵
|
|
98
|
+
|
|
99
|
+
| 功能 | HTMX | Alpine.js |
|
|
100
|
+
|------|------|-----------|
|
|
101
|
+
| 页面导航 / URL 变更 | ✅ | ❌ |
|
|
102
|
+
| 表单提交到服务端 | ✅ | ❌ |
|
|
103
|
+
| 列表分页/搜索/排序 | ✅ | ❌ |
|
|
104
|
+
| 局部内容替换(`hx-target`) | ✅ | ❌ |
|
|
105
|
+
| 下拉菜单开关 | ❌ | ✅ |
|
|
106
|
+
| Modal 显示/隐藏 | ❌ | ✅ |
|
|
107
|
+
| 表单字段即时验证 | ❌ | ✅ |
|
|
108
|
+
| Toast 通知本地状态 | ❌ | ✅ |
|
|
109
|
+
| 按钮 loading 状态切换 | ❌ | ✅ |
|
|
110
|
+
|
|
111
|
+
**Examples & Principles:** → see `references/rule-6.md`
|
|
112
|
+
|
|
113
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Rule 1: Route Splitting — Examples
|
|
2
|
+
|
|
3
|
+
## ❌ 错误示例:单文件膨胀(800+ 行)
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// admin/routes/index.ts — 反模式
|
|
7
|
+
import { FastifyInstance } from 'fastify';
|
|
8
|
+
|
|
9
|
+
export async function adminRoutes(fastify: FastifyInstance) {
|
|
10
|
+
// 模板列表 — 第 10 行
|
|
11
|
+
fastify.get('/admin/templates', async (req, reply) => { ... });
|
|
12
|
+
// 模板创建 — 第 45 行
|
|
13
|
+
fastify.post('/admin/templates', async (req, reply) => { ... });
|
|
14
|
+
// 模板编辑 — 第 80 行
|
|
15
|
+
fastify.get('/admin/templates/:id/edit', async (req, reply) => { ... });
|
|
16
|
+
// 模板删除 — 第 115 行
|
|
17
|
+
fastify.delete('/admin/templates/:id', async (req, reply) => { ... });
|
|
18
|
+
// 计划列表 — 第 150 行
|
|
19
|
+
fastify.get('/admin/plans', async (req, reply) => { ... });
|
|
20
|
+
// 计划创建 — 第 190 行
|
|
21
|
+
fastify.post('/admin/plans', async (req, reply) => { ... });
|
|
22
|
+
// ... 持续膨胀到 800+ 行 ...
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## ✅ 正确示例:按模块拆分
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// admin/routes/admin-shared.ts
|
|
30
|
+
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
31
|
+
|
|
32
|
+
export function registerAdminShared(fastify: FastifyInstance) {
|
|
33
|
+
// 共享中间件:认证
|
|
34
|
+
fastify.addHook('onRequest', authenticateAdmin);
|
|
35
|
+
|
|
36
|
+
// 共享 helper:渲染带布局的页面
|
|
37
|
+
fastify.decorate('renderAdmin', async function (
|
|
38
|
+
this: FastifyInstance,
|
|
39
|
+
req: FastifyRequest,
|
|
40
|
+
reply: FastifyReply,
|
|
41
|
+
template: string,
|
|
42
|
+
data: Record<string, unknown>
|
|
43
|
+
) {
|
|
44
|
+
return reply.view(`admin/layouts/main.njk`, {
|
|
45
|
+
...data,
|
|
46
|
+
_content: await reply.view(`admin/${template}.njk`, data),
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// admin/routes/templates.ts
|
|
52
|
+
import { FastifyInstance } from 'fastify';
|
|
53
|
+
|
|
54
|
+
export async function registerTemplateRoutes(fastify: FastifyInstance) {
|
|
55
|
+
fastify.get('/admin/templates', { onRequest: [authenticateAdmin] }, listTemplates);
|
|
56
|
+
fastify.get('/admin/templates/new', { onRequest: [authenticateAdmin] }, showNewTemplate);
|
|
57
|
+
fastify.post('/admin/templates', { onRequest: [authenticateAdmin] }, createTemplate);
|
|
58
|
+
fastify.get('/admin/templates/:id/edit', { onRequest: [authenticateAdmin] }, showEditTemplate);
|
|
59
|
+
fastify.put('/admin/templates/:id', { onRequest: [authenticateAdmin] }, updateTemplate);
|
|
60
|
+
fastify.delete('/admin/templates/:id', { onRequest: [authenticateAdmin] }, deleteTemplate);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// admin/routes/plans.ts
|
|
64
|
+
import { FastifyInstance } from 'fastify';
|
|
65
|
+
|
|
66
|
+
export async function registerPlanRoutes(fastify: FastifyInstance) {
|
|
67
|
+
fastify.get('/admin/plans', { onRequest: [authenticateAdmin] }, listPlans);
|
|
68
|
+
fastify.post('/admin/plans', { onRequest: [authenticateAdmin] }, createPlan);
|
|
69
|
+
fastify.post('/admin/plans/:id/start', { onRequest: [authenticateAdmin] }, startPlan);
|
|
70
|
+
fastify.post('/admin/plans/:id/pause', { onRequest: [authenticateAdmin] }, pausePlan);
|
|
71
|
+
fastify.post('/admin/plans/:id/complete', { onRequest: [authenticateAdmin] }, completePlan);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// admin/routes/index.ts — 仅负责聚合注册
|
|
75
|
+
import { FastifyInstance } from 'fastify';
|
|
76
|
+
import { registerAdminShared } from './admin-shared';
|
|
77
|
+
import { registerTemplateRoutes } from './templates';
|
|
78
|
+
import { registerPlanRoutes } from './plans';
|
|
79
|
+
import { registerReportRoutes } from './reports';
|
|
80
|
+
import { registerAnalyticsRoutes } from './analytics';
|
|
81
|
+
import { registerTreeRoutes } from './tree';
|
|
82
|
+
|
|
83
|
+
export async function registerAllAdminRoutes(fastify: FastifyInstance) {
|
|
84
|
+
registerAdminShared(fastify);
|
|
85
|
+
await registerTemplateRoutes(fastify);
|
|
86
|
+
await registerPlanRoutes(fastify);
|
|
87
|
+
await registerReportRoutes(fastify);
|
|
88
|
+
await registerAnalyticsRoutes(fastify);
|
|
89
|
+
await registerTreeRoutes(fastify);
|
|
90
|
+
}
|
|
91
|
+
```
|