@promptctl/cc-candybar 1.42.1 → 1.43.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.
Files changed (126) hide show
  1. package/dist/index.mjs +72 -71
  2. package/package.json +5 -6
  3. package/src/check.ts +0 -478
  4. package/src/cli-flags.ts +0 -8
  5. package/src/click/wire.ts +0 -158
  6. package/src/config/action.ts +0 -329
  7. package/src/config/cli.ts +0 -71
  8. package/src/config/default-dsl-config.ts +0 -1645
  9. package/src/config/disclosure.ts +0 -170
  10. package/src/config/dsl-loader.ts +0 -339
  11. package/src/config/dsl-types.ts +0 -581
  12. package/src/config/edit-chrome.ts +0 -559
  13. package/src/config/help.ts +0 -151
  14. package/src/config/ident.ts +0 -22
  15. package/src/config/layout-ops.ts +0 -177
  16. package/src/config/loader/actions.ts +0 -972
  17. package/src/config/loader/cache.ts +0 -206
  18. package/src/config/loader/cross-ref.ts +0 -714
  19. package/src/config/loader/cycles.ts +0 -148
  20. package/src/config/loader/diagnostics.ts +0 -99
  21. package/src/config/loader/discovery.ts +0 -182
  22. package/src/config/loader/edit-mode.ts +0 -137
  23. package/src/config/loader/emit-schema.ts +0 -68
  24. package/src/config/loader/globals.ts +0 -269
  25. package/src/config/loader/helpers.ts +0 -48
  26. package/src/config/loader/layout.ts +0 -693
  27. package/src/config/loader/looks.ts +0 -96
  28. package/src/config/loader/menu-synth.ts +0 -435
  29. package/src/config/loader/merge.ts +0 -115
  30. package/src/config/loader/persist-target.ts +0 -67
  31. package/src/config/loader/presets.ts +0 -119
  32. package/src/config/loader/refs.ts +0 -100
  33. package/src/config/loader/reserved-namespace.ts +0 -38
  34. package/src/config/loader/segments.ts +0 -120
  35. package/src/config/loader/validate-core.ts +0 -737
  36. package/src/config/loader/variables.ts +0 -260
  37. package/src/config/menu-keys.ts +0 -139
  38. package/src/config/option-domain.ts +0 -164
  39. package/src/config/presets.ts +0 -326
  40. package/src/config/settings-menu.ts +0 -775
  41. package/src/daemon/acquire.ts +0 -684
  42. package/src/daemon/cache/git.ts +0 -649
  43. package/src/daemon/cache/render.ts +0 -623
  44. package/src/daemon/cache/session-usage-store.ts +0 -720
  45. package/src/daemon/cache/watchers.ts +0 -249
  46. package/src/daemon/client-debug.ts +0 -120
  47. package/src/daemon/client-stats.ts +0 -130
  48. package/src/daemon/client-transport.ts +0 -273
  49. package/src/daemon/client.ts +0 -78
  50. package/src/daemon/config-overrides-store.ts +0 -663
  51. package/src/daemon/debug-types.ts +0 -91
  52. package/src/daemon/debug.ts +0 -264
  53. package/src/daemon/fork-bomb-breaker.ts +0 -351
  54. package/src/daemon/limits.ts +0 -211
  55. package/src/daemon/log.ts +0 -81
  56. package/src/daemon/parent-watchdog.ts +0 -87
  57. package/src/daemon/paths.ts +0 -211
  58. package/src/daemon/process-fingerprint.ts +0 -146
  59. package/src/daemon/protocol.ts +0 -292
  60. package/src/daemon/render-payload.ts +0 -1256
  61. package/src/daemon/server.ts +0 -1330
  62. package/src/daemon/session-state-file.ts +0 -108
  63. package/src/daemon/session-state.ts +0 -237
  64. package/src/daemon/socket-lease.ts +0 -209
  65. package/src/daemon/socket-ownership.ts +0 -209
  66. package/src/daemon/stats.ts +0 -235
  67. package/src/daemon/verbs/config-validators.ts +0 -250
  68. package/src/daemon/verbs/index.ts +0 -706
  69. package/src/daemon/verbs/state-validators.ts +0 -249
  70. package/src/daemon/verbs/validator-registry.ts +0 -457
  71. package/src/demo/dsl.ts +0 -143
  72. package/src/demo/mock-data.ts +0 -67
  73. package/src/demo/statusline.json5 +0 -94
  74. package/src/dsl/node-registry.ts +0 -374
  75. package/src/dsl/render.ts +0 -803
  76. package/src/help-text.ts +0 -90
  77. package/src/index.ts +0 -210
  78. package/src/install/currency.ts +0 -197
  79. package/src/install/index.ts +0 -557
  80. package/src/proc/launch.ts +0 -459
  81. package/src/proc/stats-handle.ts +0 -13
  82. package/src/render/action.ts +0 -883
  83. package/src/render/active-segment.ts +0 -78
  84. package/src/render/diagnostic-style.ts +0 -23
  85. package/src/render/diagnostic-text.ts +0 -77
  86. package/src/render/error-glyph.ts +0 -53
  87. package/src/render/menu.ts +0 -257
  88. package/src/render/outcome-plan.ts +0 -45
  89. package/src/render/picker.ts +0 -372
  90. package/src/render/segment-color.ts +0 -74
  91. package/src/render/split-lines.ts +0 -51
  92. package/src/render/strip.ts +0 -228
  93. package/src/segments/cache.ts +0 -131
  94. package/src/segments/context.ts +0 -190
  95. package/src/segments/git.ts +0 -1084
  96. package/src/segments/metrics.ts +0 -187
  97. package/src/segments/pricing.ts +0 -452
  98. package/src/segments/session.ts +0 -23
  99. package/src/segments/tmux.ts +0 -74
  100. package/src/template-engine/cells.ts +0 -90
  101. package/src/template-engine/colors.ts +0 -124
  102. package/src/template-engine/engine.ts +0 -108
  103. package/src/template-engine/funcs.ts +0 -232
  104. package/src/template-engine/index.ts +0 -11
  105. package/src/template-engine/layout.ts +0 -133
  106. package/src/template-engine/scope.ts +0 -62
  107. package/src/template-engine/sparkline.ts +0 -79
  108. package/src/themes/index.ts +0 -20
  109. package/src/themes/palette-resolvers.ts +0 -84
  110. package/src/themes/policy.ts +0 -393
  111. package/src/utils/cache.ts +0 -206
  112. package/src/utils/claude.ts +0 -683
  113. package/src/utils/color-support.ts +0 -118
  114. package/src/utils/formatters.ts +0 -99
  115. package/src/utils/logger.ts +0 -5
  116. package/src/utils/outcome.ts +0 -33
  117. package/src/utils/schema-validator.ts +0 -126
  118. package/src/utils/single-flight.ts +0 -57
  119. package/src/utils/terminal-width.ts +0 -51
  120. package/src/utils/terminal.ts +0 -11
  121. package/src/utils/transcript-fs.ts +0 -279
  122. package/src/var-system/index.ts +0 -24
  123. package/src/var-system/sources.ts +0 -1047
  124. package/src/var-system/store.ts +0 -223
  125. package/src/var-system/types.ts +0 -57
  126. package/src/version.ts +0 -17
