supipowers 1.5.3 → 2.0.1
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 +14 -8
- package/bin/install.mjs +20 -5
- package/bin/install.ts +95 -0
- package/package.json +8 -4
- package/skills/context-mode/SKILL.md +17 -10
- package/skills/harness/SKILL.md +94 -0
- package/skills/ui-design/SKILL.md +63 -0
- package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
- package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
- package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
- package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
- package/skills/ultraplan-discover/SKILL.md +96 -0
- package/skills/ultraplan-intake/SKILL.md +89 -0
- package/skills/ultraplan-research/SKILL.md +129 -0
- package/skills/ultraplan-review/SKILL.md +86 -0
- package/skills/ultraplan-review-scope/SKILL.md +111 -0
- package/skills/ultraplan-review-structure/SKILL.md +120 -0
- package/skills/ultraplan-review-tdd/SKILL.md +142 -0
- package/skills/ultraplan-scout/SKILL.md +110 -0
- package/skills/ultraplan-synthesize/SKILL.md +124 -0
- package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
- package/src/ai/schema-text.ts +129 -0
- package/src/ai/structured-output.ts +274 -0
- package/src/ai/template.ts +27 -0
- package/src/bootstrap.ts +63 -28
- package/src/commands/agents.ts +131 -42
- package/src/commands/ai-review.ts +251 -30
- package/src/commands/clear.ts +434 -0
- package/src/commands/commit.ts +1 -0
- package/src/commands/config.ts +242 -44
- package/src/commands/context.ts +55 -28
- package/src/commands/doctor.ts +234 -6
- package/src/commands/fix-pr.ts +306 -131
- package/src/commands/generate.ts +111 -21
- package/src/commands/memory.ts +192 -0
- package/src/commands/model-picker.ts +28 -21
- package/src/commands/model.ts +18 -8
- package/src/commands/optimize-context.ts +408 -29
- package/src/commands/plan.ts +2 -0
- package/src/commands/qa.ts +312 -137
- package/src/commands/release.ts +259 -76
- package/src/commands/review.ts +293 -59
- package/src/commands/status.ts +200 -13
- package/src/commands/supi.ts +3 -35
- package/src/commands/ui-design.ts +394 -0
- package/src/commands/ultraplan.ts +1518 -0
- package/src/commands/update.ts +86 -0
- package/src/config/defaults.ts +62 -0
- package/src/config/loader.ts +448 -60
- package/src/config/schema.ts +108 -2
- package/src/context/optimizer.ts +25 -33
- package/src/context/rule-renderer.ts +223 -0
- package/src/context/savings.ts +258 -0
- package/src/context/startup-check.ts +380 -0
- package/src/context/startup-optimizer.ts +355 -0
- package/src/context/tokenignore.ts +146 -0
- package/src/context-mode/cache-handle.ts +49 -0
- package/src/context-mode/cache-preview.ts +71 -0
- package/src/context-mode/cache-store.ts +738 -0
- package/src/context-mode/compressor.ts +131 -26
- package/src/context-mode/dedup.ts +108 -0
- package/src/context-mode/detector.ts +35 -4
- package/src/context-mode/event-extractor.ts +14 -12
- package/src/context-mode/event-store.ts +91 -36
- package/src/context-mode/hooks.ts +798 -56
- package/src/context-mode/knowledge/store.ts +255 -11
- package/src/context-mode/memory-store.ts +325 -0
- package/src/context-mode/metrics-recorder.ts +158 -0
- package/src/context-mode/metrics-store.ts +765 -0
- package/src/context-mode/model.ts +24 -0
- package/src/context-mode/processor-keys.ts +29 -0
- package/src/context-mode/processors/build.ts +66 -0
- package/src/context-mode/processors/docker.ts +57 -0
- package/src/context-mode/processors/git.ts +111 -0
- package/src/context-mode/processors/json.ts +112 -0
- package/src/context-mode/processors/k8s.ts +67 -0
- package/src/context-mode/processors/lint.ts +67 -0
- package/src/context-mode/processors/log.ts +86 -0
- package/src/context-mode/processors/registry.ts +116 -0
- package/src/context-mode/processors/test-runner.ts +102 -0
- package/src/context-mode/processors/types.ts +20 -0
- package/src/context-mode/repomap.ts +400 -0
- package/src/context-mode/routing.ts +97 -24
- package/src/context-mode/sandbox/runners.ts +5 -1
- package/src/context-mode/snapshot-builder.ts +106 -11
- package/src/context-mode/source-hash.ts +173 -0
- package/src/context-mode/tool-name.ts +11 -0
- package/src/context-mode/tools.ts +654 -22
- package/src/context-mode/web/fetcher.ts +31 -12
- package/src/debug/logger.ts +2 -1
- package/src/deps/registry.ts +1 -1
- package/src/discipline/failure-summarizer.ts +170 -0
- package/src/discipline/failure-taxonomy.ts +131 -0
- package/src/discipline/workflow-invariants.ts +125 -0
- package/src/discovery/index.ts +31 -0
- package/src/discovery/lsp.ts +87 -0
- package/src/discovery/rank.ts +144 -0
- package/src/discovery/sources.ts +89 -0
- package/src/discovery/workflow.ts +87 -0
- package/src/docs/contracts.ts +39 -0
- package/src/docs/drift.ts +117 -87
- package/src/fix-pr/assessment.ts +200 -0
- package/src/fix-pr/contracts.ts +47 -0
- package/src/fix-pr/fetch-comments.ts +80 -0
- package/src/fix-pr/prompt-builder.ts +58 -40
- package/src/fix-pr/scripts/exec.ts +34 -0
- package/src/fix-pr/scripts/trigger-review.ts +106 -0
- package/src/fix-pr/scripts/wait-and-check.ts +108 -0
- package/src/fix-pr/types.ts +4 -0
- package/src/git/branch-finish.ts +5 -0
- package/src/git/commit-contract.ts +83 -0
- package/src/git/commit.ts +121 -184
- package/src/git/status.ts +62 -8
- package/src/harness/anti_slop/architecture-parser.ts +210 -0
- package/src/harness/anti_slop/backend-factory.ts +30 -0
- package/src/harness/anti_slop/backend.ts +140 -0
- package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
- package/src/harness/anti_slop/fallow-adapter.ts +305 -0
- package/src/harness/anti_slop/installer.ts +227 -0
- package/src/harness/anti_slop/queue.ts +216 -0
- package/src/harness/anti_slop/recommend.ts +84 -0
- package/src/harness/anti_slop/score.ts +180 -0
- package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
- package/src/harness/artifacts/agents-md.ts +88 -0
- package/src/harness/artifacts/checks-wiring.ts +57 -0
- package/src/harness/artifacts/docs-tree.ts +79 -0
- package/src/harness/artifacts/lint-configs.ts +136 -0
- package/src/harness/artifacts/review-agents.ts +67 -0
- package/src/harness/bare-entry.ts +108 -0
- package/src/harness/command.ts +1010 -0
- package/src/harness/default-agents/design.md +23 -0
- package/src/harness/default-agents/discover.md +18 -0
- package/src/harness/default-agents/implement.md +24 -0
- package/src/harness/default-agents/plan.md +19 -0
- package/src/harness/default-agents/research.md +21 -0
- package/src/harness/default-agents/validate.md +22 -0
- package/src/harness/gc/reporter.ts +28 -0
- package/src/harness/gc/runner.ts +136 -0
- package/src/harness/hooks/layer-context-inject.ts +155 -0
- package/src/harness/hooks/post-session-sweep.ts +130 -0
- package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
- package/src/harness/hooks/register.ts +118 -0
- package/src/harness/model.ts +117 -0
- package/src/harness/pipeline.ts +348 -0
- package/src/harness/project-paths.ts +235 -0
- package/src/harness/stage-runner.ts +107 -0
- package/src/harness/stages/design.ts +386 -0
- package/src/harness/stages/discover.ts +454 -0
- package/src/harness/stages/implement.ts +162 -0
- package/src/harness/stages/plan.ts +335 -0
- package/src/harness/stages/research.ts +263 -0
- package/src/harness/stages/validate.ts +684 -0
- package/src/harness/storage.ts +467 -0
- package/src/harness/tools.ts +426 -0
- package/src/lsp/bridge.ts +56 -95
- package/src/lsp/capabilities.ts +108 -0
- package/src/lsp/contracts.ts +35 -0
- package/src/lsp/detector.ts +8 -12
- package/src/markdown-frontmatter.ts +68 -0
- package/src/mempalace/bridge.ts +135 -0
- package/src/mempalace/config.ts +75 -0
- package/src/mempalace/format.ts +163 -0
- package/src/mempalace/hooks.ts +370 -0
- package/src/mempalace/installer-helper.ts +194 -0
- package/src/mempalace/python/mempalace_bridge.py +440 -0
- package/src/mempalace/runtime.ts +565 -0
- package/src/mempalace/schema.ts +268 -0
- package/src/mempalace/session-summary.ts +198 -0
- package/src/mempalace/tool.ts +186 -0
- package/src/mempalace/uv.ts +256 -0
- package/src/migrate/runner.ts +354 -0
- package/src/planning/approval-flow.ts +206 -9
- package/src/planning/plan-writer-prompt.ts +4 -3
- package/src/planning/planning-ask-tool.ts +39 -0
- package/src/planning/render-markdown.ts +74 -0
- package/src/planning/spec.ts +42 -0
- package/src/planning/system-prompt.ts +11 -8
- package/src/planning/validate.ts +84 -0
- package/src/platform/omp.ts +15 -2
- package/src/platform/system-prompt.ts +37 -0
- package/src/platform/test-utils.ts +3 -0
- package/src/platform/types.ts +6 -1
- package/src/qa/config.ts +12 -6
- package/src/qa/detect-app-type.ts +13 -6
- package/src/qa/matrix.ts +12 -6
- package/src/qa/prompt-builder.ts +28 -30
- package/src/qa/scripts/dev-server-utils.ts +72 -0
- package/src/qa/scripts/run-e2e-tests.ts +226 -0
- package/src/qa/scripts/start-dev-server.ts +138 -0
- package/src/qa/scripts/stop-dev-server.ts +77 -0
- package/src/qa/session.ts +13 -7
- package/src/quality/ai-setup.ts +27 -25
- package/src/quality/contracts.ts +34 -0
- package/src/quality/gates/ai-review.ts +20 -58
- package/src/quality/gates/command.ts +249 -46
- package/src/quality/review-gates.ts +18 -2
- package/src/quality/runner.ts +63 -22
- package/src/quality/schemas.ts +37 -2
- package/src/quality/setup.ts +96 -16
- package/src/release/changelog.ts +1 -1
- package/src/release/channels/custom.ts +13 -3
- package/src/release/channels/types.ts +5 -0
- package/src/release/contracts.ts +90 -0
- package/src/release/executor.ts +122 -45
- package/src/release/prompt.ts +18 -2
- package/src/release/targets.ts +86 -0
- package/src/release/version.ts +96 -71
- package/src/review/agent-loader.ts +221 -109
- package/src/review/fixer.ts +10 -6
- package/src/review/multi-agent-runner.ts +114 -13
- package/src/review/output.ts +12 -139
- package/src/review/runner.ts +12 -6
- package/src/review/scope.ts +144 -24
- package/src/review/types.ts +1 -20
- package/src/review/validator.ts +12 -6
- package/src/storage/fix-pr-sessions.ts +21 -14
- package/src/storage/plans.ts +14 -5
- package/src/storage/qa-sessions.ts +25 -19
- package/src/storage/reliability-metrics.ts +180 -0
- package/src/storage/reports.ts +8 -7
- package/src/storage/review-sessions.ts +55 -20
- package/src/tool-catalog/active-tool-controller.ts +164 -0
- package/src/tool-catalog/active-tool-planner.ts +212 -0
- package/src/tool-catalog/tool-groups.ts +102 -0
- package/src/types.ts +1399 -5
- package/src/ui-design/backend-adapter.ts +78 -0
- package/src/ui-design/backends/local-html.ts +82 -0
- package/src/ui-design/backends/pencil-mcp.ts +111 -0
- package/src/ui-design/components-scanner.ts +124 -0
- package/src/ui-design/config.ts +55 -0
- package/src/ui-design/pen-scanner.ts +95 -0
- package/src/ui-design/pen-selector.ts +72 -0
- package/src/ui-design/prompt-builder.ts +73 -0
- package/src/ui-design/scanner.ts +136 -0
- package/src/ui-design/session.ts +974 -0
- package/src/ui-design/system-prompt.ts +312 -0
- package/src/ui-design/tokens-scanner.ts +181 -0
- package/src/ui-design/types.ts +96 -0
- package/src/ultraplan/agent-catalog.ts +522 -0
- package/src/ultraplan/authoring/agent-catalog.ts +310 -0
- package/src/ultraplan/authoring/authoring-tools.ts +552 -0
- package/src/ultraplan/authoring/command-handlers.ts +339 -0
- package/src/ultraplan/authoring/markdown.ts +510 -0
- package/src/ultraplan/authoring/model.ts +162 -0
- package/src/ultraplan/authoring/pipeline.ts +319 -0
- package/src/ultraplan/authoring/stage-runner.ts +141 -0
- package/src/ultraplan/authoring/stages/approve.ts +249 -0
- package/src/ultraplan/authoring/stages/discover.ts +289 -0
- package/src/ultraplan/authoring/stages/intake.ts +203 -0
- package/src/ultraplan/authoring/stages/research.ts +399 -0
- package/src/ultraplan/authoring/stages/review.ts +333 -0
- package/src/ultraplan/authoring/stages/scout.ts +188 -0
- package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
- package/src/ultraplan/authoring/storage.ts +594 -0
- package/src/ultraplan/authoring/synth-gate.ts +165 -0
- package/src/ultraplan/authoring-draft.ts +653 -0
- package/src/ultraplan/authoring-persist.ts +180 -0
- package/src/ultraplan/authoring-tool.ts +608 -0
- package/src/ultraplan/authoring-wizard.ts +587 -0
- package/src/ultraplan/batch/merge.ts +98 -0
- package/src/ultraplan/batch/planner.ts +150 -0
- package/src/ultraplan/batch/presenter.ts +97 -0
- package/src/ultraplan/batch/storage.ts +420 -0
- package/src/ultraplan/batch/supervisor.ts +317 -0
- package/src/ultraplan/batch/worker.ts +26 -0
- package/src/ultraplan/batch/worktree.ts +110 -0
- package/src/ultraplan/contracts.ts +1593 -0
- package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
- package/src/ultraplan/default-agents/authoring/intake.md +12 -0
- package/src/ultraplan/default-agents/authoring/planner.md +12 -0
- package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
- package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/scout.md +12 -0
- package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
- package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-executor.md +10 -0
- package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-tester.md +10 -0
- package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-executor.md +10 -0
- package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-tester.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
- package/src/ultraplan/execution/contract.ts +71 -0
- package/src/ultraplan/execution/policy.ts +217 -0
- package/src/ultraplan/execution/runtime-tools.ts +107 -0
- package/src/ultraplan/execution/session-runner.ts +281 -0
- package/src/ultraplan/next-router.ts +85 -0
- package/src/ultraplan/presenter.ts +359 -0
- package/src/ultraplan/project-paths.ts +342 -0
- package/src/ultraplan/runtime/active-execution.ts +72 -0
- package/src/ultraplan/runtime/apply-mutation.ts +416 -0
- package/src/ultraplan/runtime/blockers.ts +243 -0
- package/src/ultraplan/runtime/hook-bridge.ts +486 -0
- package/src/ultraplan/runtime/launch-context.ts +207 -0
- package/src/ultraplan/runtime/migration.ts +524 -0
- package/src/ultraplan/runtime/normalize.ts +281 -0
- package/src/ultraplan/runtime/proof.ts +260 -0
- package/src/ultraplan/runtime/reducer.ts +416 -0
- package/src/ultraplan/runtime/repair.ts +251 -0
- package/src/ultraplan/runtime/tracker-storage.ts +368 -0
- package/src/ultraplan/session-selection.ts +291 -0
- package/src/ultraplan/storage.ts +374 -0
- package/src/utils/editor.ts +38 -0
- package/src/utils/executable.ts +80 -0
- package/src/utils/paths.ts +1 -20
- package/src/utils/shell.ts +31 -0
- package/src/visual/companion.ts +2 -1
- package/src/visual/scripts/frame-template.html +60 -0
- package/src/visual/scripts/index.js +59 -13
- package/src/visual/scripts/package.json +3 -0
- package/src/visual/start-server.ts +2 -1
- package/src/workspace/git-scope.ts +64 -0
- package/src/workspace/locks.ts +23 -0
- package/src/workspace/package-manager.ts +117 -0
- package/src/workspace/path-mapping.ts +75 -0
- package/src/workspace/project-slug.ts +92 -0
- package/src/workspace/repo-root.ts +137 -0
- package/src/workspace/selector.ts +115 -0
- package/src/workspace/state-paths.ts +118 -0
- package/src/workspace/targets.ts +313 -0
- package/src/fix-pr/scripts/diff-comments.sh +0 -33
- package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
- package/src/fix-pr/scripts/trigger-review.sh +0 -36
- package/src/fix-pr/scripts/wait-and-check.sh +0 -37
- package/src/qa/scripts/detect-app-type.sh +0 -68
- package/src/qa/scripts/discover-routes.sh +0 -143
- package/src/qa/scripts/run-e2e-tests.sh +0 -131
- package/src/qa/scripts/start-dev-server.sh +0 -46
- package/src/qa/scripts/stop-dev-server.sh +0 -36
- package/src/review/prompts/fix-output-schema.md +0 -18
- package/src/review/prompts/review-output-schema.md +0 -38
- package/src/review/template.ts +0 -15
- /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { UiDesignBackendId } from "./types.js";
|
|
2
|
+
import { createLocalHtmlBackend } from "./backends/local-html.js";
|
|
3
|
+
import { createPencilMcpBackend } from "./backends/pencil-mcp.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Session-start options passed by the command handler to a backend.
|
|
7
|
+
* `sessionDir` is created by the caller; the adapter never creates it.
|
|
8
|
+
*/
|
|
9
|
+
export interface BackendStartSessionOptions {
|
|
10
|
+
sessionDir: string;
|
|
11
|
+
port?: number;
|
|
12
|
+
/** Absolute path to the target .pen file — required for `pencil-mcp`. */
|
|
13
|
+
penFilePath?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Handle returned by `startSession` — the canonical shape.
|
|
18
|
+
* `cleanup()` is idempotent; callers may invoke it multiple times.
|
|
19
|
+
*/
|
|
20
|
+
export interface BackendStartResult {
|
|
21
|
+
url: string;
|
|
22
|
+
cleanup: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type BackendFinalizeReason = "complete" | "discarded";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Backend contract. Implementations manage the lifecycle of the artifact
|
|
29
|
+
* surface (HTTP companion, MCP connection, etc.) — they do NOT own manifest.json.
|
|
30
|
+
*/
|
|
31
|
+
export interface UiDesignBackend {
|
|
32
|
+
id: UiDesignBackendId;
|
|
33
|
+
startSession(opts: BackendStartSessionOptions): Promise<BackendStartResult>;
|
|
34
|
+
artifactUrl(sessionDir: string, artifactPath: string): string | null;
|
|
35
|
+
finalize(sessionDir: string, reason: BackendFinalizeReason): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Thrown when a requested backend cannot be provided. */
|
|
39
|
+
export class BackendUnavailableError extends Error {
|
|
40
|
+
readonly code = "backend-unavailable" as const;
|
|
41
|
+
constructor(message: string) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "BackendUnavailableError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Runtime dependencies a backend factory may require. */
|
|
48
|
+
export interface GetBackendDeps {
|
|
49
|
+
/** Active tool names from the OMP harness. Used by the pencil-mcp backend. */
|
|
50
|
+
getActiveTools?: () => string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a backend by id. Local-html has no runtime deps; pencil-mcp needs
|
|
55
|
+
* the harness's active tool list to validate that the Pencil MCP server is
|
|
56
|
+
* connected. Additional MCP-based backends (figma, paper) will register
|
|
57
|
+
* through the same factory.
|
|
58
|
+
*/
|
|
59
|
+
export function getBackend(
|
|
60
|
+
id: UiDesignBackendId,
|
|
61
|
+
deps: GetBackendDeps = {},
|
|
62
|
+
): UiDesignBackend {
|
|
63
|
+
if (id === "local-html") {
|
|
64
|
+
return createLocalHtmlBackend();
|
|
65
|
+
}
|
|
66
|
+
if (id === "pencil-mcp") {
|
|
67
|
+
const getActiveTools = deps.getActiveTools;
|
|
68
|
+
if (!getActiveTools) {
|
|
69
|
+
throw new BackendUnavailableError(
|
|
70
|
+
"Backend 'pencil-mcp' requires `getActiveTools` — pass the OMP platform's tool introspection hook.",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return createPencilMcpBackend({ getActiveTools });
|
|
74
|
+
}
|
|
75
|
+
throw new BackendUnavailableError(
|
|
76
|
+
`Backend '${id}' is not available. Supported: local-html, pencil-mcp.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { VisualServerInfo } from "../../visual/types.js";
|
|
3
|
+
import { startVisualServer as realStartVisualServer } from "../../visual/start-server.js";
|
|
4
|
+
import { stopVisualServer as realStopVisualServer } from "../../visual/stop-server.js";
|
|
5
|
+
import {
|
|
6
|
+
BackendUnavailableError,
|
|
7
|
+
type BackendFinalizeReason,
|
|
8
|
+
type BackendStartResult,
|
|
9
|
+
type BackendStartSessionOptions,
|
|
10
|
+
type UiDesignBackend,
|
|
11
|
+
} from "../backend-adapter.js";
|
|
12
|
+
|
|
13
|
+
export interface LocalHtmlBackendDeps {
|
|
14
|
+
startVisualServer: (opts: { sessionDir: string; port?: number }) => Promise<VisualServerInfo | null>;
|
|
15
|
+
stopVisualServer: (sessionDir: string) => { status: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_DEPS: LocalHtmlBackendDeps = {
|
|
19
|
+
startVisualServer: realStartVisualServer,
|
|
20
|
+
stopVisualServer: realStopVisualServer,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Local HTML companion backend. Wraps `src/visual/` server lifecycle.
|
|
25
|
+
*
|
|
26
|
+
* `sessionDir` is the caller-created directory — we do not manage the manifest
|
|
27
|
+
* file or write design artifacts. The server streams files written to
|
|
28
|
+
* `sessionDir` to a browser companion.
|
|
29
|
+
*/
|
|
30
|
+
export function createLocalHtmlBackend(
|
|
31
|
+
deps: LocalHtmlBackendDeps = DEFAULT_DEPS,
|
|
32
|
+
): UiDesignBackend {
|
|
33
|
+
let currentUrl: string | null = null;
|
|
34
|
+
let currentSessionDir: string | null = null;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
id: "local-html",
|
|
38
|
+
|
|
39
|
+
async startSession(opts: BackendStartSessionOptions): Promise<BackendStartResult> {
|
|
40
|
+
const info = await deps.startVisualServer(
|
|
41
|
+
opts.port !== undefined
|
|
42
|
+
? { sessionDir: opts.sessionDir, port: opts.port }
|
|
43
|
+
: { sessionDir: opts.sessionDir },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (!info) {
|
|
47
|
+
throw new BackendUnavailableError(
|
|
48
|
+
`Failed to start local HTML companion for session ${opts.sessionDir}. Check that the port is free and try again.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
currentUrl = info.url;
|
|
53
|
+
currentSessionDir = opts.sessionDir;
|
|
54
|
+
|
|
55
|
+
let stopped = false;
|
|
56
|
+
const cleanup = async (): Promise<void> => {
|
|
57
|
+
if (stopped) return;
|
|
58
|
+
stopped = true;
|
|
59
|
+
deps.stopVisualServer(opts.sessionDir);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return { url: info.url, cleanup };
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
artifactUrl(sessionDir: string, artifactPath: string): string | null {
|
|
66
|
+
if (!currentUrl || currentSessionDir !== sessionDir) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const normalizedPath = artifactPath.split(path.sep).join("/").replace(/^\/?/, "/");
|
|
71
|
+
return new URL(normalizedPath, `${currentUrl}/`).toString();
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async finalize(sessionDir: string, _reason: BackendFinalizeReason): Promise<void> {
|
|
75
|
+
deps.stopVisualServer(sessionDir);
|
|
76
|
+
if (currentSessionDir === sessionDir) {
|
|
77
|
+
currentUrl = null;
|
|
78
|
+
currentSessionDir = null;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import {
|
|
4
|
+
BackendUnavailableError,
|
|
5
|
+
type BackendFinalizeReason,
|
|
6
|
+
type BackendStartResult,
|
|
7
|
+
type BackendStartSessionOptions,
|
|
8
|
+
type UiDesignBackend,
|
|
9
|
+
} from "../backend-adapter.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Minimal set of `mcp__pencil_*` tools the Director and its mandatory
|
|
13
|
+
* pencil sub-agents must be able to call for a pencil-mcp session to have a
|
|
14
|
+
* chance of succeeding. Availability is detected by checking the harness's
|
|
15
|
+
* active tools list for these exact names.
|
|
16
|
+
*/
|
|
17
|
+
export const REQUIRED_PENCIL_TOOLS = [
|
|
18
|
+
"mcp__pencil_open_document",
|
|
19
|
+
"mcp__pencil_get_editor_state",
|
|
20
|
+
"mcp__pencil_batch_get",
|
|
21
|
+
"mcp__pencil_batch_design",
|
|
22
|
+
"mcp__pencil_get_screenshot",
|
|
23
|
+
"mcp__pencil_snapshot_layout",
|
|
24
|
+
"mcp__pencil_search_all_unique_properties",
|
|
25
|
+
"mcp__pencil_export_nodes",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check whether the Pencil MCP server is currently connected by inspecting
|
|
30
|
+
* the harness's active tools list. OMP exposes MCP tools as
|
|
31
|
+
* `mcp__<server>_<tool>` — we require every non-optional tool named by the
|
|
32
|
+
* Director workflow or its pencil sub-agent templates.
|
|
33
|
+
*/
|
|
34
|
+
export function detectPencilMcp(activeTools: string[]): boolean {
|
|
35
|
+
if (!Array.isArray(activeTools) || activeTools.length === 0) return false;
|
|
36
|
+
const set = new Set(activeTools);
|
|
37
|
+
return REQUIRED_PENCIL_TOOLS.every((name) => set.has(name));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PencilMcpBackendDeps {
|
|
41
|
+
getActiveTools: () => string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PencilMcpStartOptions extends BackendStartSessionOptions {
|
|
45
|
+
/** Absolute path to the target .pen file, chosen by the caller. */
|
|
46
|
+
penFilePath?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pencil-MCP backend. Owns no long-running process — edits happen through
|
|
51
|
+
* `mcp__pencil_*` tool calls driven by the Design Director. `startSession`
|
|
52
|
+
* validates that the server is still connected and pins the chosen `.pen`
|
|
53
|
+
* path for later `artifactUrl` lookups.
|
|
54
|
+
*/
|
|
55
|
+
export function createPencilMcpBackend(
|
|
56
|
+
deps: PencilMcpBackendDeps,
|
|
57
|
+
): UiDesignBackend {
|
|
58
|
+
let currentSessionDir: string | null = null;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
id: "pencil-mcp",
|
|
62
|
+
|
|
63
|
+
async startSession(opts: PencilMcpStartOptions): Promise<BackendStartResult> {
|
|
64
|
+
if (!detectPencilMcp(deps.getActiveTools())) {
|
|
65
|
+
throw new BackendUnavailableError(
|
|
66
|
+
"Pencil MCP server is not connected. Start the `pencil` MCP server (exposes `mcp__pencil_batch_design` + `mcp__pencil_batch_get`) and retry.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!opts.penFilePath || !path.isAbsolute(opts.penFilePath)) {
|
|
71
|
+
throw new BackendUnavailableError(
|
|
72
|
+
"pencil-mcp backend requires an absolute `penFilePath`.",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
currentSessionDir = opts.sessionDir;
|
|
77
|
+
|
|
78
|
+
// No companion server to tear down — cleanup is an idempotent no-op so
|
|
79
|
+
// the command layer can call it freely on startup-failure paths.
|
|
80
|
+
let cleaned = false;
|
|
81
|
+
const cleanup = async (): Promise<void> => {
|
|
82
|
+
if (cleaned) return;
|
|
83
|
+
cleaned = true;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
url: pathToFileURL(opts.penFilePath).toString(),
|
|
88
|
+
cleanup,
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
artifactUrl(sessionDir: string, artifactPath: string): string | null {
|
|
93
|
+
if (currentSessionDir !== sessionDir) return null;
|
|
94
|
+
if (!artifactPath) return null;
|
|
95
|
+
const absolute = path.isAbsolute(artifactPath)
|
|
96
|
+
? artifactPath
|
|
97
|
+
: path.join(sessionDir, artifactPath);
|
|
98
|
+
return pathToFileURL(absolute).toString();
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async finalize(sessionDir: string, _reason: BackendFinalizeReason): Promise<void> {
|
|
102
|
+
// Deliberately a no-op: the `.pen` file is user-owned and must survive
|
|
103
|
+
// both `complete` and `discarded` terminal states. The command layer
|
|
104
|
+
// handles session-directory removal on discard; external `.pen` files
|
|
105
|
+
// are never touched here.
|
|
106
|
+
if (currentSessionDir === sessionDir) {
|
|
107
|
+
currentSessionDir = null;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ExistingComponent } from "./types.js";
|
|
4
|
+
import { normalizeRepoPath } from "../workspace/path-mapping.js";
|
|
5
|
+
|
|
6
|
+
export interface ScanExistingComponentsOptions {
|
|
7
|
+
globs?: string[];
|
|
8
|
+
excludes?: string[];
|
|
9
|
+
max?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ComponentsScanResult =
|
|
13
|
+
| { status: "missing"; items: [] }
|
|
14
|
+
| { status: "error"; items: []; reason: string }
|
|
15
|
+
| { status: "ok"; items: ExistingComponent[] };
|
|
16
|
+
|
|
17
|
+
const DEFAULT_GLOBS = [
|
|
18
|
+
"components/**/*.{tsx,jsx,vue,svelte}",
|
|
19
|
+
"src/components/**/*.{tsx,jsx,vue,svelte}",
|
|
20
|
+
"app/components/**/*.{tsx,jsx,vue,svelte}",
|
|
21
|
+
"ui/**/*.{tsx,jsx,vue,svelte}",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const DEFAULT_EXCLUDES = [
|
|
25
|
+
"node_modules/**",
|
|
26
|
+
"dist/**",
|
|
27
|
+
".omp/**",
|
|
28
|
+
"*.test.*",
|
|
29
|
+
"*.spec.*",
|
|
30
|
+
"*.stories.*",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const DEFAULT_MAX = 100;
|
|
34
|
+
|
|
35
|
+
function detectFramework(filePath: string): ExistingComponent["framework"] {
|
|
36
|
+
if (filePath.endsWith(".vue")) return "vue";
|
|
37
|
+
if (filePath.endsWith(".svelte")) return "svelte";
|
|
38
|
+
if (filePath.endsWith(".tsx") || filePath.endsWith(".jsx")) return "react";
|
|
39
|
+
return "unknown";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extractExports(content: string): string[] {
|
|
43
|
+
const names = new Set<string>();
|
|
44
|
+
const re = /export\s+(?:default\s+)?(?:function|const|class|let|var)\s+(\w+)/g;
|
|
45
|
+
let m: RegExpExecArray | null;
|
|
46
|
+
while ((m = re.exec(content)) !== null) {
|
|
47
|
+
names.add(m[1]!);
|
|
48
|
+
}
|
|
49
|
+
return Array.from(names);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function inferName(filePath: string): string {
|
|
53
|
+
const base = path.basename(filePath);
|
|
54
|
+
return base.replace(/\.(tsx|jsx|vue|svelte)$/, "");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function matchesExcludePattern(relPath: string, patterns: string[]): boolean {
|
|
58
|
+
for (const p of patterns) {
|
|
59
|
+
const glob = new Bun.Glob(p);
|
|
60
|
+
if (glob.match(relPath)) return true;
|
|
61
|
+
const base = path.basename(relPath);
|
|
62
|
+
if (glob.match(base)) return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function scanExistingComponents(
|
|
68
|
+
repoRoot: string,
|
|
69
|
+
opts: ScanExistingComponentsOptions = {},
|
|
70
|
+
): Promise<ComponentsScanResult> {
|
|
71
|
+
const globs = opts.globs ?? DEFAULT_GLOBS;
|
|
72
|
+
const excludes = opts.excludes ?? DEFAULT_EXCLUDES;
|
|
73
|
+
const max = opts.max ?? DEFAULT_MAX;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
if (!fs.existsSync(repoRoot) || !fs.statSync(repoRoot).isDirectory()) {
|
|
77
|
+
return { status: "missing", items: [] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const items: ExistingComponent[] = [];
|
|
81
|
+
const seen = new Set<string>();
|
|
82
|
+
|
|
83
|
+
for (const pattern of globs) {
|
|
84
|
+
const glob = new Bun.Glob(pattern);
|
|
85
|
+
for await (const match of glob.scan({ cwd: repoRoot, onlyFiles: true })) {
|
|
86
|
+
const repoRelativePath = normalizeRepoPath(match);
|
|
87
|
+
if (seen.has(repoRelativePath)) continue;
|
|
88
|
+
if (matchesExcludePattern(repoRelativePath, excludes)) continue;
|
|
89
|
+
seen.add(repoRelativePath);
|
|
90
|
+
|
|
91
|
+
const absPath = path.join(repoRoot, repoRelativePath);
|
|
92
|
+
let content = "";
|
|
93
|
+
try {
|
|
94
|
+
content = fs.readFileSync(absPath, "utf-8");
|
|
95
|
+
} catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
items.push({
|
|
100
|
+
name: inferName(repoRelativePath),
|
|
101
|
+
path: repoRelativePath,
|
|
102
|
+
framework: detectFramework(repoRelativePath),
|
|
103
|
+
exports: extractExports(content),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (items.length >= max) break;
|
|
107
|
+
}
|
|
108
|
+
if (items.length >= max) break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (items.length === 0) {
|
|
112
|
+
return { status: "missing", items: [] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
items.sort((a, b) => a.path.localeCompare(b.path));
|
|
116
|
+
return { status: "ok", items };
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
status: "error",
|
|
120
|
+
items: [],
|
|
121
|
+
reason: (err as Error).message,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { PlatformPaths } from "../platform/types.js";
|
|
4
|
+
import type { UiDesignBackendId } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const CONFIG_FILENAME = "ui-design.json";
|
|
7
|
+
const SUPPORTED_BACKENDS: UiDesignBackendId[] = ["local-html", "pencil-mcp"];
|
|
8
|
+
|
|
9
|
+
export interface UiDesignConfig {
|
|
10
|
+
backend: UiDesignBackendId;
|
|
11
|
+
/** Optional port override for local HTML companion. */
|
|
12
|
+
port?: number;
|
|
13
|
+
/** Optional overrides for the components-scanner glob list. */
|
|
14
|
+
componentsGlobs?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_UI_DESIGN_CONFIG: UiDesignConfig = {
|
|
18
|
+
backend: "local-html",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getConfigPath(paths: PlatformPaths, cwd: string): string {
|
|
22
|
+
return paths.project(cwd, CONFIG_FILENAME);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isValidConfig(data: unknown): data is UiDesignConfig {
|
|
26
|
+
if (!data || typeof data !== "object") return false;
|
|
27
|
+
const backend = (data as { backend?: unknown }).backend;
|
|
28
|
+
if (typeof backend !== "string") return false;
|
|
29
|
+
return SUPPORTED_BACKENDS.includes(backend as UiDesignBackendId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function loadUiDesignConfig(
|
|
33
|
+
paths: PlatformPaths,
|
|
34
|
+
cwd: string,
|
|
35
|
+
): UiDesignConfig | null {
|
|
36
|
+
const configPath = getConfigPath(paths, cwd);
|
|
37
|
+
if (!fs.existsSync(configPath)) return null;
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
40
|
+
if (!isValidConfig(parsed)) return null;
|
|
41
|
+
return parsed;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function saveUiDesignConfig(
|
|
48
|
+
paths: PlatformPaths,
|
|
49
|
+
cwd: string,
|
|
50
|
+
config: UiDesignConfig,
|
|
51
|
+
): void {
|
|
52
|
+
const configPath = getConfigPath(paths, cwd);
|
|
53
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
54
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
55
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface PenFileEntry {
|
|
5
|
+
/** Absolute path — passed to `mcp__pencil_*` tools as `filePath`. */
|
|
6
|
+
absolutePath: string;
|
|
7
|
+
/** Path relative to the repo root, using POSIX separators for display. */
|
|
8
|
+
relativePath: string;
|
|
9
|
+
/** File size in bytes (cheap disambiguation between same-named files). */
|
|
10
|
+
bytes: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ScanPenFilesOptions {
|
|
14
|
+
excludes?: string[];
|
|
15
|
+
max?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_EXCLUDES = [
|
|
19
|
+
"node_modules",
|
|
20
|
+
"dist",
|
|
21
|
+
"build",
|
|
22
|
+
".next",
|
|
23
|
+
".cache",
|
|
24
|
+
".omp",
|
|
25
|
+
".git",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const DEFAULT_MAX = 50;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Walk `repoRoot` recursively and collect every `.pen` file outside the
|
|
32
|
+
* excluded directories. Never throws: IO failures degrade to an empty list.
|
|
33
|
+
* Entries are returned sorted by relative path for deterministic UI order.
|
|
34
|
+
*/
|
|
35
|
+
export function scanPenFiles(
|
|
36
|
+
repoRoot: string,
|
|
37
|
+
opts: ScanPenFilesOptions = {},
|
|
38
|
+
): PenFileEntry[] {
|
|
39
|
+
const excludes = new Set(opts.excludes ?? DEFAULT_EXCLUDES);
|
|
40
|
+
const max = opts.max ?? DEFAULT_MAX;
|
|
41
|
+
|
|
42
|
+
let root: string;
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.statSync(repoRoot);
|
|
45
|
+
if (!stat.isDirectory()) return [];
|
|
46
|
+
root = path.resolve(repoRoot);
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const entries: PenFileEntry[] = [];
|
|
52
|
+
|
|
53
|
+
const visit = (absDir: string): void => {
|
|
54
|
+
if (entries.length >= max) return;
|
|
55
|
+
let dirents: fs.Dirent[];
|
|
56
|
+
try {
|
|
57
|
+
dirents = fs.readdirSync(absDir, { withFileTypes: true });
|
|
58
|
+
} catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const entry of dirents) {
|
|
63
|
+
if (entries.length >= max) return;
|
|
64
|
+
const entryAbs = path.join(absDir, entry.name);
|
|
65
|
+
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
if (excludes.has(entry.name)) continue;
|
|
68
|
+
visit(entryAbs);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!entry.isFile()) continue;
|
|
73
|
+
if (!entry.name.endsWith(".pen")) continue;
|
|
74
|
+
|
|
75
|
+
let bytes = 0;
|
|
76
|
+
try {
|
|
77
|
+
bytes = fs.statSync(entryAbs).size;
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const relative = path.relative(root, entryAbs).split(path.sep).join("/");
|
|
83
|
+
entries.push({
|
|
84
|
+
absolutePath: entryAbs,
|
|
85
|
+
relativePath: relative,
|
|
86
|
+
bytes,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
visit(root);
|
|
92
|
+
|
|
93
|
+
entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
94
|
+
return entries;
|
|
95
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { scanPenFiles, type PenFileEntry } from "./pen-scanner.js";
|
|
3
|
+
|
|
4
|
+
export interface PenSelection {
|
|
5
|
+
kind: "existing" | "new";
|
|
6
|
+
/** Absolute path — passed directly to `mcp__pencil_*` tools. */
|
|
7
|
+
penFilePath: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SelectPenFileOptions {
|
|
11
|
+
ctx: any;
|
|
12
|
+
repoRoot: string;
|
|
13
|
+
/** Used as the parent for the "Create a new .pen" fallback. */
|
|
14
|
+
sessionDir: string;
|
|
15
|
+
/** Injection hook for tests. Defaults to `scanPenFiles`. */
|
|
16
|
+
scan?: (repoRoot: string) => PenFileEntry[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CREATE_NEW_LABEL = "Create a new .pen in the session directory";
|
|
20
|
+
|
|
21
|
+
function formatBytes(bytes: number): string {
|
|
22
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
23
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
24
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatLabel(entry: PenFileEntry): string {
|
|
28
|
+
return `${entry.relativePath} (${formatBytes(entry.bytes)})`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function newPenPath(sessionDir: string): string {
|
|
32
|
+
return path.join(sessionDir, "design.pen");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Ask the user which `.pen` file to drive this `/supi:ui-design` session
|
|
37
|
+
* against. Scans `repoRoot` for existing `.pen` files and offers them plus a
|
|
38
|
+
* "create a new one in the session dir" fallback.
|
|
39
|
+
*
|
|
40
|
+
* Zero discovered files or a headless context → auto-default (no prompt). A
|
|
41
|
+
* null UI selection (user cancelled) → returns `null`; the caller aborts.
|
|
42
|
+
*/
|
|
43
|
+
export async function selectPenFile(
|
|
44
|
+
opts: SelectPenFileOptions,
|
|
45
|
+
): Promise<PenSelection | null> {
|
|
46
|
+
const scan = opts.scan ?? scanPenFiles;
|
|
47
|
+
const entries = scan(opts.repoRoot);
|
|
48
|
+
|
|
49
|
+
if (entries.length === 0) {
|
|
50
|
+
return { kind: "new", penFilePath: newPenPath(opts.sessionDir) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!opts.ctx?.hasUI) {
|
|
54
|
+
// Headless: deterministic fallback to the first (alphabetically sorted) entry
|
|
55
|
+
// so CI smokes stay non-interactive.
|
|
56
|
+
const first = entries[0]!;
|
|
57
|
+
return { kind: "existing", penFilePath: first.absolutePath };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const labels = [...entries.map(formatLabel), CREATE_NEW_LABEL];
|
|
61
|
+
const choice = await opts.ctx.ui.select("Select a .pen file", labels);
|
|
62
|
+
if (!choice) return null;
|
|
63
|
+
|
|
64
|
+
if (choice === CREATE_NEW_LABEL) {
|
|
65
|
+
return { kind: "new", penFilePath: newPenPath(opts.sessionDir) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const matched = entries.find((entry) => formatLabel(entry) === choice);
|
|
69
|
+
if (!matched) return null;
|
|
70
|
+
|
|
71
|
+
return { kind: "existing", penFilePath: matched.absolutePath };
|
|
72
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ContextScan } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface UiDesignKickoffOptions {
|
|
4
|
+
topic?: string;
|
|
5
|
+
sessionDir: string;
|
|
6
|
+
companionUrl: string;
|
|
7
|
+
contextScanSummary: string;
|
|
8
|
+
/** When set, the kickoff cites this .pen path and instructs the director
|
|
9
|
+
* to `mcp__pencil_open_document` it at the start of Phase 2. */
|
|
10
|
+
penFilePath?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build the kickoff user-message. The system-prompt override carries the full
|
|
15
|
+
* phase contract; the kickoff itself just triggers the first turn and provides
|
|
16
|
+
* the session coordinates.
|
|
17
|
+
*/
|
|
18
|
+
export function buildUiDesignKickoffPrompt(opts: UiDesignKickoffOptions): string {
|
|
19
|
+
const lines: string[] = [
|
|
20
|
+
"Begin the Design Director workflow for `/supi:ui-design`.",
|
|
21
|
+
"",
|
|
22
|
+
`Session directory: \`${opts.sessionDir}\``,
|
|
23
|
+
`Companion URL: ${opts.companionUrl}`,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
if (opts.penFilePath) {
|
|
27
|
+
lines.push(`Target .pen file: \`${opts.penFilePath}\``);
|
|
28
|
+
lines.push(
|
|
29
|
+
"At the start of Phase 2, call `mcp__pencil_open_document` on the target .pen file. Every subsequent `mcp__pencil_*` call MUST pass the same `filePath`.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
lines.push("", "## Context Scan", "", opts.contextScanSummary.trim(), "");
|
|
34
|
+
|
|
35
|
+
if (opts.topic) {
|
|
36
|
+
lines.push(`Design target: ${opts.topic}`, "");
|
|
37
|
+
lines.push(
|
|
38
|
+
"Start Phase 1: use `planning_ask` to confirm the scope (page / flow / component) for this target, then update `manifest.json`.",
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
lines.push(
|
|
42
|
+
"No target provided. Start Phase 1: use `planning_ask` to learn what the user wants to design, then confirm the scope (page / flow / component) before updating `manifest.json`.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return lines.join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pre-render a compact markdown summary of the ContextScan for use in the
|
|
51
|
+
* kickoff prompt and the director system-prompt block. Stays under ~500 bytes.
|
|
52
|
+
*/
|
|
53
|
+
export function renderContextScanSummary(scan: ContextScan): string {
|
|
54
|
+
const framework = scan.packageInfo.status === "ok" ? scan.packageInfo.framework : "missing";
|
|
55
|
+
const tokens = scan.tokens.status === "ok" ? scan.tokens.source : scan.tokens.status;
|
|
56
|
+
const componentCount = scan.components.status === "ok" ? scan.components.items.length : 0;
|
|
57
|
+
const componentsLabel =
|
|
58
|
+
scan.components.status === "ok" ? `${componentCount}` : scan.components.status;
|
|
59
|
+
const designMd =
|
|
60
|
+
scan.designMd.status === "ok" ? scan.designMd.path : `design.md: ${scan.designMd.status}`;
|
|
61
|
+
const uiLibs =
|
|
62
|
+
scan.packageInfo.status === "ok" && scan.packageInfo.uiLibraries.length > 0
|
|
63
|
+
? scan.packageInfo.uiLibraries.join(", ")
|
|
64
|
+
: "none";
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
`- Framework: ${framework}`,
|
|
68
|
+
`- Tokens: ${tokens}`,
|
|
69
|
+
`- Components: ${componentsLabel}`,
|
|
70
|
+
`- design.md: ${scan.designMd.status === "ok" ? designMd : "missing"}`,
|
|
71
|
+
`- UI libraries: ${uiLibs}`,
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|