@aacombarro89/praxis 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/dist/anchors.d.ts +36 -0
- package/dist/anchors.js +197 -0
- package/dist/anchors.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -0
- package/dist/emit.d.ts +51 -0
- package/dist/emit.js +253 -0
- package/dist/emit.js.map +1 -0
- package/dist/external.d.ts +61 -0
- package/dist/external.js +155 -0
- package/dist/external.js.map +1 -0
- package/dist/init.d.ts +45 -0
- package/dist/init.js +123 -0
- package/dist/init.js.map +1 -0
- package/dist/manifest.d.ts +36 -0
- package/dist/manifest.js +63 -0
- package/dist/manifest.js.map +1 -0
- package/dist/merge-json.d.ts +83 -0
- package/dist/merge-json.js +207 -0
- package/dist/merge-json.js.map +1 -0
- package/dist/merge.d.ts +53 -0
- package/dist/merge.js +89 -0
- package/dist/merge.js.map +1 -0
- package/dist/packages.d.ts +47 -0
- package/dist/packages.js +121 -0
- package/dist/packages.js.map +1 -0
- package/dist/permissions.d.ts +54 -0
- package/dist/permissions.js +83 -0
- package/dist/permissions.js.map +1 -0
- package/dist/plugins.d.ts +21 -0
- package/dist/plugins.js +88 -0
- package/dist/plugins.js.map +1 -0
- package/dist/program.d.ts +2 -0
- package/dist/program.js +201 -0
- package/dist/program.js.map +1 -0
- package/dist/sync.d.ts +37 -0
- package/dist/sync.js +45 -0
- package/dist/sync.js.map +1 -0
- package/package.json +40 -0
- package/packages/external/drawio/package.yaml +7 -0
- package/packages/external/drawio/plugins.yaml +13 -0
- package/packages/external/ponytail/package.yaml +7 -0
- package/packages/external/ponytail/plugins.yaml +13 -0
- package/packages/layer1/instruction-upkeep/commands/audit.md +41 -0
- package/packages/layer1/instruction-upkeep/package.yaml +8 -0
- package/packages/layer1/instruction-upkeep/rules.md +21 -0
- package/packages/layer1/karpathy-claude/package.yaml +5 -0
- package/packages/layer1/karpathy-claude/rules.md +35 -0
- package/packages/layer1/safe-permissions/package.yaml +5 -0
- package/packages/layer1/safe-permissions/permissions.yaml +26 -0
- package/packages/layer1/session-handoff/commands/handoff.md +185 -0
- package/packages/layer1/session-handoff/package.yaml +10 -0
- package/packages/layer1/session-handoff/rules.md +20 -0
- package/packages/layer1/upkeep/commands/upkeep.md +31 -0
- package/packages/layer1/upkeep/package.yaml +10 -0
- package/packages/layer1/upkeep/rules.md +12 -0
- package/packages/layer1/wiki-memory/commands/wiki.md +103 -0
- package/packages/layer1/wiki-memory/package.yaml +10 -0
- package/packages/layer1/wiki-memory/rules.md +49 -0
- package/packages/layer2/node-recipes/package.yaml +10 -0
- package/packages/layer2/node-recipes/rules.md +33 -0
- package/packages/layer2/node-testing/package.yaml +10 -0
- package/packages/layer2/node-testing/rules.md +33 -0
- package/packages/layer2/python-backend-recipes/package.yaml +10 -0
- package/packages/layer2/python-backend-recipes/rules.md +37 -0
- package/packages/layer2/python-testing/package.yaml +10 -0
- package/packages/layer2/python-testing/rules.md +33 -0
- package/packages/layer2/react-components/package.yaml +10 -0
- package/packages/layer2/react-components/rules.md +40 -0
- package/packages/layer2/react-testing/package.yaml +10 -0
- package/packages/layer2/react-testing/rules.md +35 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-aware managed merge for `.claude/settings.json` (docs/wiki/merge-engine.md,
|
|
3
|
+
* decisions D10/D11/D18/D19). The markdown engine in `src/merge.ts` owns spans of *text*
|
|
4
|
+
* delimited by HTML-comment markers; that trick has **no JSON equivalent** — JSON
|
|
5
|
+
* has no comments. So this module is the structured-config analogue: Praxis owns
|
|
6
|
+
* only the **individual entries** it emits — permission rule strings, marketplace
|
|
7
|
+
* declarations, enabled-plugin keys — deep-merged into the host file, and records
|
|
8
|
+
* exactly which entries are its own in a self-contained top-level `_praxis` key
|
|
9
|
+
* (the JSON-native marker; D11 — no separate lockfile). One reconcile pass owns
|
|
10
|
+
* the whole file so two concerns (permissions, plugins) never compose into two
|
|
11
|
+
* separate writes of the same path (the same-file compose bug).
|
|
12
|
+
*
|
|
13
|
+
* Invariants (mirrors the markdown engine, verified by conformance):
|
|
14
|
+
* - User-added entries in the same structures survive untouched (per-entry
|
|
15
|
+
* ownership).
|
|
16
|
+
* - Entries Praxis previously emitted but no longer ships are removed on update.
|
|
17
|
+
* - If the user edited/removed one of Praxis's managed entries, that group is a
|
|
18
|
+
* **conflict, never a clobber** (D10): left exactly as-is, surfaced.
|
|
19
|
+
* - Idempotent: re-merging identical input is a byte-for-byte no-op.
|
|
20
|
+
* - Every other key in the file (model, env, defaultMode, …) is preserved.
|
|
21
|
+
*
|
|
22
|
+
* The `_praxis` key is a valid top-level addition: the Claude Code settings schema
|
|
23
|
+
* sets top-level `additionalProperties: true` (verified against
|
|
24
|
+
* json.schemastore.org/claude-code-settings.json), while `permissions` itself is
|
|
25
|
+
* `additionalProperties: false` — hence the marker lives as a top-level sibling of
|
|
26
|
+
* `permissions`/`extraKnownMarketplaces`/`enabledPlugins`, never inside them.
|
|
27
|
+
*/
|
|
28
|
+
/** The three permission buckets Praxis manages, in stable emit order. */
|
|
29
|
+
export const BUCKETS = ["allow", "ask", "deny"];
|
|
30
|
+
function asStringArray(value) {
|
|
31
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
32
|
+
}
|
|
33
|
+
function isObject(v) {
|
|
34
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Bring `.claude/settings.json` in line with the desired permissions and/or
|
|
38
|
+
* plugin-marketplace state in a single pass — the only function that may write
|
|
39
|
+
* this file, so permissions and plugins compose into one write instead of two
|
|
40
|
+
* operations racing on the same path. Splices only the entries Praxis owns;
|
|
41
|
+
* preserves user entries and every unrelated key; refuses to overwrite a group
|
|
42
|
+
* the user has edited (conflict). Pure; no I/O.
|
|
43
|
+
*
|
|
44
|
+
* @param existing the destination file's current text ("" if it does not exist)
|
|
45
|
+
* @param desired the permissions and/or plugins state Praxis wants to own
|
|
46
|
+
*/
|
|
47
|
+
export function reconcileSettings(existing, desired) {
|
|
48
|
+
const root = parseRoot(existing);
|
|
49
|
+
const marker = isObject(root._praxis) ? root._praxis : {};
|
|
50
|
+
const managed = marker.managed ?? {};
|
|
51
|
+
const conflicts = [];
|
|
52
|
+
const newManaged = {};
|
|
53
|
+
if (desired.permissions) {
|
|
54
|
+
const recorded = managed.permissions ?? {};
|
|
55
|
+
const permissions = isObject(root.permissions)
|
|
56
|
+
? root.permissions
|
|
57
|
+
: {};
|
|
58
|
+
const newRecorded = {};
|
|
59
|
+
for (const bucket of BUCKETS) {
|
|
60
|
+
const onDisk = asStringArray(permissions[bucket]);
|
|
61
|
+
const rec = asStringArray(recorded[bucket]);
|
|
62
|
+
const want = desired.permissions[bucket] ?? [];
|
|
63
|
+
// Conflict, never clobber (D10): the managed set is intact only if every
|
|
64
|
+
// rule Praxis last emitted is still present.
|
|
65
|
+
const intact = rec.every((r) => onDisk.includes(r));
|
|
66
|
+
if (!intact) {
|
|
67
|
+
conflicts.push(`permissions.${bucket}`);
|
|
68
|
+
newRecorded[bucket] = rec;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const recSet = new Set(rec);
|
|
72
|
+
const wantSet = new Set(want);
|
|
73
|
+
const merged = [];
|
|
74
|
+
for (const r of onDisk) {
|
|
75
|
+
if (!recSet.has(r) || wantSet.has(r))
|
|
76
|
+
merged.push(r);
|
|
77
|
+
}
|
|
78
|
+
for (const r of want) {
|
|
79
|
+
if (!merged.includes(r))
|
|
80
|
+
merged.push(r);
|
|
81
|
+
}
|
|
82
|
+
if (merged.length > 0)
|
|
83
|
+
permissions[bucket] = merged;
|
|
84
|
+
else
|
|
85
|
+
delete permissions[bucket];
|
|
86
|
+
if (want.length > 0)
|
|
87
|
+
newRecorded[bucket] = want;
|
|
88
|
+
}
|
|
89
|
+
if (Object.keys(permissions).length > 0)
|
|
90
|
+
root.permissions = permissions;
|
|
91
|
+
else
|
|
92
|
+
delete root.permissions;
|
|
93
|
+
if (Object.keys(newRecorded).length > 0)
|
|
94
|
+
newManaged.permissions = newRecorded;
|
|
95
|
+
}
|
|
96
|
+
else if (managed.permissions) {
|
|
97
|
+
// Not part of this sync — keep tracking what we previously owned untouched.
|
|
98
|
+
newManaged.permissions = managed.permissions;
|
|
99
|
+
}
|
|
100
|
+
if (desired.plugins) {
|
|
101
|
+
const recordedMarketplaces = managed.marketplaces ?? [];
|
|
102
|
+
const recordedPlugins = managed.plugins ?? [];
|
|
103
|
+
const marketplaces = isObject(root.extraKnownMarketplaces)
|
|
104
|
+
? root.extraKnownMarketplaces
|
|
105
|
+
: {};
|
|
106
|
+
const enabledPlugins = isObject(root.enabledPlugins)
|
|
107
|
+
? root.enabledPlugins
|
|
108
|
+
: {};
|
|
109
|
+
const marketplacesIntact = recordedMarketplaces.every((name) => name in marketplaces);
|
|
110
|
+
const pluginsIntact = recordedPlugins.every((key) => key in enabledPlugins);
|
|
111
|
+
if (!marketplacesIntact) {
|
|
112
|
+
// Conflict, never clobber (D10): leave extraKnownMarketplaces exactly as
|
|
113
|
+
// found on disk (including a present-but-empty `{}`), don't recompute it.
|
|
114
|
+
conflicts.push("plugins.marketplaces");
|
|
115
|
+
newManaged.marketplaces = recordedMarketplaces;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const wantNames = new Set(desired.plugins.marketplaces.map((m) => m.name));
|
|
119
|
+
for (const name of Object.keys(marketplaces)) {
|
|
120
|
+
if (recordedMarketplaces.includes(name) && !wantNames.has(name))
|
|
121
|
+
delete marketplaces[name];
|
|
122
|
+
}
|
|
123
|
+
for (const m of desired.plugins.marketplaces) {
|
|
124
|
+
marketplaces[m.name] = { source: m.source };
|
|
125
|
+
}
|
|
126
|
+
if (Object.keys(marketplaces).length > 0)
|
|
127
|
+
root.extraKnownMarketplaces = marketplaces;
|
|
128
|
+
else
|
|
129
|
+
delete root.extraKnownMarketplaces;
|
|
130
|
+
if (desired.plugins.marketplaces.length > 0) {
|
|
131
|
+
newManaged.marketplaces = desired.plugins.marketplaces.map((m) => m.name);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!pluginsIntact) {
|
|
135
|
+
// Same conflict rule for enabledPlugins, independently of marketplaces.
|
|
136
|
+
conflicts.push("plugins.enable");
|
|
137
|
+
newManaged.plugins = recordedPlugins;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const wantKeys = new Set(desired.plugins.enable);
|
|
141
|
+
for (const key of Object.keys(enabledPlugins)) {
|
|
142
|
+
if (recordedPlugins.includes(key) && !wantKeys.has(key))
|
|
143
|
+
delete enabledPlugins[key];
|
|
144
|
+
}
|
|
145
|
+
for (const key of desired.plugins.enable) {
|
|
146
|
+
enabledPlugins[key] = true;
|
|
147
|
+
}
|
|
148
|
+
if (Object.keys(enabledPlugins).length > 0)
|
|
149
|
+
root.enabledPlugins = enabledPlugins;
|
|
150
|
+
else
|
|
151
|
+
delete root.enabledPlugins;
|
|
152
|
+
if (desired.plugins.enable.length > 0)
|
|
153
|
+
newManaged.plugins = desired.plugins.enable;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
if (managed.marketplaces)
|
|
158
|
+
newManaged.marketplaces = managed.marketplaces;
|
|
159
|
+
if (managed.plugins)
|
|
160
|
+
newManaged.plugins = managed.plugins;
|
|
161
|
+
}
|
|
162
|
+
if (Object.keys(newManaged).length > 0) {
|
|
163
|
+
root._praxis = { managed: newManaged };
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
delete root._praxis;
|
|
167
|
+
}
|
|
168
|
+
const text = `${JSON.stringify(root, null, 2)}\n`;
|
|
169
|
+
return { text, changed: text !== normalize(existing), conflicts };
|
|
170
|
+
}
|
|
171
|
+
/** Permissions-only convenience wrapper over `reconcileSettings` — kept because
|
|
172
|
+
* most call sites and tests only ever reconcile permissions. */
|
|
173
|
+
export function reconcilePermissions(existing, desired) {
|
|
174
|
+
return reconcileSettings(existing, { permissions: desired });
|
|
175
|
+
}
|
|
176
|
+
/** Parse the host file into a mutable object, preserving key order. Empty/blank
|
|
177
|
+
* → `{}`. Invalid JSON throws a readable error rather than risk a clobber. */
|
|
178
|
+
function parseRoot(existing) {
|
|
179
|
+
if (existing.trim() === "")
|
|
180
|
+
return {};
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
parsed = JSON.parse(existing);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
throw new Error(`Cannot merge settings: the destination file is not valid JSON ` +
|
|
187
|
+
`(${err.message}). Fix or remove it, then re-run \`praxis sync\`.`);
|
|
188
|
+
}
|
|
189
|
+
if (!isObject(parsed)) {
|
|
190
|
+
throw new Error(`Cannot merge settings: the destination file must be a JSON object, ` +
|
|
191
|
+
`got ${Array.isArray(parsed) ? "an array" : typeof parsed}.`);
|
|
192
|
+
}
|
|
193
|
+
return parsed;
|
|
194
|
+
}
|
|
195
|
+
/** The byte-form a no-op write would produce for the given input, used to compute
|
|
196
|
+
* `changed` honestly when the input is already-formatted JSON. */
|
|
197
|
+
function normalize(existing) {
|
|
198
|
+
if (existing.trim() === "")
|
|
199
|
+
return "";
|
|
200
|
+
try {
|
|
201
|
+
return `${JSON.stringify(JSON.parse(existing), null, 2)}\n`;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return existing;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=merge-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-json.js","sourceRoot":"","sources":["../src/merge-json.ts"],"names":[],"mappings":"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"}
|
package/dist/merge.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The block-splice merge core (docs/wiki/merge-engine.md, decisions D7/D10/D11).
|
|
3
|
+
*
|
|
4
|
+
* Praxis owns only the text between its `praxis:begin`/`praxis:end` markers.
|
|
5
|
+
* The host file is never parsed as markdown — we locate our own machine markers
|
|
6
|
+
* and treat the content between them as OPAQUE text. This is the whole point of
|
|
7
|
+
* rejecting a markdown AST (D7): re-serializing a tree would normalize the user's
|
|
8
|
+
* prose. Here, every byte outside a managed span is preserved exactly.
|
|
9
|
+
*/
|
|
10
|
+
/** A Praxis-managed region located in a host file. */
|
|
11
|
+
export interface ManagedBlock {
|
|
12
|
+
id: string;
|
|
13
|
+
/** Exact bytes between the markers, excluding the marker lines. Opaque. */
|
|
14
|
+
content: string;
|
|
15
|
+
/** sha256 recorded in the begin marker (`""` if the marker carried none). */
|
|
16
|
+
recordedHash: string;
|
|
17
|
+
/** Offset of the block start (begin marker) in the source text. */
|
|
18
|
+
start: number;
|
|
19
|
+
/** Offset just past the end marker in the source text. */
|
|
20
|
+
end: number;
|
|
21
|
+
}
|
|
22
|
+
export type BlockStatus = "absent" | "unchanged" | "user-edited";
|
|
23
|
+
/** sha256 (hex) of the content. Used to detect user edits inside a block. */
|
|
24
|
+
export declare function hashContent(content: string): string;
|
|
25
|
+
/** Render a full managed block, stamping the begin marker with a fresh hash. */
|
|
26
|
+
export declare function renderBlock(id: string, content: string): string;
|
|
27
|
+
/** All managed blocks in the text, in document order. */
|
|
28
|
+
export declare function findBlocks(text: string): ManagedBlock[];
|
|
29
|
+
/** The first managed block with the given id, wherever it sits in the file. */
|
|
30
|
+
export declare function findBlock(text: string, id: string): ManagedBlock | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* absent — no such block in the file.
|
|
33
|
+
* unchanged — on-disk content still matches the recorded hash (Praxis's last
|
|
34
|
+
* emission is intact; safe to update).
|
|
35
|
+
* user-edited — on-disk content differs from the recorded hash; the user (or
|
|
36
|
+
* agent) edited managed content. Never overwrite (D10).
|
|
37
|
+
*/
|
|
38
|
+
export declare function blockStatus(block: ManagedBlock | undefined): BlockStatus;
|
|
39
|
+
export interface ReconcileResult {
|
|
40
|
+
/** The resulting file text. */
|
|
41
|
+
text: string;
|
|
42
|
+
/** Whether any byte changed. */
|
|
43
|
+
changed: boolean;
|
|
44
|
+
/** Ids of blocks the user edited; left untouched, surfaced for resolution. */
|
|
45
|
+
conflicts: string[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Bring the host text in line with the desired managed content, one block per
|
|
49
|
+
* entry of `desired` (id -> content). Splices only managed spans; appends blocks
|
|
50
|
+
* that are absent; refuses to overwrite blocks the user has edited (conflict).
|
|
51
|
+
* Idempotent: reconciling with already-applied content is a byte-for-byte no-op.
|
|
52
|
+
*/
|
|
53
|
+
export declare function reconcile(text: string, desired: Record<string, string>): ReconcileResult;
|
package/dist/merge.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const beginMarker = (id, hash) => `<!-- praxis:begin ${id} sha256=${hash} (managed by praxis - edit praxis.yaml then run: praxis sync) -->`;
|
|
3
|
+
const endMarker = (id) => `<!-- praxis:end ${id} -->`;
|
|
4
|
+
// Match our own fences only. Group 1: id. Group 2: begin-marker attributes
|
|
5
|
+
// (carries sha256). Group 3: opaque content. The id backreference (\1) ties the
|
|
6
|
+
// end marker to its begin marker; content is non-greedy so the nearest end wins.
|
|
7
|
+
const BLOCK_RE = /<!--\s*praxis:begin\s+([A-Za-z0-9._-]+)([^>]*?)-->[^\n]*\r?\n([\s\S]*?)\r?\n?<!--\s*praxis:end\s+\1\s*-->/g;
|
|
8
|
+
function extractHash(attrs) {
|
|
9
|
+
const m = /sha256=([A-Za-z0-9]+)/.exec(attrs);
|
|
10
|
+
return m?.[1] ?? "";
|
|
11
|
+
}
|
|
12
|
+
/** sha256 (hex) of the content. Used to detect user edits inside a block. */
|
|
13
|
+
export function hashContent(content) {
|
|
14
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
15
|
+
}
|
|
16
|
+
/** Render a full managed block, stamping the begin marker with a fresh hash. */
|
|
17
|
+
export function renderBlock(id, content) {
|
|
18
|
+
return `${beginMarker(id, hashContent(content))}\n${content}\n${endMarker(id)}`;
|
|
19
|
+
}
|
|
20
|
+
/** All managed blocks in the text, in document order. */
|
|
21
|
+
export function findBlocks(text) {
|
|
22
|
+
const blocks = [];
|
|
23
|
+
BLOCK_RE.lastIndex = 0;
|
|
24
|
+
let m;
|
|
25
|
+
while ((m = BLOCK_RE.exec(text)) !== null) {
|
|
26
|
+
blocks.push({
|
|
27
|
+
id: m[1] ?? "",
|
|
28
|
+
recordedHash: extractHash(m[2] ?? ""),
|
|
29
|
+
content: m[3] ?? "",
|
|
30
|
+
start: m.index,
|
|
31
|
+
end: m.index + m[0].length,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return blocks;
|
|
35
|
+
}
|
|
36
|
+
/** The first managed block with the given id, wherever it sits in the file. */
|
|
37
|
+
export function findBlock(text, id) {
|
|
38
|
+
return findBlocks(text).find((b) => b.id === id);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* absent — no such block in the file.
|
|
42
|
+
* unchanged — on-disk content still matches the recorded hash (Praxis's last
|
|
43
|
+
* emission is intact; safe to update).
|
|
44
|
+
* user-edited — on-disk content differs from the recorded hash; the user (or
|
|
45
|
+
* agent) edited managed content. Never overwrite (D10).
|
|
46
|
+
*/
|
|
47
|
+
export function blockStatus(block) {
|
|
48
|
+
if (!block)
|
|
49
|
+
return "absent";
|
|
50
|
+
return block.recordedHash === hashContent(block.content)
|
|
51
|
+
? "unchanged"
|
|
52
|
+
: "user-edited";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Bring the host text in line with the desired managed content, one block per
|
|
56
|
+
* entry of `desired` (id -> content). Splices only managed spans; appends blocks
|
|
57
|
+
* that are absent; refuses to overwrite blocks the user has edited (conflict).
|
|
58
|
+
* Idempotent: reconciling with already-applied content is a byte-for-byte no-op.
|
|
59
|
+
*/
|
|
60
|
+
export function reconcile(text, desired) {
|
|
61
|
+
let out = text;
|
|
62
|
+
let changed = false;
|
|
63
|
+
const conflicts = [];
|
|
64
|
+
for (const [id, content] of Object.entries(desired)) {
|
|
65
|
+
const block = findBlock(out, id);
|
|
66
|
+
if (!block) {
|
|
67
|
+
out = appendBlock(out, id, content);
|
|
68
|
+
changed = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (blockStatus(block) === "user-edited") {
|
|
72
|
+
conflicts.push(id);
|
|
73
|
+
continue; // never clobber a user edit (D10)
|
|
74
|
+
}
|
|
75
|
+
if (block.content !== content) {
|
|
76
|
+
out = out.slice(0, block.start) + renderBlock(id, content) + out.slice(block.end);
|
|
77
|
+
changed = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { text: out, changed, conflicts };
|
|
81
|
+
}
|
|
82
|
+
function appendBlock(text, id, content) {
|
|
83
|
+
const block = renderBlock(id, content);
|
|
84
|
+
if (text.length === 0)
|
|
85
|
+
return `${block}\n`;
|
|
86
|
+
const sep = text.endsWith("\n") ? "\n" : "\n\n";
|
|
87
|
+
return `${text}${sep}${block}\n`;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.js","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const ARTIFACTS: readonly ["rules", "permissions", "commands", "plugins"];
|
|
3
|
+
export type Artifact = (typeof ARTIFACTS)[number];
|
|
4
|
+
export declare const packageManifestSchema: z.ZodObject<{
|
|
5
|
+
name: z.ZodString;
|
|
6
|
+
layer: z.ZodEnum<{
|
|
7
|
+
layer1: "layer1";
|
|
8
|
+
layer2: "layer2";
|
|
9
|
+
decision: "decision";
|
|
10
|
+
external: "external";
|
|
11
|
+
}>;
|
|
12
|
+
provides: z.ZodArray<z.ZodEnum<{
|
|
13
|
+
rules: "rules";
|
|
14
|
+
permissions: "permissions";
|
|
15
|
+
commands: "commands";
|
|
16
|
+
plugins: "plugins";
|
|
17
|
+
}>>;
|
|
18
|
+
requires: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
19
|
+
conflicts: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
20
|
+
stack: z.ZodOptional<z.ZodEnum<{
|
|
21
|
+
"python-backend": "python-backend";
|
|
22
|
+
node: "node";
|
|
23
|
+
react: "react";
|
|
24
|
+
}>>;
|
|
25
|
+
}, z.core.$strict>;
|
|
26
|
+
export type PackageManifest = z.infer<typeof packageManifestSchema>;
|
|
27
|
+
export interface ResolvedPackage extends PackageManifest {
|
|
28
|
+
/** Absolute path to the package directory (where its artifacts live). */
|
|
29
|
+
dir: string;
|
|
30
|
+
}
|
|
31
|
+
/** Parse + validate a `package.yaml`. `dirName` is the directory the file was
|
|
32
|
+
* found in; the declared `name` must match it. */
|
|
33
|
+
export declare function parsePackageManifest(yamlText: string, dirName: string): PackageManifest;
|
|
34
|
+
/** Every installed package, keyed by name. Scans `packages/<layer>/<pkg>/package.yaml`. */
|
|
35
|
+
export declare function availablePackages(): Map<string, ResolvedPackage>;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the selected package names against what's available, enforcing the
|
|
38
|
+
* composition rule (docs/wiki/packages-and-emit.md): every name must exist, every `requires` must also be
|
|
39
|
+
* selected, no two selected packages may `conflict`, and a package targeting a
|
|
40
|
+
* stack is only valid when the repo declares that stack. Pure — `available` and
|
|
41
|
+
* `declaredStacks` are injected so the logic is testable without the filesystem.
|
|
42
|
+
* Throws an agent-actionable error on the first problem.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolve(selected: string[], available: Map<string, ResolvedPackage>, declaredStacks?: string[]): ResolvedPackage[];
|
|
45
|
+
/** Resolve the selected packages against the installed set (filesystem-backed). */
|
|
46
|
+
export declare function resolvePackages(selected: string[], declaredStacks?: string[]): ResolvedPackage[];
|
|
47
|
+
export {};
|
package/dist/packages.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { STACKS } from "./manifest.js";
|
|
7
|
+
/**
|
|
8
|
+
* The package loader (docs/wiki/packages-and-emit.md, decisions D20/D21). Each package carries
|
|
9
|
+
* a `package.yaml` declaring what it is and **provides**; the loader discovers all
|
|
10
|
+
* installed packages, validates the set selected in `praxis.yaml`, and resolves
|
|
11
|
+
* `requires`/`conflicts`.
|
|
12
|
+
*
|
|
13
|
+
* This replaces filesystem *probing* (try to load each artifact, keep what isn't
|
|
14
|
+
* undefined) with a *declared* contract: a package says what it provides, and an
|
|
15
|
+
* unknown package name fails loudly rather than emitting nothing (D4's "typos
|
|
16
|
+
* fail loud", extended to packages).
|
|
17
|
+
*/
|
|
18
|
+
const PACKAGES_DIR = new URL("../packages/", import.meta.url);
|
|
19
|
+
// What a package can contribute. Closed vocabulary; each maps to an emitter path.
|
|
20
|
+
const ARTIFACTS = ["rules", "permissions", "commands", "plugins"];
|
|
21
|
+
// "external" holds thin packages that declare a Claude Code plugin marketplace
|
|
22
|
+
// (packages/external/<pkg>/) rather than authoring methodology content directly.
|
|
23
|
+
const LAYERS = ["layer1", "layer2", "decision", "external"];
|
|
24
|
+
export const packageManifestSchema = z.strictObject({
|
|
25
|
+
name: z.string().min(1),
|
|
26
|
+
layer: z.enum(LAYERS),
|
|
27
|
+
provides: z.array(z.enum(ARTIFACTS)).min(1),
|
|
28
|
+
requires: z.array(z.string().min(1)).default([]),
|
|
29
|
+
conflicts: z.array(z.string().min(1)).default([]),
|
|
30
|
+
// Layer 2 packages declare the stack they target; a repo only gets them when it
|
|
31
|
+
// declares that stack in praxis.yaml `stacks`. Layer 1 / external packages omit it.
|
|
32
|
+
stack: z.enum(STACKS).optional(),
|
|
33
|
+
});
|
|
34
|
+
/** Parse + validate a `package.yaml`. `dirName` is the directory the file was
|
|
35
|
+
* found in; the declared `name` must match it. */
|
|
36
|
+
export function parsePackageManifest(yamlText, dirName) {
|
|
37
|
+
let raw;
|
|
38
|
+
try {
|
|
39
|
+
raw = parseYaml(yamlText);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
throw new Error(`package.yaml in "${dirName}" is not valid YAML: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
const result = packageManifestSchema.safeParse(raw);
|
|
45
|
+
if (!result.success) {
|
|
46
|
+
const issues = result.error.issues
|
|
47
|
+
.map((i) => ` - ${i.path.map(String).join(".") || "(root)"}: ${i.message}`)
|
|
48
|
+
.join("\n");
|
|
49
|
+
throw new Error(`package.yaml in "${dirName}" is invalid:\n${issues}`);
|
|
50
|
+
}
|
|
51
|
+
if (result.data.name !== dirName) {
|
|
52
|
+
throw new Error(`package.yaml name "${result.data.name}" must match its directory "${dirName}".`);
|
|
53
|
+
}
|
|
54
|
+
return result.data;
|
|
55
|
+
}
|
|
56
|
+
/** Every installed package, keyed by name. Scans `packages/<layer>/<pkg>/package.yaml`. */
|
|
57
|
+
export function availablePackages() {
|
|
58
|
+
const root = fileURLToPath(PACKAGES_DIR);
|
|
59
|
+
const out = new Map();
|
|
60
|
+
for (const layer of readdirSync(root, { withFileTypes: true })) {
|
|
61
|
+
if (!layer.isDirectory())
|
|
62
|
+
continue;
|
|
63
|
+
const layerDir = join(root, layer.name);
|
|
64
|
+
for (const pkg of readdirSync(layerDir, { withFileTypes: true })) {
|
|
65
|
+
if (!pkg.isDirectory())
|
|
66
|
+
continue;
|
|
67
|
+
const dir = join(layerDir, pkg.name);
|
|
68
|
+
const manifestPath = join(dir, "package.yaml");
|
|
69
|
+
if (!existsSync(manifestPath))
|
|
70
|
+
continue; // a directory without a manifest is not a package
|
|
71
|
+
const manifest = parsePackageManifest(readFileSync(manifestPath, "utf8"), pkg.name);
|
|
72
|
+
out.set(manifest.name, { ...manifest, dir });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the selected package names against what's available, enforcing the
|
|
79
|
+
* composition rule (docs/wiki/packages-and-emit.md): every name must exist, every `requires` must also be
|
|
80
|
+
* selected, no two selected packages may `conflict`, and a package targeting a
|
|
81
|
+
* stack is only valid when the repo declares that stack. Pure — `available` and
|
|
82
|
+
* `declaredStacks` are injected so the logic is testable without the filesystem.
|
|
83
|
+
* Throws an agent-actionable error on the first problem.
|
|
84
|
+
*/
|
|
85
|
+
export function resolve(selected, available, declaredStacks = []) {
|
|
86
|
+
const resolved = selected.map((name) => {
|
|
87
|
+
const pkg = available.get(name);
|
|
88
|
+
if (!pkg) {
|
|
89
|
+
throw new Error(`praxis.yaml lists package "${name}" but no such package is installed.\n` +
|
|
90
|
+
`Available packages: ${[...available.keys()].sort().join(", ") || "(none)"}.`);
|
|
91
|
+
}
|
|
92
|
+
return pkg;
|
|
93
|
+
});
|
|
94
|
+
const selectedSet = new Set(selected);
|
|
95
|
+
const stackSet = new Set(declaredStacks);
|
|
96
|
+
for (const pkg of resolved) {
|
|
97
|
+
if (pkg.stack && !stackSet.has(pkg.stack)) {
|
|
98
|
+
throw new Error(`Package "${pkg.name}" targets the "${pkg.stack}" stack, which praxis.yaml does not declare.\n` +
|
|
99
|
+
`Add "${pkg.stack}" to \`stacks\` in praxis.yaml, or remove the package — a Layer 2 ` +
|
|
100
|
+
`recipe applies only when its stack is wanted (D15).`);
|
|
101
|
+
}
|
|
102
|
+
for (const req of pkg.requires) {
|
|
103
|
+
if (!selectedSet.has(req)) {
|
|
104
|
+
throw new Error(`Package "${pkg.name}" requires "${req}", which is not in praxis.yaml.\n` +
|
|
105
|
+
`Add "${req}" to packages — a rule without its required support is broken (D21).`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const con of pkg.conflicts) {
|
|
109
|
+
if (selectedSet.has(con)) {
|
|
110
|
+
throw new Error(`Packages "${pkg.name}" and "${con}" conflict and cannot be installed together.\n` +
|
|
111
|
+
`Remove one from praxis.yaml.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return resolved;
|
|
116
|
+
}
|
|
117
|
+
/** Resolve the selected packages against the installed set (filesystem-backed). */
|
|
118
|
+
export function resolvePackages(selected, declaredStacks = []) {
|
|
119
|
+
return resolve(selected, availablePackages(), declaredStacks);
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=packages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packages.js","sourceRoot":"","sources":["../src/packages.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const CAPABILITIES: readonly ["read-repo", "edit-repo", "run-dev-scripts", "read-only-git", "git-commit", "git-push", "install-deps", "destructive-delete", "force-push", "read-secrets", "global-install"];
|
|
3
|
+
export type Capability = (typeof CAPABILITIES)[number];
|
|
4
|
+
export declare const policySchema: z.ZodObject<{
|
|
5
|
+
allow: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
6
|
+
"read-repo": "read-repo";
|
|
7
|
+
"edit-repo": "edit-repo";
|
|
8
|
+
"run-dev-scripts": "run-dev-scripts";
|
|
9
|
+
"read-only-git": "read-only-git";
|
|
10
|
+
"git-commit": "git-commit";
|
|
11
|
+
"git-push": "git-push";
|
|
12
|
+
"install-deps": "install-deps";
|
|
13
|
+
"destructive-delete": "destructive-delete";
|
|
14
|
+
"force-push": "force-push";
|
|
15
|
+
"read-secrets": "read-secrets";
|
|
16
|
+
"global-install": "global-install";
|
|
17
|
+
}>>>;
|
|
18
|
+
ask: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
19
|
+
"read-repo": "read-repo";
|
|
20
|
+
"edit-repo": "edit-repo";
|
|
21
|
+
"run-dev-scripts": "run-dev-scripts";
|
|
22
|
+
"read-only-git": "read-only-git";
|
|
23
|
+
"git-commit": "git-commit";
|
|
24
|
+
"git-push": "git-push";
|
|
25
|
+
"install-deps": "install-deps";
|
|
26
|
+
"destructive-delete": "destructive-delete";
|
|
27
|
+
"force-push": "force-push";
|
|
28
|
+
"read-secrets": "read-secrets";
|
|
29
|
+
"global-install": "global-install";
|
|
30
|
+
}>>>;
|
|
31
|
+
deny: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
32
|
+
"read-repo": "read-repo";
|
|
33
|
+
"edit-repo": "edit-repo";
|
|
34
|
+
"run-dev-scripts": "run-dev-scripts";
|
|
35
|
+
"read-only-git": "read-only-git";
|
|
36
|
+
"git-commit": "git-commit";
|
|
37
|
+
"git-push": "git-push";
|
|
38
|
+
"install-deps": "install-deps";
|
|
39
|
+
"destructive-delete": "destructive-delete";
|
|
40
|
+
"force-push": "force-push";
|
|
41
|
+
"read-secrets": "read-secrets";
|
|
42
|
+
"global-install": "global-install";
|
|
43
|
+
}>>>;
|
|
44
|
+
}, z.core.$strict>;
|
|
45
|
+
export type Policy = z.infer<typeof policySchema>;
|
|
46
|
+
/** Parse + validate neutral policy text. Throws a readable, agent-actionable
|
|
47
|
+
* error naming each problem — mirrors parseManifest in src/manifest.ts. */
|
|
48
|
+
export declare function parsePolicy(yamlText: string): Policy;
|
|
49
|
+
/** The neutral policy for a content package, or undefined if it provides none.
|
|
50
|
+
* Scans packages/<layer>/<pkg>/permissions.yaml — mirrors loadPackageSource. */
|
|
51
|
+
export declare function loadPolicy(pkg: string): Policy | undefined;
|
|
52
|
+
/** All capabilities referenced by a policy, across every bucket. */
|
|
53
|
+
export declare function policyCapabilities(policy: Policy): Capability[];
|
|
54
|
+
export {};
|