supipowers 1.5.2 → 2.0.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 +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 +149 -45
- 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 +19 -9
- 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 +129 -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 +264 -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 +298 -127
- package/src/review/fixer.ts +10 -6
- package/src/review/multi-agent-runner.ts +115 -14
- 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 +11 -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 +1401 -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
package/src/config/loader.ts
CHANGED
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type {
|
|
5
|
+
CommandGateId,
|
|
5
6
|
ConfigScope,
|
|
6
7
|
SupipowersConfig,
|
|
7
8
|
ReleaseChannel,
|
|
8
9
|
QualityGatesConfig,
|
|
9
10
|
} from "../types.js";
|
|
10
11
|
import type { PlatformPaths } from "../platform/types.js";
|
|
12
|
+
import { resolvePackageManager } from "../workspace/package-manager.js";
|
|
13
|
+
import {
|
|
14
|
+
discoverWorkspaceTargets,
|
|
15
|
+
normalizeWorkspaceRelativePath,
|
|
16
|
+
} from "../workspace/targets.js";
|
|
17
|
+
import { getRootConfigPath } from "../workspace/state-paths.js";
|
|
11
18
|
import { DEFAULT_CONFIG } from "./defaults.js";
|
|
12
19
|
import {
|
|
13
20
|
collectConfigValidationErrors,
|
|
@@ -32,16 +39,49 @@ export interface QualityGateRecoveryInspection {
|
|
|
32
39
|
scopes: ScopedConfigInspection[];
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
export interface ConfigResolutionOptions {
|
|
43
|
+
repoRoot?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ConfigMutationOptions extends ConfigResolutionOptions {
|
|
47
|
+
scope?: ConfigScope;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ResolvedConfigContext {
|
|
51
|
+
repoRoot: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ResolvedConfigLayer {
|
|
55
|
+
scope: ConfigScope;
|
|
56
|
+
path: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveConfigContext(cwd: string, options?: ConfigResolutionOptions): ResolvedConfigContext {
|
|
60
|
+
return { repoRoot: options?.repoRoot ?? cwd };
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
function getGlobalConfigPath(paths: PlatformPaths): string {
|
|
40
64
|
return paths.global("config.json");
|
|
41
65
|
}
|
|
42
66
|
|
|
43
|
-
function getConfigPath(
|
|
44
|
-
|
|
67
|
+
function getConfigPath(
|
|
68
|
+
paths: PlatformPaths,
|
|
69
|
+
cwd: string,
|
|
70
|
+
scope: ConfigScope,
|
|
71
|
+
options?: ConfigResolutionOptions,
|
|
72
|
+
): string {
|
|
73
|
+
const { repoRoot } = resolveConfigContext(cwd, options);
|
|
74
|
+
|
|
75
|
+
switch (scope) {
|
|
76
|
+
case "global":
|
|
77
|
+
return getGlobalConfigPath(paths);
|
|
78
|
+
case "root":
|
|
79
|
+
return getRootConfigPath(paths, repoRoot);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getInspectionScopes(): ConfigScope[] {
|
|
84
|
+
return ["global", "root"];
|
|
45
85
|
}
|
|
46
86
|
|
|
47
87
|
function readJsonFile(
|
|
@@ -136,6 +176,49 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
|
|
136
176
|
: null;
|
|
137
177
|
}
|
|
138
178
|
|
|
179
|
+
function hasMeaningfulUltraPlanPolicy(config: Record<string, unknown> | null): boolean {
|
|
180
|
+
const ultraplan = asRecord(config?.ultraplan);
|
|
181
|
+
if (!ultraplan) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const slots = asRecord(ultraplan.slots);
|
|
186
|
+
if (slots && Object.values(slots).some((override) => {
|
|
187
|
+
const slotOverride = asRecord(override);
|
|
188
|
+
return !!slotOverride && (
|
|
189
|
+
typeof slotOverride.agentName === "string"
|
|
190
|
+
|| typeof slotOverride.model === "string"
|
|
191
|
+
|| typeof slotOverride.thinkingLevel === "string"
|
|
192
|
+
);
|
|
193
|
+
})) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const reviewGates = asRecord(ultraplan.reviewGates);
|
|
198
|
+
return !!reviewGates && Object.values(reviewGates).some((policy) => {
|
|
199
|
+
const reviewGate = asRecord(policy);
|
|
200
|
+
return !!reviewGate && typeof reviewGate.enabled === "boolean";
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function omitMeaninglessUltraPlanFromGlobalRead(
|
|
205
|
+
config: Record<string, unknown> | null,
|
|
206
|
+
scope: ConfigScope,
|
|
207
|
+
): Record<string, unknown> | null {
|
|
208
|
+
if (
|
|
209
|
+
scope !== "global"
|
|
210
|
+
|| !config
|
|
211
|
+
|| !Object.prototype.hasOwnProperty.call(config, "ultraplan")
|
|
212
|
+
|| hasMeaningfulUltraPlanPolicy(config)
|
|
213
|
+
) {
|
|
214
|
+
return config;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const { ultraplan: _ultraplan, ...scoped } = config;
|
|
218
|
+
return scoped;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
139
222
|
function normalizeReleaseChannels(
|
|
140
223
|
existingChannels: unknown,
|
|
141
224
|
pipeline: string | null,
|
|
@@ -153,6 +236,93 @@ function normalizeReleaseChannels(
|
|
|
153
236
|
return [];
|
|
154
237
|
}
|
|
155
238
|
|
|
239
|
+
const COMMAND_GATE_IDS: CommandGateId[] = ["lint", "typecheck", "format", "test-suite", "build"];
|
|
240
|
+
|
|
241
|
+
function createAllTargetsRun(command: string): { command: string; target: { scope: "all-targets" } } {
|
|
242
|
+
return {
|
|
243
|
+
command,
|
|
244
|
+
target: { scope: "all-targets" },
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeCommandGateRunTarget(target: unknown): unknown {
|
|
249
|
+
const record = asRecord(target);
|
|
250
|
+
if (!record || typeof record.scope !== "string") {
|
|
251
|
+
return target;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
switch (record.scope) {
|
|
255
|
+
case "all-targets":
|
|
256
|
+
case "root":
|
|
257
|
+
case "all-workspaces":
|
|
258
|
+
return { scope: record.scope };
|
|
259
|
+
case "workspace":
|
|
260
|
+
return {
|
|
261
|
+
scope: "workspace",
|
|
262
|
+
relativeDir:
|
|
263
|
+
typeof record.relativeDir === "string"
|
|
264
|
+
? normalizeWorkspaceRelativePath(record.relativeDir.trim())
|
|
265
|
+
: record.relativeDir,
|
|
266
|
+
};
|
|
267
|
+
default:
|
|
268
|
+
return target;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizeCommandGateRuns(runs: unknown): unknown {
|
|
273
|
+
if (!Array.isArray(runs)) {
|
|
274
|
+
return runs;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return runs.map((run) => {
|
|
278
|
+
const record = asRecord(run);
|
|
279
|
+
if (!record) {
|
|
280
|
+
return run;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
...record,
|
|
285
|
+
command: typeof record.command === "string" ? record.command.trim() : record.command,
|
|
286
|
+
target: normalizeCommandGateRunTarget(record.target),
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function migrateCommandGateConfig(config: unknown): unknown {
|
|
292
|
+
const record = asRecord(config);
|
|
293
|
+
if (!record) {
|
|
294
|
+
return config;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (record.enabled === false) {
|
|
298
|
+
return { enabled: false };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (record.enabled !== true) {
|
|
302
|
+
return config;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (Array.isArray(record.runs)) {
|
|
306
|
+
return {
|
|
307
|
+
enabled: true,
|
|
308
|
+
runs: normalizeCommandGateRuns(record.runs),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const legacyCommand =
|
|
313
|
+
typeof record.command === "string" && record.command.trim().length > 0
|
|
314
|
+
? record.command.trim()
|
|
315
|
+
: null;
|
|
316
|
+
if (legacyCommand) {
|
|
317
|
+
return {
|
|
318
|
+
enabled: true,
|
|
319
|
+
runs: [createAllTargetsRun(legacyCommand)],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return { enabled: true };
|
|
324
|
+
}
|
|
325
|
+
|
|
156
326
|
function legacyGatesFromProfile(
|
|
157
327
|
profileName: string | null,
|
|
158
328
|
legacyTestCommand: string | null,
|
|
@@ -164,7 +334,10 @@ function legacyGatesFromProfile(
|
|
|
164
334
|
}
|
|
165
335
|
|
|
166
336
|
if (legacyTestCommand) {
|
|
167
|
-
gates["test-suite"] = {
|
|
337
|
+
gates["test-suite"] = {
|
|
338
|
+
enabled: true,
|
|
339
|
+
runs: [createAllTargetsRun(legacyTestCommand)],
|
|
340
|
+
};
|
|
168
341
|
}
|
|
169
342
|
|
|
170
343
|
return Object.keys(gates).length > 0 ? gates : null;
|
|
@@ -207,6 +380,12 @@ function migrateConfig(config: Record<string, unknown>): Record<string, unknown>
|
|
|
207
380
|
if (gates) {
|
|
208
381
|
// Strip legacy ai-review gate — removed from the schema in the checks/review split.
|
|
209
382
|
delete gates["ai-review"];
|
|
383
|
+
|
|
384
|
+
for (const gateId of COMMAND_GATE_IDS) {
|
|
385
|
+
if (gateId in gates) {
|
|
386
|
+
gates[gateId] = migrateCommandGateConfig(gates[gateId]);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
210
389
|
}
|
|
211
390
|
if (!gates || Object.keys(gates).length === 0) {
|
|
212
391
|
const legacyGates = legacyGatesFromProfile(legacyProfile, legacyTestCommand);
|
|
@@ -214,7 +393,10 @@ function migrateConfig(config: Record<string, unknown>): Record<string, unknown>
|
|
|
214
393
|
quality.gates = legacyGates as unknown as Record<string, unknown>;
|
|
215
394
|
}
|
|
216
395
|
} else if (legacyTestCommand && !("test-suite" in gates)) {
|
|
217
|
-
gates["test-suite"] = {
|
|
396
|
+
gates["test-suite"] = {
|
|
397
|
+
enabled: true,
|
|
398
|
+
runs: [createAllTargetsRun(legacyTestCommand)],
|
|
399
|
+
};
|
|
218
400
|
quality.gates = gates;
|
|
219
401
|
}
|
|
220
402
|
migrated.quality = quality;
|
|
@@ -226,17 +408,14 @@ function migrateConfig(config: Record<string, unknown>): Record<string, unknown>
|
|
|
226
408
|
|
|
227
409
|
function mergeConfigLayers(
|
|
228
410
|
defaults: SupipowersConfig,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
): Record<string, unknown> {
|
|
411
|
+
...layers: Array<Record<string, unknown> | null>
|
|
412
|
+
): Record<string, unknown> {
|
|
232
413
|
let merged = structuredClone(defaults) as unknown as Record<string, unknown>;
|
|
233
414
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (projectData) {
|
|
239
|
-
merged = applyConfigOverride(merged, projectData);
|
|
415
|
+
for (const layer of layers) {
|
|
416
|
+
if (layer) {
|
|
417
|
+
merged = applyConfigOverride(merged, layer);
|
|
418
|
+
}
|
|
240
419
|
}
|
|
241
420
|
|
|
242
421
|
// The config schema changed without a version bump, so normalize known
|
|
@@ -246,14 +425,112 @@ function mergeConfigLayers(
|
|
|
246
425
|
return merged;
|
|
247
426
|
}
|
|
248
427
|
|
|
428
|
+
interface ResolvedConfigLayerRead extends ResolvedConfigLayer {
|
|
429
|
+
readResult: { data: Record<string, unknown> | null; error: ConfigParseError | null };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function readConfigLayers(
|
|
433
|
+
paths: PlatformPaths,
|
|
434
|
+
cwd: string,
|
|
435
|
+
options?: ConfigResolutionOptions,
|
|
436
|
+
): ResolvedConfigLayerRead[] {
|
|
437
|
+
return getInspectionScopes().map((scope) => {
|
|
438
|
+
const filePath = getConfigPath(paths, cwd, scope, options);
|
|
439
|
+
const readResult = readJsonFile(scope, filePath);
|
|
440
|
+
return {
|
|
441
|
+
scope,
|
|
442
|
+
path: filePath,
|
|
443
|
+
readResult: {
|
|
444
|
+
...readResult,
|
|
445
|
+
data: omitMeaninglessUltraPlanFromGlobalRead(readResult.data, scope),
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function collectCommandGateSelectorValidationErrors(
|
|
452
|
+
config: Record<string, unknown>,
|
|
453
|
+
repoRoot: string,
|
|
454
|
+
): ConfigValidationError[] {
|
|
455
|
+
const quality = asRecord(config.quality);
|
|
456
|
+
const gates = asRecord(quality?.gates);
|
|
457
|
+
if (!gates) {
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const workspaceRelativeDirs = new Set(
|
|
462
|
+
discoverWorkspaceTargets(repoRoot, resolvePackageManager(repoRoot).id)
|
|
463
|
+
.filter((target) => target.kind === "workspace")
|
|
464
|
+
.map((target) => target.relativeDir),
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const errors: ConfigValidationError[] = [];
|
|
468
|
+
for (const gateId of COMMAND_GATE_IDS) {
|
|
469
|
+
const gateConfig = asRecord(gates[gateId]);
|
|
470
|
+
if (!gateConfig || gateConfig.enabled !== true || !Array.isArray(gateConfig.runs)) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
gateConfig.runs.forEach((run, index) => {
|
|
475
|
+
const target = asRecord(asRecord(run)?.target);
|
|
476
|
+
if (target?.scope !== "workspace") {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const relativeDir =
|
|
481
|
+
typeof target.relativeDir === "string"
|
|
482
|
+
? normalizeWorkspaceRelativePath(target.relativeDir)
|
|
483
|
+
: null;
|
|
484
|
+
if (relativeDir && workspaceRelativeDirs.has(relativeDir)) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
errors.push({
|
|
489
|
+
path: `quality.gates.${gateId}.runs.${index}.target.relativeDir`,
|
|
490
|
+
message: `Unknown workspace target "${String(target.relativeDir)}"`,
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return errors;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function collectScopeValidationErrors(
|
|
499
|
+
config: Record<string, unknown> | null,
|
|
500
|
+
scope: ConfigScope,
|
|
501
|
+
): ConfigValidationError[] {
|
|
502
|
+
if (scope !== "global" || !hasMeaningfulUltraPlanPolicy(config)) {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return [
|
|
507
|
+
{
|
|
508
|
+
path: "ultraplan",
|
|
509
|
+
message: "Only repository config may define ultraplan",
|
|
510
|
+
},
|
|
511
|
+
];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
function collectAllValidationErrors(
|
|
516
|
+
config: Record<string, unknown>,
|
|
517
|
+
repoRoot: string,
|
|
518
|
+
): ConfigValidationError[] {
|
|
519
|
+
return [
|
|
520
|
+
...collectConfigValidationErrors(config),
|
|
521
|
+
...collectCommandGateSelectorValidationErrors(config, repoRoot),
|
|
522
|
+
];
|
|
523
|
+
}
|
|
524
|
+
|
|
249
525
|
function inspectScopeConfig(
|
|
250
526
|
paths: PlatformPaths,
|
|
251
527
|
cwd: string,
|
|
252
528
|
scope: ConfigScope,
|
|
529
|
+
options?: ConfigResolutionOptions,
|
|
253
530
|
): ScopedConfigInspection {
|
|
254
|
-
const
|
|
531
|
+
const { repoRoot } = resolveConfigContext(cwd, options);
|
|
532
|
+
const filePath = getConfigPath(paths, cwd, scope, options);
|
|
255
533
|
const readResult = readJsonFile(scope, filePath);
|
|
256
|
-
|
|
257
534
|
if (readResult.error) {
|
|
258
535
|
return {
|
|
259
536
|
scope,
|
|
@@ -269,11 +546,12 @@ function inspectScopeConfig(
|
|
|
269
546
|
}
|
|
270
547
|
|
|
271
548
|
const hasOwnQualityGates = !!readResult.data && hasOwnNestedProperty(readResult.data, "quality", "gates");
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
549
|
+
const scopeData = omitMeaninglessUltraPlanFromGlobalRead(readResult.data, scope);
|
|
550
|
+
const mergedConfig = mergeConfigLayers(DEFAULT_CONFIG, scopeData);
|
|
551
|
+
const validationErrors = [
|
|
552
|
+
...collectAllValidationErrors(mergedConfig, repoRoot),
|
|
553
|
+
...collectScopeValidationErrors(readResult.data, scope),
|
|
554
|
+
];
|
|
277
555
|
const qualityGateValidationErrors = hasOwnQualityGates
|
|
278
556
|
? validationErrors.filter((error) => error.path === "quality.gates" || error.path.startsWith("quality.gates."))
|
|
279
557
|
: [];
|
|
@@ -295,6 +573,7 @@ function inspectScopeConfig(
|
|
|
295
573
|
};
|
|
296
574
|
}
|
|
297
575
|
|
|
576
|
+
|
|
298
577
|
function removeQualityGatesFromRecord(config: Record<string, unknown>): Record<string, unknown> {
|
|
299
578
|
const next = structuredClone(config) as Record<string, unknown>;
|
|
300
579
|
const quality = asRecord(next.quality);
|
|
@@ -317,14 +596,21 @@ function writeRawConfigFile(filePath: string, config: Record<string, unknown>):
|
|
|
317
596
|
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
|
|
318
597
|
}
|
|
319
598
|
|
|
599
|
+
export function inspectConfigScopes(
|
|
600
|
+
paths: PlatformPaths,
|
|
601
|
+
cwd: string,
|
|
602
|
+
options?: ConfigResolutionOptions,
|
|
603
|
+
): ScopedConfigInspection[] {
|
|
604
|
+
return getInspectionScopes().map((scope) => inspectScopeConfig(paths, cwd, scope, options));
|
|
605
|
+
}
|
|
606
|
+
|
|
320
607
|
export function inspectQualityGateRecovery(
|
|
321
608
|
paths: PlatformPaths,
|
|
322
609
|
cwd: string,
|
|
323
|
-
|
|
610
|
+
options?: ConfigResolutionOptions,
|
|
611
|
+
): QualityGateRecoveryInspection {
|
|
324
612
|
return {
|
|
325
|
-
scopes: (
|
|
326
|
-
inspectScopeConfig(paths, cwd, scope),
|
|
327
|
-
),
|
|
613
|
+
scopes: inspectConfigScopes(paths, cwd, options),
|
|
328
614
|
};
|
|
329
615
|
}
|
|
330
616
|
|
|
@@ -333,8 +619,9 @@ export function writeQualityGatesConfig(
|
|
|
333
619
|
cwd: string,
|
|
334
620
|
scope: ConfigScope,
|
|
335
621
|
gates: QualityGatesConfig,
|
|
336
|
-
|
|
337
|
-
|
|
622
|
+
options?: ConfigResolutionOptions,
|
|
623
|
+
): void {
|
|
624
|
+
const configPath = getConfigPath(paths, cwd, scope, options);
|
|
338
625
|
const current = readJsonFile(scope, configPath);
|
|
339
626
|
if (current.error) {
|
|
340
627
|
throw new Error(`${scope} config ${configPath}: ${current.error.message}`);
|
|
@@ -355,8 +642,9 @@ export function removeQualityGatesConfig(
|
|
|
355
642
|
paths: PlatformPaths,
|
|
356
643
|
cwd: string,
|
|
357
644
|
scope: ConfigScope,
|
|
358
|
-
|
|
359
|
-
|
|
645
|
+
options?: ConfigResolutionOptions,
|
|
646
|
+
): boolean {
|
|
647
|
+
const configPath = getConfigPath(paths, cwd, scope, options);
|
|
360
648
|
const current = readJsonFile(scope, configPath);
|
|
361
649
|
if (current.error) {
|
|
362
650
|
throw new Error(`${scope} config ${configPath}: ${current.error.message}`);
|
|
@@ -369,10 +657,14 @@ export function removeQualityGatesConfig(
|
|
|
369
657
|
return true;
|
|
370
658
|
}
|
|
371
659
|
|
|
660
|
+
function describeConfigSource(source: ConfigParseError["source"]): string {
|
|
661
|
+
return source === "root" ? "repository" : source;
|
|
662
|
+
}
|
|
663
|
+
|
|
372
664
|
export function formatConfigErrors(result: InspectionLoadResult): string {
|
|
373
665
|
const messages = [
|
|
374
666
|
...result.parseErrors.map(
|
|
375
|
-
(error) => `${error.source} config ${error.path}: ${error.message}`,
|
|
667
|
+
(error) => `${describeConfigSource(error.source)} config ${error.path}: ${error.message}`,
|
|
376
668
|
),
|
|
377
669
|
...result.validationErrors.map(
|
|
378
670
|
(error) => `${error.path}: ${error.message}`,
|
|
@@ -382,14 +674,21 @@ export function formatConfigErrors(result: InspectionLoadResult): string {
|
|
|
382
674
|
return messages.join("\n") || "Unknown config error";
|
|
383
675
|
}
|
|
384
676
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
677
|
+
function buildInspectionLoadResult(
|
|
678
|
+
layers: ResolvedConfigLayerRead[],
|
|
679
|
+
repoRoot: string,
|
|
680
|
+
): InspectionLoadResult {
|
|
681
|
+
const mergedConfig = mergeConfigLayers(
|
|
682
|
+
DEFAULT_CONFIG,
|
|
683
|
+
...layers.map((layer) => layer.readResult.data),
|
|
391
684
|
);
|
|
392
|
-
const
|
|
685
|
+
const parseErrors = layers
|
|
686
|
+
.map((layer) => layer.readResult.error)
|
|
687
|
+
.filter((error): error is ConfigParseError => error !== null);
|
|
688
|
+
const validationErrors = [
|
|
689
|
+
...collectAllValidationErrors(mergedConfig, repoRoot),
|
|
690
|
+
...layers.flatMap((layer) => collectScopeValidationErrors(layer.readResult.data, layer.scope)),
|
|
691
|
+
];
|
|
393
692
|
|
|
394
693
|
return {
|
|
395
694
|
mergedConfig,
|
|
@@ -402,9 +701,38 @@ export function inspectConfig(paths: PlatformPaths, cwd: string): InspectionLoad
|
|
|
402
701
|
};
|
|
403
702
|
}
|
|
404
703
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
704
|
+
export function inspectConfigAtScope(
|
|
705
|
+
paths: PlatformPaths,
|
|
706
|
+
cwd: string,
|
|
707
|
+
scope: ConfigScope,
|
|
708
|
+
options?: ConfigResolutionOptions,
|
|
709
|
+
): InspectionLoadResult {
|
|
710
|
+
const { repoRoot } = resolveConfigContext(cwd, options);
|
|
711
|
+
const layers = readConfigLayers(paths, cwd, options).filter((layer) =>
|
|
712
|
+
scope === "global"
|
|
713
|
+
? layer.scope === "global"
|
|
714
|
+
: layer.scope === "global" || layer.scope === "root",
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
return buildInspectionLoadResult(layers, repoRoot);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function inspectConfig(
|
|
721
|
+
paths: PlatformPaths,
|
|
722
|
+
cwd: string,
|
|
723
|
+
options?: ConfigResolutionOptions,
|
|
724
|
+
): InspectionLoadResult {
|
|
725
|
+
const { repoRoot } = resolveConfigContext(cwd, options);
|
|
726
|
+
return buildInspectionLoadResult(readConfigLayers(paths, cwd, options), repoRoot);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** Load config with global -> repository layering over defaults. */
|
|
730
|
+
export function loadConfig(
|
|
731
|
+
paths: PlatformPaths,
|
|
732
|
+
cwd: string,
|
|
733
|
+
options?: ConfigResolutionOptions,
|
|
734
|
+
): SupipowersConfig {
|
|
735
|
+
const result = inspectConfig(paths, cwd, options);
|
|
408
736
|
|
|
409
737
|
if (!result.effectiveConfig) {
|
|
410
738
|
throw new Error(formatConfigErrors(result));
|
|
@@ -413,43 +741,103 @@ export function loadConfig(paths: PlatformPaths, cwd: string): SupipowersConfig
|
|
|
413
741
|
return result.effectiveConfig;
|
|
414
742
|
}
|
|
415
743
|
|
|
416
|
-
|
|
417
|
-
|
|
744
|
+
// Global scope never persists UltraPlan policy; writes only keep the shared config surface it may own.
|
|
745
|
+
function omitUnsupportedUltraPlanFromGlobalWrite(
|
|
746
|
+
config: Record<string, unknown>,
|
|
747
|
+
scope: ConfigScope,
|
|
748
|
+
): Record<string, unknown> {
|
|
749
|
+
if (scope !== "global") {
|
|
750
|
+
return config;
|
|
751
|
+
}
|
|
418
752
|
|
|
419
|
-
|
|
753
|
+
const { ultraplan: _ultraplan, ...scoped } = config;
|
|
754
|
+
return scoped;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function prepareScopedConfigWrite(
|
|
758
|
+
fullConfig: unknown,
|
|
759
|
+
scopeData: Record<string, unknown>,
|
|
760
|
+
repoRoot: string,
|
|
761
|
+
scope: ConfigScope,
|
|
762
|
+
): Record<string, unknown> {
|
|
763
|
+
const persistedScopeData = omitUnsupportedUltraPlanFromGlobalWrite(scopeData, scope);
|
|
764
|
+
const validatedScopeData = scope === "global" && !hasMeaningfulUltraPlanPolicy(scopeData)
|
|
765
|
+
? persistedScopeData
|
|
766
|
+
: scopeData;
|
|
767
|
+
|
|
768
|
+
assertValidConfig(fullConfig, repoRoot, scope, validatedScopeData);
|
|
769
|
+
return persistedScopeData;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function assertValidConfig(
|
|
773
|
+
data: unknown,
|
|
774
|
+
repoRoot: string,
|
|
775
|
+
scope?: ConfigScope,
|
|
776
|
+
scopeData?: Record<string, unknown> | null,
|
|
777
|
+
): void {
|
|
778
|
+
const record = asRecord(data);
|
|
779
|
+
const validationErrors = record ? collectAllValidationErrors(record, repoRoot) : collectConfigValidationErrors(data);
|
|
780
|
+
const scopeValidationErrors = scope ? collectScopeValidationErrors(scopeData ?? null, scope) : [];
|
|
781
|
+
|
|
782
|
+
if (validationErrors.length === 0 && scopeValidationErrors.length === 0) {
|
|
420
783
|
return;
|
|
421
784
|
}
|
|
422
785
|
|
|
423
786
|
throw new Error(
|
|
424
|
-
validationErrors
|
|
787
|
+
[...validationErrors, ...scopeValidationErrors]
|
|
425
788
|
.map((error) => `${error.path}: ${error.message}`)
|
|
426
789
|
.join("\n"),
|
|
427
790
|
);
|
|
428
791
|
}
|
|
429
792
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
793
|
+
/** Save a full config document to the selected scope. */
|
|
794
|
+
export function saveConfig(
|
|
795
|
+
paths: PlatformPaths,
|
|
796
|
+
cwd: string,
|
|
797
|
+
config: SupipowersConfig,
|
|
798
|
+
options?: ConfigMutationOptions,
|
|
799
|
+
): void {
|
|
800
|
+
const { repoRoot } = resolveConfigContext(cwd, options);
|
|
801
|
+
const scope = options?.scope ?? "root";
|
|
802
|
+
const rawConfig = prepareScopedConfigWrite(
|
|
803
|
+
config,
|
|
804
|
+
config as unknown as Record<string, unknown>,
|
|
805
|
+
repoRoot,
|
|
806
|
+
scope,
|
|
807
|
+
);
|
|
808
|
+
const configPath = getConfigPath(paths, cwd, scope, options);
|
|
436
809
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
437
|
-
fs.writeFileSync(configPath, JSON.stringify(
|
|
810
|
+
fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
|
|
438
811
|
}
|
|
439
812
|
|
|
440
|
-
/** Update specific config fields
|
|
813
|
+
/** Update specific config fields in the selected raw scope. */
|
|
441
814
|
export function updateConfig(
|
|
442
815
|
paths: PlatformPaths,
|
|
443
816
|
cwd: string,
|
|
444
817
|
updates: Record<string, unknown>,
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
818
|
+
options?: ConfigMutationOptions,
|
|
819
|
+
): SupipowersConfig {
|
|
820
|
+
const { repoRoot } = resolveConfigContext(cwd, options);
|
|
821
|
+
const scope = options?.scope ?? "root";
|
|
822
|
+
const configPath = getConfigPath(paths, cwd, scope, options);
|
|
823
|
+
const current = readJsonFile(scope, configPath);
|
|
824
|
+
if (current.error) {
|
|
825
|
+
throw new Error(`${scope} config ${configPath}: ${current.error.message}`);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const nextScopeData = applyConfigOverride(
|
|
829
|
+
current.data ? structuredClone(current.data) as Record<string, unknown> : {},
|
|
449
830
|
updates,
|
|
450
831
|
);
|
|
451
|
-
assertValidConfig(updated);
|
|
452
832
|
|
|
453
|
-
|
|
454
|
-
|
|
833
|
+
const scopedLayerData = omitUnsupportedUltraPlanFromGlobalWrite(nextScopeData, scope);
|
|
834
|
+
const layers = readConfigLayers(paths, cwd, options);
|
|
835
|
+
const mergedConfig = mergeConfigLayers(
|
|
836
|
+
DEFAULT_CONFIG,
|
|
837
|
+
...layers.map((layer) => layer.scope === scope ? scopedLayerData : layer.readResult.data),
|
|
838
|
+
);
|
|
839
|
+
const rawConfig = prepareScopedConfigWrite(mergedConfig, nextScopeData, repoRoot, scope);
|
|
840
|
+
|
|
841
|
+
writeRawConfigFile(configPath, rawConfig);
|
|
842
|
+
return mergedConfig as unknown as SupipowersConfig;
|
|
455
843
|
}
|