@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.
- package/dist/index.mjs +72 -71
- package/package.json +5 -6
- package/src/check.ts +0 -478
- package/src/cli-flags.ts +0 -8
- package/src/click/wire.ts +0 -158
- package/src/config/action.ts +0 -329
- package/src/config/cli.ts +0 -71
- package/src/config/default-dsl-config.ts +0 -1645
- package/src/config/disclosure.ts +0 -170
- package/src/config/dsl-loader.ts +0 -339
- package/src/config/dsl-types.ts +0 -581
- package/src/config/edit-chrome.ts +0 -559
- package/src/config/help.ts +0 -151
- package/src/config/ident.ts +0 -22
- package/src/config/layout-ops.ts +0 -177
- package/src/config/loader/actions.ts +0 -972
- package/src/config/loader/cache.ts +0 -206
- package/src/config/loader/cross-ref.ts +0 -714
- package/src/config/loader/cycles.ts +0 -148
- package/src/config/loader/diagnostics.ts +0 -99
- package/src/config/loader/discovery.ts +0 -182
- package/src/config/loader/edit-mode.ts +0 -137
- package/src/config/loader/emit-schema.ts +0 -68
- package/src/config/loader/globals.ts +0 -269
- package/src/config/loader/helpers.ts +0 -48
- package/src/config/loader/layout.ts +0 -693
- package/src/config/loader/looks.ts +0 -96
- package/src/config/loader/menu-synth.ts +0 -435
- package/src/config/loader/merge.ts +0 -115
- package/src/config/loader/persist-target.ts +0 -67
- package/src/config/loader/presets.ts +0 -119
- package/src/config/loader/refs.ts +0 -100
- package/src/config/loader/reserved-namespace.ts +0 -38
- package/src/config/loader/segments.ts +0 -120
- package/src/config/loader/validate-core.ts +0 -737
- package/src/config/loader/variables.ts +0 -260
- package/src/config/menu-keys.ts +0 -139
- package/src/config/option-domain.ts +0 -164
- package/src/config/presets.ts +0 -326
- package/src/config/settings-menu.ts +0 -775
- package/src/daemon/acquire.ts +0 -684
- package/src/daemon/cache/git.ts +0 -649
- package/src/daemon/cache/render.ts +0 -623
- package/src/daemon/cache/session-usage-store.ts +0 -720
- package/src/daemon/cache/watchers.ts +0 -249
- package/src/daemon/client-debug.ts +0 -120
- package/src/daemon/client-stats.ts +0 -130
- package/src/daemon/client-transport.ts +0 -273
- package/src/daemon/client.ts +0 -78
- package/src/daemon/config-overrides-store.ts +0 -663
- package/src/daemon/debug-types.ts +0 -91
- package/src/daemon/debug.ts +0 -264
- package/src/daemon/fork-bomb-breaker.ts +0 -351
- package/src/daemon/limits.ts +0 -211
- package/src/daemon/log.ts +0 -81
- package/src/daemon/parent-watchdog.ts +0 -87
- package/src/daemon/paths.ts +0 -211
- package/src/daemon/process-fingerprint.ts +0 -146
- package/src/daemon/protocol.ts +0 -292
- package/src/daemon/render-payload.ts +0 -1256
- package/src/daemon/server.ts +0 -1330
- package/src/daemon/session-state-file.ts +0 -108
- package/src/daemon/session-state.ts +0 -237
- package/src/daemon/socket-lease.ts +0 -209
- package/src/daemon/socket-ownership.ts +0 -209
- package/src/daemon/stats.ts +0 -235
- package/src/daemon/verbs/config-validators.ts +0 -250
- package/src/daemon/verbs/index.ts +0 -706
- package/src/daemon/verbs/state-validators.ts +0 -249
- package/src/daemon/verbs/validator-registry.ts +0 -457
- package/src/demo/dsl.ts +0 -143
- package/src/demo/mock-data.ts +0 -67
- package/src/demo/statusline.json5 +0 -94
- package/src/dsl/node-registry.ts +0 -374
- package/src/dsl/render.ts +0 -803
- package/src/help-text.ts +0 -90
- package/src/index.ts +0 -210
- package/src/install/currency.ts +0 -197
- package/src/install/index.ts +0 -557
- package/src/proc/launch.ts +0 -459
- package/src/proc/stats-handle.ts +0 -13
- package/src/render/action.ts +0 -883
- package/src/render/active-segment.ts +0 -78
- package/src/render/diagnostic-style.ts +0 -23
- package/src/render/diagnostic-text.ts +0 -77
- package/src/render/error-glyph.ts +0 -53
- package/src/render/menu.ts +0 -257
- package/src/render/outcome-plan.ts +0 -45
- package/src/render/picker.ts +0 -372
- package/src/render/segment-color.ts +0 -74
- package/src/render/split-lines.ts +0 -51
- package/src/render/strip.ts +0 -228
- package/src/segments/cache.ts +0 -131
- package/src/segments/context.ts +0 -190
- package/src/segments/git.ts +0 -1084
- package/src/segments/metrics.ts +0 -187
- package/src/segments/pricing.ts +0 -452
- package/src/segments/session.ts +0 -23
- package/src/segments/tmux.ts +0 -74
- package/src/template-engine/cells.ts +0 -90
- package/src/template-engine/colors.ts +0 -124
- package/src/template-engine/engine.ts +0 -108
- package/src/template-engine/funcs.ts +0 -232
- package/src/template-engine/index.ts +0 -11
- package/src/template-engine/layout.ts +0 -133
- package/src/template-engine/scope.ts +0 -62
- package/src/template-engine/sparkline.ts +0 -79
- package/src/themes/index.ts +0 -20
- package/src/themes/palette-resolvers.ts +0 -84
- package/src/themes/policy.ts +0 -393
- package/src/utils/cache.ts +0 -206
- package/src/utils/claude.ts +0 -683
- package/src/utils/color-support.ts +0 -118
- package/src/utils/formatters.ts +0 -99
- package/src/utils/logger.ts +0 -5
- package/src/utils/outcome.ts +0 -33
- package/src/utils/schema-validator.ts +0 -126
- package/src/utils/single-flight.ts +0 -57
- package/src/utils/terminal-width.ts +0 -51
- package/src/utils/terminal.ts +0 -11
- package/src/utils/transcript-fs.ts +0 -279
- package/src/var-system/index.ts +0 -24
- package/src/var-system/sources.ts +0 -1047
- package/src/var-system/store.ts +0 -223
- package/src/var-system/types.ts +0 -57
- 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
|
-
}
|