claude-code-modes 0.1.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/LICENSE +21 -0
- package/README.md +304 -0
- package/package.json +35 -0
- package/prompts/axis/agency/autonomous.md +9 -0
- package/prompts/axis/agency/collaborative.md +9 -0
- package/prompts/axis/agency/surgical.md +9 -0
- package/prompts/axis/quality/architect.md +24 -0
- package/prompts/axis/quality/minimal.md +19 -0
- package/prompts/axis/quality/pragmatic.md +21 -0
- package/prompts/axis/scope/adjacent.md +9 -0
- package/prompts/axis/scope/narrow.md +9 -0
- package/prompts/axis/scope/unrestricted.md +8 -0
- package/prompts/base/actions.md +9 -0
- package/prompts/base/base.json +12 -0
- package/prompts/base/doing-tasks.md +12 -0
- package/prompts/base/env.md +16 -0
- package/prompts/base/intro.md +5 -0
- package/prompts/base/session-guidance.md +7 -0
- package/prompts/base/system.md +7 -0
- package/prompts/base/tone.md +5 -0
- package/prompts/base/tools.md +10 -0
- package/prompts/chill/actions.md +16 -0
- package/prompts/chill/base.json +8 -0
- package/prompts/chill/core.md +60 -0
- package/prompts/chill/env.md +15 -0
- package/prompts/chill/tools.md +12 -0
- package/prompts/modifiers/context-pacing.md +14 -0
- package/prompts/modifiers/readonly.md +10 -0
- package/src/args.ts +118 -0
- package/src/assemble.ts +187 -0
- package/src/build-prompt.ts +119 -0
- package/src/cli.ts +132 -0
- package/src/config-cli.ts +320 -0
- package/src/config.ts +300 -0
- package/src/embedded-prompts.ts +366 -0
- package/src/env.ts +64 -0
- package/src/inspect.ts +315 -0
- package/src/presets.ts +39 -0
- package/src/resolve.ts +278 -0
- package/src/types.ts +99 -0
- package/src/usage.ts +51 -0
package/src/inspect.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { parseCliArgs } from "./args.js";
|
|
4
|
+
import { loadConfig, resolveConfigPath, type LoadedConfig } from "./config.js";
|
|
5
|
+
import { resolveConfig } from "./resolve.js";
|
|
6
|
+
import { getFragmentOrder } from "./assemble.js";
|
|
7
|
+
import { detectEnv, buildTemplateVars } from "./env.js";
|
|
8
|
+
import type { TemplateVars } from "./types.js";
|
|
9
|
+
|
|
10
|
+
const PROVENANCE_VALUES = ["built-in", "config-defined", "cli-path"] as const;
|
|
11
|
+
type Provenance = (typeof PROVENANCE_VALUES)[number];
|
|
12
|
+
|
|
13
|
+
const WARNING_KINDS = ["outside-project", "non-md", "missing-file", "suspicious-path"] as const;
|
|
14
|
+
type WarningKind = (typeof WARNING_KINDS)[number];
|
|
15
|
+
|
|
16
|
+
interface FragmentEntry {
|
|
17
|
+
index: number;
|
|
18
|
+
path: string;
|
|
19
|
+
resolvedPath: string;
|
|
20
|
+
provenance: Provenance;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Warning {
|
|
24
|
+
fragmentIndex: number;
|
|
25
|
+
fragmentPath: string;
|
|
26
|
+
kind: WarningKind;
|
|
27
|
+
detail: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ConfigSource {
|
|
31
|
+
loaded: boolean;
|
|
32
|
+
path: string | null;
|
|
33
|
+
scope: "project" | "global" | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface InspectResult {
|
|
37
|
+
baseName: string;
|
|
38
|
+
configSource: ConfigSource;
|
|
39
|
+
fragments: FragmentEntry[];
|
|
40
|
+
fragmentContents: Map<number, string | null>;
|
|
41
|
+
warnings: Warning[];
|
|
42
|
+
templateVars: TemplateVars;
|
|
43
|
+
verbose: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SUSPICIOUS_PATTERN = /\.(ssh|env|gnupg|aws|kube|docker|pgpass)|\.npmrc|\.netrc|id_rsa|id_ed25519|id_ecdsa|credentials|secret|token|password|private/i;
|
|
47
|
+
|
|
48
|
+
/** Builds a Set of all absolute paths that config definitions could resolve to. */
|
|
49
|
+
function collectConfigDefinedPaths(loadedConfig: LoadedConfig | null): Set<string> {
|
|
50
|
+
const paths = new Set<string>();
|
|
51
|
+
if (!loadedConfig) return paths;
|
|
52
|
+
|
|
53
|
+
const { config, configDir } = loadedConfig;
|
|
54
|
+
|
|
55
|
+
// Modifier definitions
|
|
56
|
+
if (config.modifiers) {
|
|
57
|
+
for (const val of Object.values(config.modifiers)) {
|
|
58
|
+
paths.add(resolveConfigPath(configDir, val));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Axis definitions
|
|
63
|
+
if (config.axes) {
|
|
64
|
+
for (const axisName of ["agency", "quality", "scope"] as const) {
|
|
65
|
+
const axisMap = config.axes[axisName];
|
|
66
|
+
if (axisMap) {
|
|
67
|
+
for (const val of Object.values(axisMap)) {
|
|
68
|
+
paths.add(resolveConfigPath(configDir, val));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Preset modifier lists
|
|
75
|
+
if (config.presets) {
|
|
76
|
+
for (const preset of Object.values(config.presets)) {
|
|
77
|
+
if (preset.modifiers) {
|
|
78
|
+
for (const mod of preset.modifiers) {
|
|
79
|
+
// Modifier names resolve through config.modifiers; raw paths resolve against configDir
|
|
80
|
+
if (config.modifiers && mod in config.modifiers) {
|
|
81
|
+
paths.add(resolveConfigPath(configDir, config.modifiers[mod]));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// defaultModifiers
|
|
89
|
+
if (config.defaultModifiers) {
|
|
90
|
+
for (const mod of config.defaultModifiers) {
|
|
91
|
+
if (config.modifiers && mod in config.modifiers) {
|
|
92
|
+
paths.add(resolveConfigPath(configDir, config.modifiers[mod]));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return paths;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Classifies a single fragment path as built-in, config-defined, or cli-path. */
|
|
101
|
+
function classifyProvenance(
|
|
102
|
+
fragPath: string,
|
|
103
|
+
configDefinedPaths: Set<string>,
|
|
104
|
+
): Provenance {
|
|
105
|
+
if (!isAbsolute(fragPath)) return "built-in";
|
|
106
|
+
if (configDefinedPaths.has(fragPath)) return "config-defined";
|
|
107
|
+
return "cli-path";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Detects warnings for the fragment manifest. */
|
|
111
|
+
function detectWarnings(
|
|
112
|
+
fragments: FragmentEntry[],
|
|
113
|
+
promptsDir: string,
|
|
114
|
+
): Warning[] {
|
|
115
|
+
const warnings: Warning[] = [];
|
|
116
|
+
const cwd = process.cwd();
|
|
117
|
+
const resolvedPromptsDir = resolve(promptsDir);
|
|
118
|
+
|
|
119
|
+
for (const frag of fragments) {
|
|
120
|
+
if (frag.provenance === "built-in") {
|
|
121
|
+
// Only check existence for built-in fragments
|
|
122
|
+
if (!existsSync(frag.resolvedPath)) {
|
|
123
|
+
warnings.push({
|
|
124
|
+
fragmentIndex: frag.index,
|
|
125
|
+
fragmentPath: frag.path,
|
|
126
|
+
kind: "missing-file",
|
|
127
|
+
detail: `file not found: ${frag.resolvedPath}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Custom fragments: check all warning types
|
|
134
|
+
// Use cwd + "/" to prevent prefix attacks (e.g. /project-evil matching /project)
|
|
135
|
+
const inProject = frag.resolvedPath === cwd || frag.resolvedPath.startsWith(cwd + "/");
|
|
136
|
+
const inPrompts = frag.resolvedPath === resolvedPromptsDir || frag.resolvedPath.startsWith(resolvedPromptsDir + "/");
|
|
137
|
+
if (!inProject && !inPrompts) {
|
|
138
|
+
warnings.push({
|
|
139
|
+
fragmentIndex: frag.index,
|
|
140
|
+
fragmentPath: frag.resolvedPath,
|
|
141
|
+
kind: "outside-project",
|
|
142
|
+
detail: `resolves outside project directory`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!frag.resolvedPath.endsWith(".md")) {
|
|
147
|
+
warnings.push({
|
|
148
|
+
fragmentIndex: frag.index,
|
|
149
|
+
fragmentPath: frag.resolvedPath,
|
|
150
|
+
kind: "non-md",
|
|
151
|
+
detail: `non-.md extension`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!existsSync(frag.resolvedPath)) {
|
|
156
|
+
warnings.push({
|
|
157
|
+
fragmentIndex: frag.index,
|
|
158
|
+
fragmentPath: frag.resolvedPath,
|
|
159
|
+
kind: "missing-file",
|
|
160
|
+
detail: `file not found`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (SUSPICIOUS_PATTERN.test(frag.resolvedPath)) {
|
|
165
|
+
warnings.push({
|
|
166
|
+
fragmentIndex: frag.index,
|
|
167
|
+
fragmentPath: frag.resolvedPath,
|
|
168
|
+
kind: "suspicious-path",
|
|
169
|
+
detail: `references potentially sensitive path`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return warnings;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Determines config source info from LoadedConfig. */
|
|
178
|
+
function describeConfigSource(loadedConfig: LoadedConfig | null): ConfigSource {
|
|
179
|
+
if (!loadedConfig) return { loaded: false, path: null, scope: null };
|
|
180
|
+
|
|
181
|
+
const { configDir } = loadedConfig;
|
|
182
|
+
const cwd = process.cwd();
|
|
183
|
+
|
|
184
|
+
if (configDir === cwd) {
|
|
185
|
+
return { loaded: true, path: resolve(configDir, ".claude-mode.json"), scope: "project" };
|
|
186
|
+
}
|
|
187
|
+
return { loaded: true, path: resolve(configDir, "config.json"), scope: "global" };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Formats an InspectResult as a human-readable string. */
|
|
191
|
+
function formatInspectOutput(result: InspectResult): string {
|
|
192
|
+
const lines: string[] = [];
|
|
193
|
+
|
|
194
|
+
// Warning banner — top of output when warnings exist
|
|
195
|
+
if (result.warnings.length > 0) {
|
|
196
|
+
const count = result.warnings.length;
|
|
197
|
+
lines.push(`!! ${count} warning${count > 1 ? "s" : ""} found -- review before running`);
|
|
198
|
+
lines.push("");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Config section
|
|
202
|
+
lines.push("=== Config ===");
|
|
203
|
+
if (!result.configSource.loaded) {
|
|
204
|
+
lines.push("No config file found.");
|
|
205
|
+
} else {
|
|
206
|
+
const label = result.configSource.scope === "project"
|
|
207
|
+
? ".claude-mode.json (project)"
|
|
208
|
+
: "config.json (global)";
|
|
209
|
+
lines.push(`Loaded: ${label}`);
|
|
210
|
+
lines.push(` Path: ${result.configSource.path}`);
|
|
211
|
+
}
|
|
212
|
+
lines.push("");
|
|
213
|
+
|
|
214
|
+
// Base section
|
|
215
|
+
lines.push("=== Base ===");
|
|
216
|
+
lines.push(`Active: ${result.baseName}`);
|
|
217
|
+
lines.push("");
|
|
218
|
+
|
|
219
|
+
// Warnings section — before fragments so issues surface early
|
|
220
|
+
lines.push("=== Warnings ===");
|
|
221
|
+
if (result.warnings.length === 0) {
|
|
222
|
+
lines.push("(none)");
|
|
223
|
+
} else {
|
|
224
|
+
for (const w of result.warnings) {
|
|
225
|
+
lines.push(`[!] #${w.fragmentIndex} ${w.detail}: ${w.fragmentPath}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
lines.push("");
|
|
229
|
+
|
|
230
|
+
// Fragments section
|
|
231
|
+
lines.push("=== Fragments ===");
|
|
232
|
+
if (result.verbose) {
|
|
233
|
+
for (const frag of result.fragments) {
|
|
234
|
+
const displayPath = frag.provenance === "built-in" ? frag.path : frag.resolvedPath;
|
|
235
|
+
lines.push("");
|
|
236
|
+
lines.push(`--- #${frag.index} [${frag.provenance}] ${displayPath} ---`);
|
|
237
|
+
const content = result.fragmentContents.get(frag.index);
|
|
238
|
+
if (content === null || content === undefined) {
|
|
239
|
+
lines.push("(file not found)");
|
|
240
|
+
} else {
|
|
241
|
+
lines.push(content.trim());
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
const indexWidth = String(result.fragments.length).length;
|
|
246
|
+
lines.push(`${" ".repeat(indexWidth + 1)} Provenance Path`);
|
|
247
|
+
for (const frag of result.fragments) {
|
|
248
|
+
const idx = String(frag.index).padStart(indexWidth);
|
|
249
|
+
const prov = frag.provenance.padEnd(16);
|
|
250
|
+
const displayPath = frag.provenance === "built-in" ? frag.path : frag.resolvedPath;
|
|
251
|
+
lines.push(`${idx} ${prov} ${displayPath}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
lines.push("");
|
|
255
|
+
|
|
256
|
+
// Template variables section
|
|
257
|
+
lines.push("=== Template Variables ===");
|
|
258
|
+
const maxKeyLen = Math.max(...Object.keys(result.templateVars).map((k) => k.length));
|
|
259
|
+
for (const [key, value] of Object.entries(result.templateVars)) {
|
|
260
|
+
const displayValue = value.includes("\n") ? "(multiline)" : value;
|
|
261
|
+
lines.push(`${key.padEnd(maxKeyLen)} = ${displayValue}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return lines.join("\n") + "\n";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Reads a fragment file, returning its content or null if missing. */
|
|
268
|
+
function readFragmentContent(resolvedPath: string): string | null {
|
|
269
|
+
try {
|
|
270
|
+
return readFileSync(resolvedPath, "utf8");
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Runs the inspect subcommand. Accepts argv after "inspect" has been stripped. */
|
|
277
|
+
export function runInspectCommand(argv: string[], promptsDir: string): void {
|
|
278
|
+
const verbose = argv.includes("--print");
|
|
279
|
+
const filteredArgv = argv.filter((a) => a !== "--print");
|
|
280
|
+
|
|
281
|
+
const parsed = parseCliArgs(filteredArgv);
|
|
282
|
+
const loadedConfig = loadConfig();
|
|
283
|
+
const config = resolveConfig(parsed, loadedConfig);
|
|
284
|
+
|
|
285
|
+
const env = detectEnv();
|
|
286
|
+
const templateVars = buildTemplateVars(env);
|
|
287
|
+
|
|
288
|
+
const fragmentPaths = getFragmentOrder(config, promptsDir);
|
|
289
|
+
const configDefinedPaths = collectConfigDefinedPaths(loadedConfig);
|
|
290
|
+
|
|
291
|
+
const fragments: FragmentEntry[] = fragmentPaths.map((fragPath, i) => {
|
|
292
|
+
const resolvedPath = isAbsolute(fragPath)
|
|
293
|
+
? fragPath
|
|
294
|
+
: resolve(promptsDir, fragPath);
|
|
295
|
+
return {
|
|
296
|
+
index: i + 1,
|
|
297
|
+
path: fragPath,
|
|
298
|
+
resolvedPath,
|
|
299
|
+
provenance: classifyProvenance(fragPath, configDefinedPaths),
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const fragmentContents = new Map<number, string | null>();
|
|
304
|
+
if (verbose) {
|
|
305
|
+
for (const frag of fragments) {
|
|
306
|
+
fragmentContents.set(frag.index, readFragmentContent(frag.resolvedPath));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const warnings = detectWarnings(fragments, promptsDir);
|
|
311
|
+
const configSource = describeConfigSource(loadedConfig);
|
|
312
|
+
|
|
313
|
+
const result: InspectResult = { baseName: config.base, configSource, fragments, fragmentContents, warnings, templateVars, verbose };
|
|
314
|
+
process.stdout.write(formatInspectOutput(result));
|
|
315
|
+
}
|
package/src/presets.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AxisConfig, PresetName } from "./types.js";
|
|
2
|
+
export { isPresetName } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export interface PresetDefinition {
|
|
5
|
+
axes: AxisConfig | null;
|
|
6
|
+
readonly: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const PRESETS: Record<PresetName, PresetDefinition> = {
|
|
10
|
+
"create": {
|
|
11
|
+
axes: { agency: "autonomous", quality: "architect", scope: "unrestricted" },
|
|
12
|
+
readonly: false,
|
|
13
|
+
},
|
|
14
|
+
"extend": {
|
|
15
|
+
axes: { agency: "autonomous", quality: "pragmatic", scope: "adjacent" },
|
|
16
|
+
readonly: false,
|
|
17
|
+
},
|
|
18
|
+
"safe": {
|
|
19
|
+
axes: { agency: "collaborative", quality: "minimal", scope: "narrow" },
|
|
20
|
+
readonly: false,
|
|
21
|
+
},
|
|
22
|
+
"refactor": {
|
|
23
|
+
axes: { agency: "autonomous", quality: "pragmatic", scope: "unrestricted" },
|
|
24
|
+
readonly: false,
|
|
25
|
+
},
|
|
26
|
+
"explore": {
|
|
27
|
+
axes: { agency: "collaborative", quality: "architect", scope: "narrow" },
|
|
28
|
+
readonly: true,
|
|
29
|
+
},
|
|
30
|
+
"none": {
|
|
31
|
+
axes: null,
|
|
32
|
+
readonly: false,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function getPreset(name: PresetName): PresetDefinition {
|
|
37
|
+
return PRESETS[name];
|
|
38
|
+
}
|
|
39
|
+
|
package/src/resolve.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type { ModeConfig } from "./types.js";
|
|
2
|
+
import type { ParsedArgs } from "./args.js";
|
|
3
|
+
import type { LoadedConfig } from "./config.js";
|
|
4
|
+
import { resolveConfigPath } from "./config.js";
|
|
5
|
+
import { getPreset, isPresetName } from "./presets.js";
|
|
6
|
+
import {
|
|
7
|
+
AGENCY_VALUES,
|
|
8
|
+
QUALITY_VALUES,
|
|
9
|
+
SCOPE_VALUES,
|
|
10
|
+
BUILTIN_MODIFIER_NAMES,
|
|
11
|
+
BUILTIN_BASE_NAMES,
|
|
12
|
+
PRESET_NAMES,
|
|
13
|
+
isBuiltinModifier,
|
|
14
|
+
isBuiltinBase,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
import { resolve as pathResolve, isAbsolute } from "node:path";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_AGENCY = "collaborative";
|
|
19
|
+
const DEFAULT_QUALITY = "pragmatic";
|
|
20
|
+
const DEFAULT_SCOPE = "adjacent";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns true if a string looks like a file path rather than a name.
|
|
24
|
+
* Paths contain "/" or "\" or end with ".md".
|
|
25
|
+
*/
|
|
26
|
+
function looksLikeFilePath(value: string): boolean {
|
|
27
|
+
return value.includes("/") || value.includes("\\") || value.endsWith(".md");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolves an axis value to either a built-in name or an absolute path.
|
|
32
|
+
* Resolution order: built-in values → config-defined names → file path heuristic.
|
|
33
|
+
* Throws with descriptive error if unresolvable.
|
|
34
|
+
*/
|
|
35
|
+
function resolveAxisValue(
|
|
36
|
+
raw: string,
|
|
37
|
+
axisName: "agency" | "quality" | "scope",
|
|
38
|
+
builtinValues: readonly string[],
|
|
39
|
+
loadedConfig: LoadedConfig | null,
|
|
40
|
+
): string {
|
|
41
|
+
// 1. Built-in value
|
|
42
|
+
if (builtinValues.includes(raw)) return raw;
|
|
43
|
+
|
|
44
|
+
// 2. Config-defined custom name
|
|
45
|
+
const configAxes = loadedConfig?.config.axes?.[axisName];
|
|
46
|
+
if (configAxes && raw in configAxes) {
|
|
47
|
+
return resolveConfigPath(loadedConfig!.configDir, configAxes[raw]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. File path
|
|
51
|
+
if (looksLikeFilePath(raw)) {
|
|
52
|
+
return isAbsolute(raw) ? raw : pathResolve(raw);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 4. Unknown
|
|
56
|
+
const configHint = loadedConfig
|
|
57
|
+
? ` Config loaded from: ${loadedConfig.configDir}`
|
|
58
|
+
: " No config file found.";
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Unknown --${axisName} value: "${raw}". ` +
|
|
61
|
+
`Must be one of: ${builtinValues.join(", ")}, ` +
|
|
62
|
+
`a name defined in your config, or a file path.${configHint}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolves a modifier reference to either a built-in flag name or an absolute path.
|
|
68
|
+
* Resolution order: built-in modifier name → config-defined name → file path.
|
|
69
|
+
*/
|
|
70
|
+
function resolveModifier(
|
|
71
|
+
raw: string,
|
|
72
|
+
loadedConfig: LoadedConfig | null,
|
|
73
|
+
): { kind: "builtin"; name: string } | { kind: "custom"; path: string } {
|
|
74
|
+
// 1. Built-in modifier
|
|
75
|
+
if (isBuiltinModifier(raw)) {
|
|
76
|
+
return { kind: "builtin", name: raw };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. Config-defined custom modifier
|
|
80
|
+
const configModifiers = loadedConfig?.config.modifiers;
|
|
81
|
+
if (configModifiers && raw in configModifiers) {
|
|
82
|
+
return {
|
|
83
|
+
kind: "custom",
|
|
84
|
+
path: resolveConfigPath(loadedConfig!.configDir, configModifiers[raw]),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 3. File path
|
|
89
|
+
if (looksLikeFilePath(raw)) {
|
|
90
|
+
const absPath = isAbsolute(raw) ? raw : pathResolve(raw);
|
|
91
|
+
return { kind: "custom", path: absPath };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 4. Unknown
|
|
95
|
+
const configHint = loadedConfig
|
|
96
|
+
? ` Config loaded from: ${loadedConfig.configDir}`
|
|
97
|
+
: " No config file found.";
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Unknown modifier: "${raw}". ` +
|
|
100
|
+
`Must be a built-in modifier (${BUILTIN_MODIFIER_NAMES.join(", ")}), ` +
|
|
101
|
+
`a name defined in your config, or a file path.${configHint}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Resolves a list of modifier references, updating flags and custom paths in place. */
|
|
106
|
+
function applyModifiers(
|
|
107
|
+
modifiers: string[],
|
|
108
|
+
loadedConfig: LoadedConfig | null,
|
|
109
|
+
flags: { readonly: boolean; contextPacing: boolean },
|
|
110
|
+
customPaths: string[],
|
|
111
|
+
position: "append" | "prepend",
|
|
112
|
+
): void {
|
|
113
|
+
for (const raw of modifiers) {
|
|
114
|
+
const resolved = resolveModifier(raw, loadedConfig);
|
|
115
|
+
if (resolved.kind === "builtin") {
|
|
116
|
+
if (resolved.name === "readonly") flags.readonly = true;
|
|
117
|
+
if (resolved.name === "context-pacing") flags.contextPacing = true;
|
|
118
|
+
} else {
|
|
119
|
+
if (!customPaths.includes(resolved.path)) {
|
|
120
|
+
if (position === "prepend") customPaths.unshift(resolved.path);
|
|
121
|
+
else customPaths.push(resolved.path);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolves a base reference to a built-in name or absolute directory path.
|
|
129
|
+
* Priority: CLI --base > preset base > config defaultBase > "standard"
|
|
130
|
+
*/
|
|
131
|
+
function resolveBase(
|
|
132
|
+
raw: string | undefined,
|
|
133
|
+
loadedConfig: LoadedConfig | null,
|
|
134
|
+
presetBase: string | undefined,
|
|
135
|
+
): string {
|
|
136
|
+
const config = loadedConfig?.config ?? null;
|
|
137
|
+
|
|
138
|
+
// Priority: CLI --base > preset base > config defaultBase > "standard"
|
|
139
|
+
const value = raw ?? presetBase ?? config?.defaultBase ?? "standard";
|
|
140
|
+
|
|
141
|
+
// 1. Built-in name
|
|
142
|
+
if (isBuiltinBase(value)) return value;
|
|
143
|
+
|
|
144
|
+
// 2. Config-defined name
|
|
145
|
+
const configBases = config?.bases;
|
|
146
|
+
if (configBases && value in configBases) {
|
|
147
|
+
return resolveConfigPath(loadedConfig!.configDir, configBases[value]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 3. Directory path
|
|
151
|
+
if (looksLikeFilePath(value)) {
|
|
152
|
+
return isAbsolute(value) ? value : pathResolve(value);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 4. Unknown
|
|
156
|
+
const configHint = loadedConfig
|
|
157
|
+
? ` Config loaded from: ${loadedConfig.configDir}`
|
|
158
|
+
: " No config file found.";
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Unknown --base value: "${value}". ` +
|
|
161
|
+
`Must be one of: ${BUILTIN_BASE_NAMES.join(", ")}, ` +
|
|
162
|
+
`a name defined in your config, or a directory path.${configHint}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function resolveConfig(
|
|
167
|
+
parsed: ParsedArgs,
|
|
168
|
+
loadedConfig: LoadedConfig | null,
|
|
169
|
+
): ModeConfig {
|
|
170
|
+
const config = loadedConfig?.config ?? null;
|
|
171
|
+
|
|
172
|
+
// Resolve modifiers: defaultModifiers (config) → --modifier flags (CLI)
|
|
173
|
+
const flags = { readonly: parsed.modifiers.readonly, contextPacing: parsed.modifiers.contextPacing };
|
|
174
|
+
const customModifierPaths: string[] = [];
|
|
175
|
+
|
|
176
|
+
// 1. Config defaultModifiers — always applied first
|
|
177
|
+
if (config?.defaultModifiers) {
|
|
178
|
+
applyModifiers(config.defaultModifiers, loadedConfig, flags, customModifierPaths, "append");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 2. CLI --modifier flags — appended after defaults
|
|
182
|
+
applyModifiers(parsed.customModifiers, loadedConfig, flags, customModifierPaths, "append");
|
|
183
|
+
|
|
184
|
+
// Handle "none" preset — resolve base before early return
|
|
185
|
+
if (parsed.preset === "none") {
|
|
186
|
+
const base = resolveBase(parsed.base, loadedConfig, undefined);
|
|
187
|
+
return {
|
|
188
|
+
base,
|
|
189
|
+
axes: null,
|
|
190
|
+
modifiers: {
|
|
191
|
+
readonly: flags.readonly,
|
|
192
|
+
contextPacing: flags.contextPacing,
|
|
193
|
+
custom: customModifierPaths,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let agency: string;
|
|
199
|
+
let quality: string;
|
|
200
|
+
let scope: string;
|
|
201
|
+
let presetBase: string | undefined;
|
|
202
|
+
|
|
203
|
+
if (parsed.preset) {
|
|
204
|
+
// Check built-in presets first, then config presets
|
|
205
|
+
if (isPresetName(parsed.preset)) {
|
|
206
|
+
const preset = getPreset(parsed.preset);
|
|
207
|
+
if (preset.axes === null) throw new Error(`Preset "${parsed.preset}" has null axes`);
|
|
208
|
+
agency = parsed.overrides.agency
|
|
209
|
+
? resolveAxisValue(parsed.overrides.agency, "agency", AGENCY_VALUES, loadedConfig)
|
|
210
|
+
: preset.axes.agency;
|
|
211
|
+
quality = parsed.overrides.quality
|
|
212
|
+
? resolveAxisValue(parsed.overrides.quality, "quality", QUALITY_VALUES, loadedConfig)
|
|
213
|
+
: preset.axes.quality;
|
|
214
|
+
scope = parsed.overrides.scope
|
|
215
|
+
? resolveAxisValue(parsed.overrides.scope, "scope", SCOPE_VALUES, loadedConfig)
|
|
216
|
+
: preset.axes.scope;
|
|
217
|
+
flags.readonly = flags.readonly || preset.readonly;
|
|
218
|
+
// Built-in presets don't specify a base — presetBase stays undefined
|
|
219
|
+
} else if (config?.presets && parsed.preset in config.presets) {
|
|
220
|
+
// Config-defined preset
|
|
221
|
+
const customPreset = config.presets[parsed.preset];
|
|
222
|
+
presetBase = customPreset.base;
|
|
223
|
+
agency = parsed.overrides.agency
|
|
224
|
+
? resolveAxisValue(parsed.overrides.agency, "agency", AGENCY_VALUES, loadedConfig)
|
|
225
|
+
: customPreset.agency
|
|
226
|
+
? resolveAxisValue(customPreset.agency, "agency", AGENCY_VALUES, loadedConfig)
|
|
227
|
+
: DEFAULT_AGENCY;
|
|
228
|
+
quality = parsed.overrides.quality
|
|
229
|
+
? resolveAxisValue(parsed.overrides.quality, "quality", QUALITY_VALUES, loadedConfig)
|
|
230
|
+
: customPreset.quality
|
|
231
|
+
? resolveAxisValue(customPreset.quality, "quality", QUALITY_VALUES, loadedConfig)
|
|
232
|
+
: DEFAULT_QUALITY;
|
|
233
|
+
scope = parsed.overrides.scope
|
|
234
|
+
? resolveAxisValue(parsed.overrides.scope, "scope", SCOPE_VALUES, loadedConfig)
|
|
235
|
+
: customPreset.scope
|
|
236
|
+
? resolveAxisValue(customPreset.scope, "scope", SCOPE_VALUES, loadedConfig)
|
|
237
|
+
: DEFAULT_SCOPE;
|
|
238
|
+
if (customPreset.readonly) flags.readonly = true;
|
|
239
|
+
if (customPreset.contextPacing) flags.contextPacing = true;
|
|
240
|
+
|
|
241
|
+
// Resolve preset's modifiers list — inserted before CLI modifiers
|
|
242
|
+
if (customPreset.modifiers) {
|
|
243
|
+
applyModifiers(customPreset.modifiers, loadedConfig, flags, customModifierPaths, "prepend");
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Unknown preset: "${parsed.preset}". ` +
|
|
248
|
+
`Built-in presets: ${PRESET_NAMES.join(", ")}. ` +
|
|
249
|
+
(config?.presets
|
|
250
|
+
? `Config presets: ${Object.keys(config.presets).join(", ")}.`
|
|
251
|
+
: "No config file found.")
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
// No preset: use overrides with defaults
|
|
256
|
+
agency = parsed.overrides.agency
|
|
257
|
+
? resolveAxisValue(parsed.overrides.agency, "agency", AGENCY_VALUES, loadedConfig)
|
|
258
|
+
: DEFAULT_AGENCY;
|
|
259
|
+
quality = parsed.overrides.quality
|
|
260
|
+
? resolveAxisValue(parsed.overrides.quality, "quality", QUALITY_VALUES, loadedConfig)
|
|
261
|
+
: DEFAULT_QUALITY;
|
|
262
|
+
scope = parsed.overrides.scope
|
|
263
|
+
? resolveAxisValue(parsed.overrides.scope, "scope", SCOPE_VALUES, loadedConfig)
|
|
264
|
+
: DEFAULT_SCOPE;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const base = resolveBase(parsed.base, loadedConfig, presetBase);
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
base,
|
|
271
|
+
axes: { agency, quality, scope },
|
|
272
|
+
modifiers: {
|
|
273
|
+
readonly: flags.readonly,
|
|
274
|
+
contextPacing: flags.contextPacing,
|
|
275
|
+
custom: customModifierPaths,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|