@@ -1,148 +0,0 @@
1
- // [LAW:types-are-the-program] Dependency-cycle detection over the variable graph.
2
- // Edges come from three sources (template refs, cache.key refs, cache.depends_on);
3
- // a single DFS catches mixed cycles spanning edge types. This file changes when
4
- // what constitutes a runtime dependency edge changes.
5
-
6
- import {
7
- hasCacheField,
8
- type DslConfig,
9
- type VariableDecl,
10
- } from "../dsl-types.js";
11
- import { findKeyLine } from "./diagnostics.js";
12
- import { type ValidateCtx } from "./validate-core.js";
13
- import { extractTemplateRefs } from "./refs.js";
14
-
15
- // Carries declaration metadata for each graph node so cycle errors report the
16
- // correct config path (variables.X vs segments.S.vars.X) and correct line.
17
- interface NodeInfo {
18
- readonly declarationPath: string;
19
- readonly linePathParts: readonly string[];
20
- }
21
-
22
- export function validateNoCycles(ctx: ValidateCtx, cfg: DslConfig): void {
23
- const { graph, nodeInfo } = buildTemplateGraph(cfg);
24
-
25
- const WHITE = 0;
26
- const GRAY = 1;
27
- const BLACK = 2;
28
- const color = new Map<string, number>();
29
- const stack: string[] = [];
30
-
31
- for (const node of graph.keys()) color.set(node, WHITE);
32
-
33
- for (const start of graph.keys()) {
34
- if (color.get(start) !== WHITE) continue;
35
- if (dfs(start)) return; // first cycle is enough — report and stop walking
36
- }
37
-
38
- function dfs(node: string): boolean {
39
- color.set(node, GRAY);
40
- stack.push(node);
41
- for (const next of graph.get(node) ?? []) {
42
- const c = color.get(next);
43
- if (c === GRAY) {
44
- const cycleStart = stack.indexOf(next);
45
- const cycle = [...stack.slice(cycleStart), next];
46
- const firstNode = cycle[0]!;
47
- const info = nodeInfo.get(firstNode);
48
- ctx.issues.push({
49
- path: info?.declarationPath ?? `variables.${firstNode}`,
50
- message: `Dependency cycle: ${cycle.join(" → ")}`,
51
- line: findKeyLine(
52
- ctx.source,
53
- info?.linePathParts ?? ["variables", firstNode],
54
- ),
55
- });
56
- return true;
57
- }
58
- if (c === WHITE && dfs(next)) return true;
59
- }
60
- color.set(node, BLACK);
61
- stack.pop();
62
- return false;
63
- }
64
- }
65
-
66
- // [LAW:types-are-the-program] Build the full variable dependency graph: edges
67
- // are X → Y for any of three edge kinds:
68
- // 1. template-kind vars: template string references Y (eval dependency)
69
- // 2. any var with cache.key: key template references Y (cache-key dependency)
70
- // 3. any var with cache.depends_on: each listed name is Y (invalidation dep)
71
- // All three kinds can form infinite loops at runtime; a single DFS catches
72
- // mixed cycles that span multiple edge types.
73
- //
74
- // Segment vars use the namespaced form (segName.varName) as their sole graph
75
- // node — eliminates bare-name collisions when two segments both declare a var
76
- // named e.g. "local". Global vars keep their bare names.
77
- function buildTemplateGraph(cfg: DslConfig): {
78
- graph: Map<string, Set<string>>;
79
- nodeInfo: Map<string, NodeInfo>;
80
- } {
81
- const allVarNames = new Set<string>(Object.keys(cfg.variables));
82
- const nodeInfo = new Map<string, NodeInfo>();
83
-
84
- for (const name of Object.keys(cfg.variables)) {
85
- nodeInfo.set(name, {
86
- declarationPath: `variables.${name}`,
87
- linePathParts: ["variables", name],
88
- });
89
- }
90
- for (const [segName, seg] of Object.entries(cfg.segments)) {
91
- if (!seg.vars) continue;
92
- for (const vName of Object.keys(seg.vars)) {
93
- const canonical = `${segName}.${vName}`;
94
- allVarNames.add(canonical);
95
- nodeInfo.set(canonical, {
96
- declarationPath: `segments.${segName}.vars.${vName}`,
97
- linePathParts: ["segments", segName, "vars", vName],
98
- });
99
- }
100
- }
101
-
102
- const graph = new Map<string, Set<string>>();
103
- for (const name of allVarNames) graph.set(name, new Set());
104
-
105
- // [LAW:one-source-of-truth] Edges resolve refs exactly as the runtime scope
106
- // proxy does: a ref is the literal store key (globals bare, segment locals
107
- // namespaced as segName.varName) — never re-derived per segment. Bare
108
- // own-segment refs are not aliased here because the runtime has no such
109
- // aliasing; cross-ref rejects them at load with the namespaced suggestion.
110
- const addTemplateEdges = (from: string, template: string): void => {
111
- for (const ref of extractTemplateRefs(template)) {
112
- if (allVarNames.has(ref)) {
113
- graph.get(from)!.add(ref);
114
- continue;
115
- }
116
- // Resolve "first identifier" — `.session.id` may indicate dependence on
117
- // `session` if that's the declared var (matches scope.ts proxy walk).
118
- const head = ref.split(".")[0]!;
119
- if (head !== ref && allVarNames.has(head)) {
120
- graph.get(from)!.add(head);
121
- }
122
- }
123
- };
124
-
125
- const addVarEdges = (name: string, v: VariableDecl): void => {
126
- if (v.kind === "template") addTemplateEdges(name, v.template);
127
- if (hasCacheField(v)) {
128
- if (v.cache && "key" in v.cache) addTemplateEdges(name, v.cache.key);
129
- if (v.cache && "depends_on" in v.cache) {
130
- for (const dep of v.cache.depends_on) {
131
- if (allVarNames.has(dep)) graph.get(name)!.add(dep);
132
- }
133
- }
134
- }
135
- };
136
-
137
- for (const [name, v] of Object.entries(cfg.variables)) {
138
- addVarEdges(name, v);
139
- }
140
- for (const [segName, seg] of Object.entries(cfg.segments)) {
141
- if (!seg.vars) continue;
142
- for (const [vName, vDecl] of Object.entries(seg.vars)) {
143
- addVarEdges(`${segName}.${vName}`, vDecl);
144
- }
145
- }
146
-
147
- return { graph, nodeInfo };
148
- }
@@ -1,99 +0,0 @@
1
- // [LAW:single-enforcer] One home for config-error reporting: the public issue/
2
- // error types, the best-effort source-line lookup every validator calls, and the
3
- // human-readable formatter ConfigError renders. Changes here are display/source-
4
- // mapping changes; the schema validators never touch this file.
5
-
6
- // ─── Public types ────────────────────────────────────────────────────────────
7
-
8
- export interface ConfigIssue {
9
- /** Dotted logical path inside the config (e.g., "variables.foo.cache"). */
10
- readonly path: string;
11
- /** Short, actionable description of the problem. */
12
- readonly message: string;
13
- /** Source line (1-based). For semantic errors, best-effort from the path. */
14
- readonly line?: number;
15
- /** Source column (1-based). Present only for parse errors. */
16
- readonly col?: number;
17
- }
18
-
19
- export class ConfigError extends Error {
20
- readonly file: string;
21
- readonly issues: readonly ConfigIssue[];
22
-
23
- constructor(file: string, issues: readonly ConfigIssue[]) {
24
- super(formatIssues(file, issues));
25
- this.name = "ConfigError";
26
- this.file = file;
27
- this.issues = issues;
28
- }
29
- }
30
-
31
- // ─── Best-effort source-line lookup ──────────────────────────────────────────
32
-
33
- // Walk source forward, finding each path component as a JSON5 key in turn.
34
- // JSON5 keys are unquoted identifiers (`foo:`), double-quoted strings, or
35
- // single-quoted strings. Numeric path parts (e.g., layout indices) are
36
- // skipped — they point inside arrays where line lookup is less useful.
37
- //
38
- // This is "good enough" navigation, not a guarantee. Returns undefined if a
39
- // path part can't be located — the caller falls back to the logical path.
40
- export function findKeyLine(
41
- source: string,
42
- pathParts: readonly string[],
43
- ): number | undefined {
44
- let cursor = 0;
45
- let foundCursor: number | undefined;
46
- for (const part of pathParts) {
47
- if (part === "" || /^\d+$/.test(part)) continue;
48
- const found = findKeyOccurrence(source, cursor, part);
49
- if (found === -1) {
50
- return foundCursor !== undefined
51
- ? lineFromOffset(source, foundCursor)
52
- : undefined;
53
- }
54
- cursor = found;
55
- foundCursor = found;
56
- }
57
- return foundCursor !== undefined
58
- ? lineFromOffset(source, foundCursor)
59
- : undefined;
60
- }
61
-
62
- function findKeyOccurrence(source: string, from: number, key: string): number {
63
- // Match `<key>:` or `"<key>":` or `'<key>':` — any whitespace before the colon
64
- // is allowed by JSON5. Escape regex specials in key.
65
- const escaped = key.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
66
- const re = new RegExp(`(?:["']${escaped}["']|\\b${escaped}\\b)\\s*:`, "g");
67
- re.lastIndex = from;
68
- const m = re.exec(source);
69
- return m ? m.index : -1;
70
- }
71
-
72
- function lineFromOffset(source: string, offset: number): number {
73
- let line = 1;
74
- for (let i = 0; i < offset && i < source.length; i++) {
75
- if (source.charCodeAt(i) === 0x0a) line++;
76
- }
77
- return line;
78
- }
79
-
80
- // ─── Error formatting ────────────────────────────────────────────────────────
81
-
82
- function formatIssues(file: string, issues: readonly ConfigIssue[]): string {
83
- if (issues.length === 0) return `${file}: invalid config (no details)`;
84
- const lines: string[] = [
85
- `Invalid config in ${file} (${issues.length} issue${issues.length === 1 ? "" : "s"}):`,
86
- ];
87
- for (const issue of issues) {
88
- const locParts: string[] = [];
89
- if (issue.line !== undefined) {
90
- locParts.push(
91
- `line ${issue.line}${issue.col !== undefined ? `:${issue.col}` : ""}`,
92
- );
93
- }
94
- if (issue.path) locParts.push(issue.path);
95
- const loc = locParts.length > 0 ? `[${locParts.join(" • ")}] ` : "";
96
- lines.push(` ${loc}${issue.message}`);
97
- }
98
- return lines.join("\n");
99
- }
@@ -1,182 +0,0 @@
1
- // [LAW:single-enforcer] Config-file discovery: where the DSL config can live, in
2
- // what precedence, and how `~` expands. One candidate-path enumerator feeds the
3
- // resolver, the watchers, and the collision detector so none can disagree about
4
- // which files are candidates. This file changes when the resolution rules change.
5
-
6
- import fs from "node:fs";
7
- import os from "node:os";
8
- import path from "node:path";
9
-
10
- // [LAW:one-source-of-truth] The set of accepted extensions lives here once.
11
- // Both .json5 and .json are accepted: JSON ⊂ JSON5, so the same parser
12
- // (JSON5.parse) handles both — only the filename lookup varies. Ordering is
13
- // load-bearing: .json5 wins over .json at the same location (documented
14
- // format > compatibility tail).
15
- const CONFIG_EXTENSIONS = ["json5", "json"] as const;
16
-
17
- // [LAW:single-enforcer] One implementation of `~`-prefix expansion, called at
18
- // each trust boundary that takes a user-supplied path. The CLI `--config`
19
- // value is expanded in `parseRenderArgs` (server.ts) before it ever reaches
20
- // here; `CC_CANDYBAR_CONFIG` is expanded below where the env var is read.
21
- // One function, one rule, two callers.
22
- //
23
- // [LAW:enumeration-gap] Only the shell-standard home-expansion forms trigger
24
- // replacement: bare `~`, `~/...`, or `~\...` on Windows. A string like
25
- // `~alice/cfg` (POSIX named-home lookup) is NOT expanded — we have no way
26
- // to resolve another user's home and a literal substitution would corrupt
27
- // the path (`<homedir>alice/cfg`).
28
- export function expandHome(p: string): string {
29
- return p === "~" || p.startsWith("~/") || p.startsWith("~\\")
30
- ? os.homedir() + p.slice(1)
31
- : p;
32
- }
33
-
34
- /**
35
- * The full ordered list of candidate paths the DSL config could live at,
36
- * for a given (projectDir, cwd). Returned regardless of which exist — the
37
- * cache uses this to watch every candidate location so the creation of any
38
- * file in the resolution chain triggers hot-reload.
39
- *
40
- * `configFile` is the highest-precedence entry — the path resolved from the
41
- * client's `--config` flag (already `~`-expanded at the trust boundary in
42
- * server.ts). When present, it is the sole candidate and the rest of the
43
- * precedence chain is bypassed.
44
- *
45
- * [LAW:single-enforcer] One enumerator; `resolveDslConfigPath` finds the
46
- * first that exists, watchers listen on all of them, no second list.
47
- *
48
- * [LAW:dataflow-not-control-flow] Location is the dominant precedence axis;
49
- * extension breaks ties within a location. Encoded as a nested flat-map: each
50
- * location yields one path per extension in order. No branches on extension.
51
- */
52
- export function dslConfigCandidatePaths(
53
- projectDir?: string,
54
- cwd?: string,
55
- configFile?: string,
56
- ): readonly string[] {
57
- // CLI --config wins over everything — highest precedence. Pre-expanded at
58
- // the trust boundary; trust the type here.
59
- if (configFile) {
60
- return [configFile];
61
- }
62
-
63
- const envPath = process.env.CC_CANDYBAR_CONFIG;
64
- if (envPath) {
65
- // [LAW:single-enforcer] env-var is a separate trust boundary; expand here
66
- // where the env is read, with the shared `expandHome` helper. [LAW:
67
- // dataflow-not-control-flow] When the env var sets the path, it's the
68
- // *only* candidate — the precedence chain collapses to one entry.
69
- return [expandHome(envPath)];
70
- }
71
-
72
- const effectiveCwd = cwd ?? process.cwd();
73
- const xdgConfigHome =
74
- process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
75
-
76
- return [
77
- ...(projectDir
78
- ? CONFIG_EXTENSIONS.map((ext) =>
79
- path.join(projectDir, `.cc-candybar.${ext}`),
80
- )
81
- : []),
82
- ...CONFIG_EXTENSIONS.map((ext) =>
83
- path.join(effectiveCwd, `.cc-candybar.${ext}`),
84
- ),
85
- ...CONFIG_EXTENSIONS.map((ext) =>
86
- path.join(xdgConfigHome, "cc-candybar", `config.${ext}`),
87
- ),
88
- ];
89
- }
90
-
91
- /**
92
- * Resolution order for the user's DSL config file:
93
- * 1. `configFile` (the CLI `--config <path>` value, already `~`-expanded)
94
- * 2. $CC_CANDYBAR_CONFIG env var (literal path, `~`-expanded here)
95
- * 3. `<projectDir>/.cc-candybar.json5`
96
- * 4. `<projectDir>/.cc-candybar.json`
97
- * 5. `<cwd>/.cc-candybar.json5`
98
- * 6. `<cwd>/.cc-candybar.json`
99
- * 7. `$XDG_CONFIG_HOME/cc-candybar/config.json5`
100
- * (defaulting to `~/.config/cc-candybar/config.json5`)
101
- * 8. `$XDG_CONFIG_HOME/cc-candybar/config.json`
102
- *
103
- * Returns the first path that exists, or null if none do.
104
- *
105
- * [LAW:dataflow-not-control-flow] The locations array is data; the search is
106
- * `locations.find(fs.existsSync)`. Adding a layer is a new array entry, not a
107
- * new branch. Extension support is a property of the candidate list, not the
108
- * search.
109
- *
110
- * [LAW:single-enforcer] Built on top of `dslConfigCandidatePaths` — the
111
- * precedence list lives in one place.
112
- */
113
- export function resolveDslConfigPath(
114
- projectDir?: string,
115
- cwd?: string,
116
- configFile?: string,
117
- ): string | null {
118
- return (
119
- dslConfigCandidatePaths(projectDir, cwd, configFile).find(fs.existsSync) ??
120
- null
121
- );
122
- }
123
-
124
- /**
125
- * Detect same-location extension collisions: any location where BOTH
126
- * `<base>.json5` and `<base>.json` exist simultaneously. The resolver picks
127
- * .json5 (documented format wins), but the user almost certainly didn't
128
- * intend to keep two; the duplicate is dead weight that will drift.
129
- *
130
- * Returns a human-readable warning naming the conflicting files, or null if
131
- * no collisions exist. The render path surfaces this through the daemon's
132
- * diagnostics channel so the user sees it on every render until they remove
133
- * the duplicate.
134
- *
135
- * [LAW:single-enforcer] Consumes `dslConfigCandidatePaths` — same enumerator
136
- * as the resolver and watcher; collision detection cannot disagree with
137
- * resolution about which files are candidates.
138
- *
139
- * [LAW:dataflow-not-control-flow] Walk candidates, group by parent directory
140
- * + base name (without extension), find groups with size > 1 whose members
141
- * all exist. No special-case branches per extension.
142
- */
143
- export function detectConfigCollisions(
144
- projectDir?: string,
145
- cwd?: string,
146
- ): string | null {
147
- const candidates = dslConfigCandidatePaths(projectDir, cwd);
148
- // [LAW:dataflow-not-control-flow] Dedupe candidates by full path first.
149
- // When projectDir === cwd (a very common case — the daemon often resolves
150
- // both from the same hook payload), the enumerator yields the same path
151
- // at both precedence levels. That is a structural duplicate of *position
152
- // in the precedence list*, not a same-location duplicate of *files on
153
- // disk*. The latter is what collision detection is for; the former is
154
- // noise that would fire a false positive.
155
- const seen = new Set<string>();
156
- const uniqueExisting: string[] = [];
157
- for (const candidate of candidates) {
158
- if (seen.has(candidate)) continue;
159
- seen.add(candidate);
160
- if (!fs.existsSync(candidate)) continue;
161
- uniqueExisting.push(candidate);
162
- }
163
- // Group by (dir + base-without-extension). A group with > 1 existing
164
- // member is a collision at that logical location.
165
- const groups = new Map<string, string[]>();
166
- for (const candidate of uniqueExisting) {
167
- const dir = path.dirname(candidate);
168
- const base = path.basename(candidate).replace(/\.(json5|json)$/, "");
169
- const key = path.join(dir, base);
170
- if (!groups.has(key)) groups.set(key, []);
171
- groups.get(key)!.push(candidate);
172
- }
173
- const collisions = [...groups.values()].filter((g) => g.length > 1);
174
- if (collisions.length === 0) return null;
175
- // Stable, parseable message. The first file in each group is the .json5
176
- // (the one that wins); the rest are the shadowed siblings.
177
- const lines = collisions.map((g) => {
178
- const [winner, ...shadowed] = g;
179
- return `${winner} shadows ${shadowed.join(", ")}`;
180
- });
181
- return `config-extension collision: ${lines.join("; ")} — remove the duplicate`;
182
- }
@@ -1,137 +0,0 @@
1
- // [LAW:one-source-of-truth] brandon-layout-edit-2gc.3's TOGGLE half of edit
2
- // mode — the disclosure primitive's third body-kind, one register down from
3
- // group sugar and `{{ menu }}`: where those two synthesize a whole trigger +
4
- // body, edit mode synthesizes only the on/off state + toggle ACTION here
5
- // (`edit.mode` / `edit.toggle`), so a hand-authored `{{ action "edit.toggle"
6
- // "✎" }}` cross-ref-checks and compiles exactly like any other action — no
7
- // bespoke "this action always exists" carve-out anywhere downstream. The
8
- // per-segment +/- CHROME is a separate, LATER pass
9
- // (src/config/edit-chrome.ts) that runs on the fully merged, preset-resolved,
10
- // rootOps-replayed config (inside validateConfig, not here) because it needs
11
- // data — which segments are in which preset's CURRENT tree — that does not
12
- // exist yet at this per-file parse stage. Splitting the two halves across two
13
- // synthesis points is not incidental: the toggle is authorable/cross-ref-able
14
- // content (like a group's name or a menu's apply action), the chrome is
15
- // derived data (like a group's lowered body), and each belongs at the stage
16
- // that has what it needs.
17
- //
18
- // [LAW:carrying-cost] DEMAND-DRIVEN, not unconditional — this is the one place
19
- // this pass diverges from group/menu synthesis's OWN precedent of "reserve
20
- // unconditionally, synthesize on demand" and leans fully into the "on demand"
21
- // half: a config that references `{{ action "edit.toggle" … }}` nowhere gets
22
- // NEITHER the toggle var/action NOR (edit-chrome.ts checks for the SAME
23
- // action's presence) any per-segment chrome. This matters concretely, not just
24
- // as a purity concern — `edit.mode` is a `state` variable and `edit.toggle` is
25
- // a `set` action, and cross-ref.ts requires a global `session.id` variable the
26
- // instant ANY state var or set action exists anywhere in a config. Synthesizing
27
- // either unconditionally would force session.id onto every purely-static,
28
- // non-interactive bar in the corpus — exactly the regression an early version
29
- // of this pass caused. The reserved namespace stays reserved unconditionally
30
- // (mirroring reservedNamespaceCollisions' own contract); only the SYNTHESIS is
31
- // conditional.
32
- //
33
- // WHAT candybar-settings-ui-aok.1 CHANGED, and what it did not: the gate above
34
- // is intact and still the only way edit mode is reached — but it now has a
35
- // PERMANENT DEMANDER. synthesizeSettingsMenu mints a `settings.edit` segment
36
- // referencing `edit.toggle` into every config it can host, so in practice the
37
- // demand is satisfied for essentially every config a user writes, and reading
38
- // this section as "most bars carry no edit mode" is no longer true. The
39
- // separation the gate protects still holds exactly where it always mattered:
40
- // the menu declines to synthesize for a config with no `session.id`
41
- // (canHostSessionState in settings-menu.ts), which is precisely the static,
42
- // non-interactive bar this comment was written to keep clean. Production blast
43
- // radius was nil either way — the bundled default's `toolbar` segment already
44
- // referenced `edit.toggle`, so every config merging it already demanded edit
45
- // mode before the menu existed.
46
-
47
- import { createEngine } from "@promptctl/go-template-js";
48
- import type { Mutable, ValidateCtx } from "./validate-core.js";
49
- import type { RawDslConfig, VariableDecl } from "../dsl-types.js";
50
- import type { ActionDecl } from "../action.js";
51
- import {
52
- DISCLOSURE_CLOSED,
53
- disclosureCycleAction,
54
- disclosureGate,
55
- disclosureStateVar,
56
- type DisclosureRef,
57
- } from "../disclosure.js";
58
- import { reservedNamespaceCollisions } from "./reserved-namespace.js";
59
-
60
- // [LAW:one-source-of-truth] The reserved namespace every edit-mode artifact —
61
- // this toggle AND edit-chrome.ts's per-position +/- actions/segments — lives
62
- // under, mirroring `groups.`/`menus.`. Exported so edit-chrome.ts's LATER
63
- // synthesis (and its `isChromeExempt` exclusion of edit-mode's own chrome
64
- // from being treated as ordinary, removable/addable content) reads the same
65
- // string, never a second copy.
66
- export const EDIT_NS = "edit.";
67
-
68
- // [LAW:single-enforcer] The SessionState key edit mode's on/off state lives
69
- // at, and the toggle action's identity member. Both edit-chrome.ts (every
70
- // synthesized affordance's `when` gate) and a hand-authored trigger segment
71
- // read/write these same two names — one declaration, no drift.
72
- export const EDIT_MODE_KEY = "edit.mode";
73
- export const EDIT_TOGGLE_ACTION = "edit.toggle";
74
- export const EDIT_MODE_OPEN = "open";
75
-
76
- // [LAW:one-source-of-truth] Edit mode AS a disclosure, which is what it has
77
- // always been: a binary toggle over one SessionState key. Naming it as a ref
78
- // lets anything nested inside edit mode (a `(?)` and its body) derive its own
79
- // gate by conjunction with this one, instead of concatenating gate strings.
80
- export const EDIT_MODE_REF: DisclosureRef = {
81
- variable: EDIT_MODE_KEY,
82
- member: EDIT_MODE_OPEN,
83
- };
84
-
85
- // [LAW:one-source-of-truth] The predicate every synthesized +/- chrome
86
- // segment gates on — derived from the ref above through the same function
87
- // every other disclosure's gate comes from, so edit-mode chrome and a group
88
- // body are gated by one rule rather than by two spellings that agree today.
89
- export const EDIT_MODE_GATE = disclosureGate(EDIT_MODE_REF);
90
-
91
- // [LAW:single-enforcer] The ONE detector for "does this file want edit mode":
92
- // a literal `{{ action "edit.toggle" … }}` call somewhere a segment's
93
- // template/bg/fg can reach — the SAME AST-based approach
94
- // menu-synth.ts's segmentReferencesMenu uses (robust against whitespace,
95
- // pipelines, and lookalike text a source-string scan would false-positive
96
- // or false-negative on), one function name over. A bare engine purely for
97
- // introspection: it never evaluates, so a malformed template simply yields
98
- // no match here (registerDslConfig re-parses and reports the real error;
99
- // [LAW:no-silent-failure] this pass just isn't the one that reports it).
100
- function referencesEditToggle(template: string): boolean {
101
- const engine = createEngine<string>({ fromString: (s) => s });
102
- try {
103
- return engine
104
- .parse(template)
105
- .referencedCalls()
106
- .some((c) => c.name === "action" && c.args[0] === EDIT_TOGGLE_ACTION);
107
- } catch {
108
- return false;
109
- }
110
- }
111
-
112
- function fileWantsEditMode(out: Readonly<RawDslConfig>): boolean {
113
- for (const seg of Object.values(out.segments ?? {})) {
114
- for (const field of [seg.template, seg.bg, seg.fg] as const) {
115
- if (typeof field === "string" && referencesEditToggle(field)) {
116
- return true;
117
- }
118
- }
119
- }
120
- return false;
121
- }
122
-
123
- export function synthesizeEditModeToggle(
124
- ctx: ValidateCtx,
125
- out: Mutable<RawDslConfig>,
126
- ): void {
127
- reservedNamespaceCollisions(ctx, out, EDIT_NS, "edit mode");
128
- if (!fileWantsEditMode(out)) return;
129
- const variables: Record<string, VariableDecl> = {
130
- [EDIT_MODE_KEY]: disclosureStateVar(EDIT_MODE_KEY, DISCLOSURE_CLOSED),
131
- };
132
- const actions: Record<string, ActionDecl> = {
133
- [EDIT_TOGGLE_ACTION]: disclosureCycleAction(EDIT_MODE_KEY, EDIT_MODE_OPEN),
134
- };
135
- out.variables = { ...(out.variables ?? {}), ...variables };
136
- out.actions = { ...(out.actions ?? {}), ...actions };
137
- }
@@ -1,68 +0,0 @@
1
- // [LAW:one-source-of-truth] The JSON Schema emitter — the SECOND interpreter over
2
- // the declarative loader schemas. `validateConfig` composes the per-module
3
- // `validate*` functions to validate at runtime; `emitConfigSchema` composes the
4
- // per-module `*Json` functions to derive the editor-facing JSON Schema. Both read
5
- // the SAME module-private schema declarations (GLOBALS_SCHEMA, VARIABLE_SCHEMA,
6
- // SEGMENT_SCHEMA, CACHE_SCHEMA, SET_ARMS, the A-grammar layout grammar), so the
7
- // published schema and the runtime validator can never describe a different grammar.
8
- //
9
- // What a JSON Schema still cannot express (by construction): cross-field
10
- // refinements (min<max, by≠0, input default matches type), palette-name
11
- // membership, duration FORMAT, and cross-references between segments/variables/
12
- // cycles. Those stay SEMANTIC checks the loader carries — schema = shape, lint =
13
- // meaning, the same complementary boundary `config-schema.test.ts` pins.
14
-
15
- import { editGlobalsJson, globalsJson } from "./globals.js";
16
- import { variablesMapJson } from "./variables.js";
17
- import { segmentsJson } from "./segments.js";
18
- import { actionsJson } from "./actions.js";
19
- import { looksJson } from "./looks.js";
20
- import { presetsJson } from "./presets.js";
21
- import {
22
- layoutNodeJson,
23
- LAYOUT_NODE_DEF_NAME,
24
- LAYOUT_NODE_REF,
25
- } from "./layout.js";
26
- import type { JsonNode } from "./validate-core.js";
27
-
28
- // [LAW:one-source-of-truth] The stable published identity. The committed artifact
29
- // is self-identifying at this URL so an editor loading it via `$schema` resolves.
30
- export const SCHEMA_ID =
31
- "https://raw.githubusercontent.com/promptctl/cc-candybar/main/schema/cc-candybar.schema.json";
32
-
33
- // [LAW:dataflow-not-control-flow] The RawDslConfig schema: every top-level key is
34
- // optional (a user file declares only what differs from the bundled default), so
35
- // the object carries no `required`. Each property is one module's emitted shape —
36
- // the same composition `validateConfig` performs over the validators. `root`
37
- // references the LayoutNode definition that closes the node recursion via `$ref`.
38
- export function emitConfigSchema(): JsonNode {
39
- return {
40
- $schema: "http://json-schema.org/draft-07/schema#",
41
- $id: SCHEMA_ID,
42
- title: "cc-candybar config (.cc-candybar.json5)",
43
- type: "object",
44
- additionalProperties: false,
45
- properties: {
46
- globals: globalsJson(),
47
- variables: variablesMapJson(),
48
- segments: segmentsJson(),
49
- root: { $ref: LAYOUT_NODE_REF },
50
- actions: actionsJson(),
51
- looks: looksJson(),
52
- presets: presetsJson(),
53
- editGlobals: editGlobalsJson(),
54
- helpers: { type: "object", additionalProperties: { type: "string" } },
55
- },
56
- definitions: {
57
- [LAYOUT_NODE_DEF_NAME]: layoutNodeJson(),
58
- },
59
- };
60
- }
61
-
62
- // [LAW:single-enforcer] One serialization, shared by `gen:schema` (writes the
63
- // committed artifact) and `check:schema` (byte-diffs against it) so the two can
64
- // never disagree on how the schema is produced. Trailing newline + 2-space indent
65
- // match the committed file's format.
66
- export function serializeConfigSchema(): string {
67
- return JSON.stringify(emitConfigSchema(), null, 2) + "\n";
68
- }