@uniqbit/mate-core 0.18.4 → 1.0.0-canary.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/claude-plugin/hooks/companion-instructions.mjs +9 -0
- package/claude-plugin/hooks/hooks.json +12 -2
- package/opencode-plugin/src/add-dir.ts +76 -10
- package/opencode-plugin/src/companion.ts +1 -8
- package/opencode-plugin/src/server.ts +2 -15
- package/package.json +10 -2
- package/src/brand/art.generated.ts +69 -0
- package/src/brand/index.ts +12 -0
- package/src/brand/palette.ts +19 -0
- package/src/brand/svg.generated.ts +8 -0
- package/src/brand/terminal.ts +83 -0
- package/src/brand/types.ts +33 -0
- package/src/cli/commands/artifact/artifact.ts +8 -0
- package/src/cli/commands/artifact/delivery/archive.ts +117 -0
- package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
- package/src/cli/commands/artifact/delivery/environment.ts +104 -0
- package/src/cli/commands/artifact/delivery/link.ts +154 -0
- package/src/cli/commands/artifact/delivery/output.ts +58 -0
- package/src/cli/commands/artifact/finish/command.ts +33 -3
- package/src/cli/commands/artifact/finish/engine.ts +47 -3
- package/src/cli/commands/artifact/finish/finisher.ts +10 -0
- package/src/cli/commands/artifact/finish/openspec.ts +200 -129
- package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
- package/src/cli/commands/artifact/pending/command.ts +31 -3
- package/src/cli/commands/artifact/pending/discovery.ts +21 -57
- package/src/cli/commands/cap/graphify.ts +52 -3
- package/src/cli/commands/cap/index-cmd.ts +8 -1
- package/src/cli/commands/cap/openspec.ts +13 -25
- package/src/cli/commands/companion/open.ts +56 -39
- package/src/cli/commands/companion/update.ts +75 -42
- package/src/cli/commands/doctor.ts +257 -78
- package/src/cli/commands/install.ts +0 -8
- package/src/cli/commands/plugin/install.ts +9 -4
- package/src/cli/commands/plugin/plugin.ts +3 -2
- package/src/cli/commands/report/adapter.ts +11 -2
- package/src/cli/commands/report/contract.ts +21 -0
- package/src/cli/commands/report/highlight.ts +34 -4
- package/src/cli/commands/report/markdown.ts +110 -0
- package/src/cli/commands/report/palette.ts +118 -0
- package/src/cli/commands/report/renderer.ts +456 -153
- package/src/cli/commands/report/types.ts +16 -0
- package/src/cli/commands/setup.ts +7 -15
- package/src/cli/commands/studio/client-asset.ts +16 -0
- package/src/cli/commands/studio/openspec-cli.ts +10 -58
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +19 -1
- package/src/cli/commands/studio/server.ts +42 -1
- package/src/cli/commands/studio/terminal-launch.ts +7 -16
- package/src/cli/commands/studio/views/client.ts +6 -616
- package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
- package/src/cli/commands/studio/views/document.tsx +137 -69
- package/src/cli/commands/studio/views/markdown.tsx +250 -0
- package/src/cli/commands/studio/views/palette.ts +134 -0
- package/src/cli/commands/studio/views/reports/index.tsx +2 -7
- package/src/cli/commands/studio/views/skills/index.tsx +43 -62
- package/src/cli/commands/studio/views/specs/index.tsx +7 -15
- package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
- package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
- package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
- package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
- package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
- package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
- package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
- package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
- package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
- package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
- package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
- package/src/cli/commands/studio/views/styles.ts +217 -206
- package/src/cli/commands/studio/views/vault/state.ts +3 -5
- package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
- package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
- package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
- package/src/cli/commands/wrap.ts +9 -42
- package/src/cli/main.ts +4 -1
- package/src/cli/parse-flags.ts +25 -7
- package/src/cli/setup-selector.tsx +38 -162
- package/src/cli/usage.ts +5 -2
- package/src/hooks/companion-instructions.ts +190 -0
- package/src/hooks/hook-io.ts +63 -0
- package/src/hooks/session-banner.ts +21 -29
- package/src/hooks/session-guidance.ts +10 -36
- package/src/hooks/session-prepare.ts +115 -28
- package/src/hooks/validate-artifact-path.ts +82 -24
- package/src/index.ts +4 -0
- package/src/lib/change-layout.ts +154 -0
- package/src/lib/components/wizard-header.tsx +3 -1
- package/src/lib/context-mode-package.ts +24 -11
- package/src/lib/delivery/archive-change.ts +154 -0
- package/src/lib/delivery/branches.ts +108 -0
- package/src/lib/delivery/environment.ts +29 -0
- package/src/lib/delivery/forge.ts +279 -0
- package/src/lib/delivery/overlap.ts +202 -0
- package/src/lib/delivery/record-delivery.ts +196 -0
- package/src/lib/delivery/record.ts +343 -0
- package/src/lib/delivery/refresh.ts +174 -0
- package/src/lib/delivery/repository-access.ts +97 -0
- package/src/lib/delivery/stages.ts +111 -0
- package/src/lib/delivery/token.ts +58 -0
- package/src/lib/frontmatter.ts +18 -0
- package/src/lib/opencode-plugin-package.ts +5 -5
- package/src/lib/openspec-cli.ts +116 -0
- package/src/lib/openspec-scopes.ts +86 -0
- package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
- package/src/lib/orchestrator/adapters/claude.ts +60 -162
- package/src/lib/orchestrator/adapters/opencode.ts +9 -24
- package/src/lib/orchestrator/config-store.ts +2 -70
- package/src/lib/orchestrator/editor.ts +9 -9
- package/src/lib/orchestrator/openspec-store.ts +14 -54
- package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
- package/src/lib/orchestrator/projection-claude-links.ts +159 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +19 -190
- package/src/lib/orchestrator/projection-entries.ts +45 -15
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -2
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +49 -87
- package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +22 -88
- package/src/lib/package-paths.ts +8 -0
- package/src/opencode/companion-hooks.ts +64 -5
- package/src/opencode/companion-policy.ts +37 -18
- package/src/opencode/projected-guidance.ts +4 -1
- package/src/opencode/session-prepare.ts +2 -1
- package/src/opencode/tui.tsx +4 -2
- package/src/runtime/artifact-refusal.ts +63 -0
- package/src/runtime/claude-delivery-plan.ts +89 -7
- package/src/runtime/commit-attribution-guard.ts +181 -0
- package/src/runtime/companion-guidance.ts +15 -3
- package/src/runtime/companion-instructions.ts +340 -0
- package/src/runtime/error-message.ts +4 -0
- package/src/runtime/guidance.ts +0 -17
- package/src/runtime/index.ts +5 -0
- package/src/runtime/managed-block.ts +117 -0
- package/src/runtime/markdown-scan.ts +266 -0
- package/src/runtime/policy.ts +16 -15
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -28
- package/src/runtime/secret-guard.ts +148 -0
- package/src/runtime/secret-patterns.ts +164 -0
- package/src/runtime/secret-scan.ts +90 -0
- package/src/runtime/session-kind.ts +13 -0
- package/src/runtime/shell-heredoc.ts +81 -0
- package/src/runtime/wrap-state.ts +36 -6
- package/src/runtime/write-atomic.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
- package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
- package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/openspec-references.ts +140 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
- package/src/tools/setup/capabilities/context-mode.ts +25 -5
- package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
- package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
- package/src/tools/setup/capabilities/graphify.ts +12 -0
- package/src/tools/setup/capabilities/openspec.ts +273 -270
- package/src/tools/setup/capabilities/tokensave.ts +36 -3
- package/src/tools/setup/context-services.ts +9 -17
- package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
- package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
- package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
- package/src/tools/setup/dynamic-plugins/install.ts +24 -28
- package/src/tools/setup/engine.ts +93 -148
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +37 -17
- package/src/tools/setup/plugins/gitignore.ts +1 -7
- package/src/tools/setup/plugins/guidance.ts +7 -4
- package/src/tools/setup/providers/agent-file-sections.ts +4 -2
- package/src/tools/setup/providers/claude-attribution.ts +65 -0
- package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
- package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
- package/src/tools/setup/providers/claude.ts +285 -34
- package/src/tools/setup/providers/opencode.ts +180 -11
- package/src/tools/setup/registry.ts +2 -24
- package/src/tools/setup/surface-target.ts +4 -13
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +131 -18
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/opencode-plugin/src/audience-layer.ts +0 -91
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- package/src/runtime/audience-layer.ts +0 -84
- package/src/runtime/opencode-audience-layer.ts +0 -176
- package/src/tools/setup/audience-isolation.fixture.ts +0 -325
- package/src/tools/setup/audience-layer.ts +0 -207
- package/src/tools/setup/audience-registrations.ts +0 -52
- package/src/tools/setup/contribution-ledger.ts +0 -207
- package/src/tools/setup/isolation-diagnostics.ts +0 -139
- package/src/tools/setup/runtime-documents.ts +0 -291
- package/src/tools/setup/upgrade-sweep.ts +0 -153
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared by the setup writers and the Companion Instructions reader. In
|
|
3
|
+
* `runtime/` because hooks may import nothing outside it.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fencedRanges, trimBlankLines } from "./markdown-scan";
|
|
7
|
+
|
|
8
|
+
export interface ManagedBlockMarkers {
|
|
9
|
+
start: string;
|
|
10
|
+
end: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function managedBlockMarkers(frameworkName: string, blockKey: string): ManagedBlockMarkers {
|
|
14
|
+
return {
|
|
15
|
+
start: `<!-- ${frameworkName}:managed:${blockKey} start -->`,
|
|
16
|
+
end: `<!-- ${frameworkName}:managed:${blockKey} end -->`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function companionBlockMarkers(frameworkName: string): ManagedBlockMarkers {
|
|
21
|
+
const name = frameworkName.toUpperCase();
|
|
22
|
+
return {
|
|
23
|
+
start: `<!-- ${name}:COMPANION:START -->`,
|
|
24
|
+
end: `<!-- ${name}:COMPANION:END -->`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The plugin owning a block whose key is `<pluginId>:<content hash>`, the
|
|
30
|
+
* shape every setup writer produces; a key without a hash is returned whole.
|
|
31
|
+
*/
|
|
32
|
+
export function managedBlockPluginId(blockKey: string): string {
|
|
33
|
+
const separator = blockKey.lastIndexOf(":");
|
|
34
|
+
return separator === -1 ? blockKey : blockKey.slice(0, separator);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function escapeRegExp(value: string): string {
|
|
38
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Matches a start marker; group 1 is the key. Given `pluginId`, only that plugin's blocks match. */
|
|
42
|
+
export function managedBlockStartPattern(frameworkName: string, pluginId?: string): RegExp {
|
|
43
|
+
const keyPattern = pluginId === undefined ? "[^\\s]+" : `${escapeRegExp(pluginId)}:[0-9a-f]+`;
|
|
44
|
+
return new RegExp(`<!-- ${escapeRegExp(frameworkName)}:managed:(${keyPattern}) start -->`, "g");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ManagedBlock {
|
|
48
|
+
key: string;
|
|
49
|
+
/** Offsets of the whole block, markers included. */
|
|
50
|
+
start: number;
|
|
51
|
+
end: number;
|
|
52
|
+
body: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A marker quoted in a code fence is an example, not a block. */
|
|
56
|
+
function inFence(fenced: readonly [number, number][], index: number): boolean {
|
|
57
|
+
return fenced.some(([from, to]) => index >= from && index < to);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The first `marker` at or after `from` outside a code fence, or -1. */
|
|
61
|
+
function indexOutsideFence(
|
|
62
|
+
text: string,
|
|
63
|
+
marker: string,
|
|
64
|
+
from: number,
|
|
65
|
+
fenced: readonly [number, number][],
|
|
66
|
+
): number {
|
|
67
|
+
let index = text.indexOf(marker, from);
|
|
68
|
+
while (index !== -1 && inFence(fenced, index)) {
|
|
69
|
+
index = text.indexOf(marker, index + marker.length);
|
|
70
|
+
}
|
|
71
|
+
return index;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Offsets of the first `MATE:COMPANION` block, markers included; `null` without one. */
|
|
75
|
+
export function findCompanionBlock(
|
|
76
|
+
text: string,
|
|
77
|
+
frameworkName: string,
|
|
78
|
+
): { start: number; end: number } | null {
|
|
79
|
+
const { start, end } = companionBlockMarkers(frameworkName);
|
|
80
|
+
const fenced = fencedRanges(text);
|
|
81
|
+
const startIndex = indexOutsideFence(text, start, 0, fenced);
|
|
82
|
+
if (startIndex === -1) return null;
|
|
83
|
+
const endIndex = indexOutsideFence(text, end, startIndex + start.length, fenced);
|
|
84
|
+
return endIndex === -1 ? null : { start: startIndex, end: endIndex + end.length };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function findManagedBlocks(text: string, frameworkName: string): ManagedBlock[] {
|
|
88
|
+
const blocks: ManagedBlock[] = [];
|
|
89
|
+
const fenced = fencedRanges(text);
|
|
90
|
+
for (const match of text.matchAll(managedBlockStartPattern(frameworkName))) {
|
|
91
|
+
if (inFence(fenced, match.index)) continue;
|
|
92
|
+
const key = match[1]!;
|
|
93
|
+
const { start, end } = managedBlockMarkers(frameworkName, key);
|
|
94
|
+
const bodyStart = match.index + start.length;
|
|
95
|
+
const endIndex = indexOutsideFence(text, end, bodyStart, fenced);
|
|
96
|
+
if (endIndex === -1) continue;
|
|
97
|
+
const previous = blocks.at(-1);
|
|
98
|
+
if (previous && match.index < previous.end) continue;
|
|
99
|
+
blocks.push({
|
|
100
|
+
key,
|
|
101
|
+
start: match.index,
|
|
102
|
+
end: endIndex + end.length,
|
|
103
|
+
body: trimBlankLines(text.slice(bodyStart, endIndex)),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return blocks;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function stripManagedBlocks(text: string, frameworkName: string): string {
|
|
110
|
+
let result = "";
|
|
111
|
+
let cursor = 0;
|
|
112
|
+
for (const block of findManagedBlocks(text, frameworkName)) {
|
|
113
|
+
result += text.slice(cursor, block.start);
|
|
114
|
+
cursor = block.end;
|
|
115
|
+
}
|
|
116
|
+
return result + text.slice(cursor);
|
|
117
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A line scan of CommonMark, deep enough to find `##` sections, fenced code
|
|
3
|
+
* and block-level HTML comments as Claude Code's memory loading reads them.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** A block quote, or a list item whose content starts `width` columns into its parent's. */
|
|
7
|
+
type Container = { kind: "quote" } | { kind: "item"; width: number };
|
|
8
|
+
|
|
9
|
+
interface Fence {
|
|
10
|
+
char: string;
|
|
11
|
+
length: number;
|
|
12
|
+
/** The containers the fence opened in, outermost first; a line that leaves one ends the fence. */
|
|
13
|
+
containers: readonly Container[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const FENCE_OPEN = /^(`{3,}|~{3,})(.*)$/;
|
|
17
|
+
const FENCE_CLOSE = /^(`{3,}|~{3,})[ \t]*$/;
|
|
18
|
+
const LIST_MARKER = /^(?:[-+*]|\d{1,9}[.)])/;
|
|
19
|
+
const BLANK = /^[ \t]*$/;
|
|
20
|
+
|
|
21
|
+
function indentation(text: string, from: number): { column: number; rest: string } {
|
|
22
|
+
let column = from;
|
|
23
|
+
let index = 0;
|
|
24
|
+
for (; index < text.length; index++) {
|
|
25
|
+
if (text[index] === " ") column++;
|
|
26
|
+
else if (text[index] === "\t") column += 4 - (column % 4);
|
|
27
|
+
else break;
|
|
28
|
+
}
|
|
29
|
+
return { column, rest: text.slice(index) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Where a line's text stands: `rest` starts at `column`; the innermost container's content starts at `base`. */
|
|
33
|
+
interface Position {
|
|
34
|
+
column: number;
|
|
35
|
+
rest: string;
|
|
36
|
+
base: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** CommonMark: `>` after up to three spaces of indentation, and one optional space that belongs to the marker. */
|
|
40
|
+
function afterQuoteMarker({ column, rest, base }: Position): Position | null {
|
|
41
|
+
if (column - base > 3 || !rest.startsWith(">")) return null;
|
|
42
|
+
const marker = column + 1;
|
|
43
|
+
const after = indentation(rest.slice(1), marker);
|
|
44
|
+
return { ...after, base: after.column > marker ? marker + 1 : marker };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The open containers a line continues, outermost first. A blank line continues list items, never a quote. */
|
|
48
|
+
function continueContainers(
|
|
49
|
+
line: string,
|
|
50
|
+
containers: readonly Container[],
|
|
51
|
+
): { matched: Container[]; at: Position } {
|
|
52
|
+
let at: Position = { ...indentation(line, 0), base: 0 };
|
|
53
|
+
const matched: Container[] = [];
|
|
54
|
+
for (const container of containers) {
|
|
55
|
+
if (container.kind === "quote") {
|
|
56
|
+
const next = afterQuoteMarker(at);
|
|
57
|
+
if (!next) break;
|
|
58
|
+
at = next;
|
|
59
|
+
} else {
|
|
60
|
+
if (at.rest !== "" && at.column < at.base + container.width) break;
|
|
61
|
+
at = { ...at, base: at.base + container.width };
|
|
62
|
+
}
|
|
63
|
+
matched.push(container);
|
|
64
|
+
}
|
|
65
|
+
return { matched, at };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The open containers carried from line to line. */
|
|
69
|
+
interface ContainerContext {
|
|
70
|
+
containers: readonly Container[];
|
|
71
|
+
/** A paragraph is open, so a line that leaves its containers may continue it lazily. */
|
|
72
|
+
paragraph: boolean;
|
|
73
|
+
/** The innermost item opened on a blank line, so a blank line next ends it. */
|
|
74
|
+
emptyItem: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const TOP_LEVEL: ContainerContext = { containers: [], paragraph: false, emptyItem: false };
|
|
78
|
+
|
|
79
|
+
/** CommonMark: a line that starts a block, so it ends a paragraph rather than continuing it. */
|
|
80
|
+
const INTERRUPTS = /^(?:`{3,}|~{3,}|#{1,6}(?:[ \t]|$)|>|<!--|(?:[-+*]|\d{1,9}[.)])(?:[ \t]|$))/;
|
|
81
|
+
const THEMATIC_BREAK = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Tracks only as much of CommonMark's block quotes and list items as placing a
|
|
85
|
+
* fence needs. A backtick fence's info string may hold no backtick, so
|
|
86
|
+
* ```` ```a``` ```` is inline code.
|
|
87
|
+
*/
|
|
88
|
+
function lineBlock(
|
|
89
|
+
line: string,
|
|
90
|
+
context: ContainerContext,
|
|
91
|
+
): { fence: Fence | null; context: ContainerContext } {
|
|
92
|
+
const { matched, at } = continueContainers(line, context.containers);
|
|
93
|
+
const continues = matched.length === context.containers.length;
|
|
94
|
+
let { column, rest, base } = at;
|
|
95
|
+
if (rest === "") {
|
|
96
|
+
const containers = continues && context.emptyItem ? matched.slice(0, -1) : matched;
|
|
97
|
+
return { fence: null, context: { containers, paragraph: false, emptyItem: false } };
|
|
98
|
+
}
|
|
99
|
+
if (!continues) {
|
|
100
|
+
const starts = INTERRUPTS.test(rest) || THEMATIC_BREAK.test(rest);
|
|
101
|
+
const lazy = context.paragraph && !(column - base <= 3 && starts);
|
|
102
|
+
if (lazy) return { fence: null, context };
|
|
103
|
+
}
|
|
104
|
+
let containers = matched;
|
|
105
|
+
let paragraph = context.paragraph && continues;
|
|
106
|
+
|
|
107
|
+
while (column - base <= 3 && !THEMATIC_BREAK.test(rest)) {
|
|
108
|
+
const quoted = afterQuoteMarker({ column, rest, base });
|
|
109
|
+
if (quoted) {
|
|
110
|
+
containers = [...containers, { kind: "quote" }];
|
|
111
|
+
({ column, rest, base } = quoted);
|
|
112
|
+
paragraph = false;
|
|
113
|
+
if (rest === "") return { fence: null, context: { containers, paragraph, emptyItem: false } };
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const marker = LIST_MARKER.exec(rest);
|
|
117
|
+
if (!marker) break;
|
|
118
|
+
const markerEnd = column + marker[0].length;
|
|
119
|
+
const after = indentation(rest.slice(marker[0].length), markerEnd);
|
|
120
|
+
const gap = after.column - markerEnd;
|
|
121
|
+
if (gap < 1 && after.rest !== "") break;
|
|
122
|
+
const ordered = marker[0].length > 1;
|
|
123
|
+
if (paragraph && (after.rest === "" || (ordered && Number.parseInt(marker[0], 10) !== 1))) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
const content = gap > 4 || after.rest === "" ? markerEnd + 1 : after.column;
|
|
127
|
+
containers = [...containers, { kind: "item", width: content - base }];
|
|
128
|
+
base = content;
|
|
129
|
+
paragraph = false;
|
|
130
|
+
if (after.rest === "") {
|
|
131
|
+
return { fence: null, context: { containers, paragraph, emptyItem: true } };
|
|
132
|
+
}
|
|
133
|
+
column = after.column;
|
|
134
|
+
rest = after.rest;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (column - base > 3) {
|
|
138
|
+
return { fence: null, context: { containers, paragraph, emptyItem: false } };
|
|
139
|
+
}
|
|
140
|
+
const match = FENCE_OPEN.exec(rest);
|
|
141
|
+
const run = match?.[1];
|
|
142
|
+
if (run && !(run.startsWith("`") && match[2]!.includes("`"))) {
|
|
143
|
+
const fence = { char: run[0]!, length: run.length, containers };
|
|
144
|
+
return { fence, context: { containers, paragraph: false, emptyItem: false } };
|
|
145
|
+
}
|
|
146
|
+
paragraph = !THEMATIC_BREAK.test(rest) && !/^#{1,6}(?:[ \t]|$)/.test(rest);
|
|
147
|
+
return { fence: null, context: { containers, paragraph, emptyItem: false } };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** `ends`: a line that leaves one of the fence's containers ends it; fenced code never continues lazily. */
|
|
151
|
+
function fenceLine(line: string, fence: Fence): "content" | "close" | "ends" {
|
|
152
|
+
const { matched, at } = continueContainers(line, fence.containers);
|
|
153
|
+
if (matched.length < fence.containers.length) return "ends";
|
|
154
|
+
if (at.rest === "") return "content";
|
|
155
|
+
const match = at.column - at.base <= 3 ? FENCE_CLOSE.exec(at.rest) : null;
|
|
156
|
+
return match !== null && match[1]!.startsWith(fence.char) && match[1]!.length >= fence.length
|
|
157
|
+
? "close"
|
|
158
|
+
: "content";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** CommonMark: an ATX heading may be indented by up to three spaces, and be empty. */
|
|
162
|
+
const HEADING = /^ {0,3}##(?:[ \t]+(.*))?$/;
|
|
163
|
+
/** CommonMark: a closing run of `#`s, after a space or tab, is no part of the heading. */
|
|
164
|
+
const CLOSING_SEQUENCE = /(?:^|[ \t]+)#+[ \t]*$/;
|
|
165
|
+
|
|
166
|
+
function headingText(line: string): string | null {
|
|
167
|
+
const match = HEADING.exec(line);
|
|
168
|
+
if (!match) return null;
|
|
169
|
+
return (match[1] ?? "").replace(CLOSING_SEQUENCE, "").trim();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface ScannedLine {
|
|
173
|
+
text: string;
|
|
174
|
+
/** The heading text, `""` when empty, when the line opens a top-level (`##`) section: outside fenced code, list items and block quotes, whose scope it would otherwise leave. */
|
|
175
|
+
heading: string | null;
|
|
176
|
+
/** Inside fenced code, where blank lines are content. */
|
|
177
|
+
fenced: boolean;
|
|
178
|
+
/** Where the source line starts in the scanned text. */
|
|
179
|
+
offset: number;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** CommonMark: an HTML block opens on a line that begins, after up to three spaces, with `<!--`. */
|
|
183
|
+
const BLOCK_COMMENT_OPEN = /^ {0,3}<!--/;
|
|
184
|
+
/** Claude Code's own pattern for the comments it removes from such a block. */
|
|
185
|
+
const CLOSED_COMMENT = /<!--[\s\S]*?-->/g;
|
|
186
|
+
|
|
187
|
+
type ScanState = { in: "prose" } | { in: "fence"; fence: Fence } | { in: "comment" };
|
|
188
|
+
|
|
189
|
+
const PROSE: ScanState = { in: "prose" };
|
|
190
|
+
const COMMENT: ScanState = { in: "comment" };
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Strips block-level HTML comments as Claude Code's memory loading does: only
|
|
194
|
+
* an HTML block that opens with `<!--` and holds a `-->`, so a `<!--` inside
|
|
195
|
+
* prose, inside fenced code, or never closed stays. The rest of a closing line
|
|
196
|
+
* is still part of the HTML block, so it opens no heading and no fence.
|
|
197
|
+
*/
|
|
198
|
+
export function scanLinesWithoutComments(text: string): ScannedLine[] {
|
|
199
|
+
const lines: ScannedLine[] = [];
|
|
200
|
+
let state = PROSE;
|
|
201
|
+
let list = TOP_LEVEL;
|
|
202
|
+
let offset = 0;
|
|
203
|
+
|
|
204
|
+
const emit = (line: string): void => {
|
|
205
|
+
const next = lineBlock(line, list);
|
|
206
|
+
const heading = next.context.containers.length === 0 ? headingText(line) : null;
|
|
207
|
+
lines.push({ text: line, heading, fenced: false, offset });
|
|
208
|
+
list = next.context;
|
|
209
|
+
state = next.fence ? { in: "fence", fence: next.fence } : PROSE;
|
|
210
|
+
};
|
|
211
|
+
/** A comment's lines are one HTML block: it ends the containers it leaves. */
|
|
212
|
+
const enterComment = (line: string): void => {
|
|
213
|
+
list = { ...lineBlock(line, list).context, paragraph: false };
|
|
214
|
+
};
|
|
215
|
+
const emitAfterClose = (line: string, close: number): void => {
|
|
216
|
+
const rest = line.slice(close + 3).replace(CLOSED_COMMENT, "");
|
|
217
|
+
lines.push({ text: rest, heading: null, fenced: false, offset });
|
|
218
|
+
state = PROSE;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
for (const raw of text.split("\n")) {
|
|
222
|
+
const inFence = state.in === "fence" ? fenceLine(raw, state.fence) : "ends";
|
|
223
|
+
if (inFence !== "ends") {
|
|
224
|
+
lines.push({ text: raw, heading: null, fenced: true, offset });
|
|
225
|
+
if (inFence === "close") state = PROSE;
|
|
226
|
+
} else if (state.in === "comment") {
|
|
227
|
+
const close = raw.indexOf("-->");
|
|
228
|
+
if (close !== -1) emitAfterClose(raw, close);
|
|
229
|
+
} else if (BLOCK_COMMENT_OPEN.test(raw)) {
|
|
230
|
+
const close = text.indexOf("-->", offset + raw.indexOf("<!--") + 4);
|
|
231
|
+
if (close === -1) emit(raw);
|
|
232
|
+
else {
|
|
233
|
+
enterComment(raw);
|
|
234
|
+
if (close < offset + raw.length) emitAfterClose(raw, close - offset);
|
|
235
|
+
else state = COMMENT;
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
emit(raw);
|
|
239
|
+
}
|
|
240
|
+
offset += raw.length + 1;
|
|
241
|
+
}
|
|
242
|
+
return lines;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Offsets `[start, end)` of the lines inside fenced code. */
|
|
246
|
+
export function fencedRanges(text: string): [number, number][] {
|
|
247
|
+
return scanLinesWithoutComments(text)
|
|
248
|
+
.filter((line) => line.fenced)
|
|
249
|
+
.map((line) => [line.offset, line.offset + line.text.length]);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function joinLines(lines: readonly ScannedLine[]): string {
|
|
253
|
+
const kept: string[] = [];
|
|
254
|
+
let afterBlank = false;
|
|
255
|
+
for (const line of lines) {
|
|
256
|
+
const blank = !line.fenced && BLANK.test(line.text);
|
|
257
|
+
if (!(blank && afterBlank)) kept.push(blank ? "" : line.text);
|
|
258
|
+
afterBlank = blank;
|
|
259
|
+
}
|
|
260
|
+
return trimBlankLines(kept.join("\n"));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Drops the blank lines around a text but no indentation: an indented fence opener keeps its meaning. */
|
|
264
|
+
export function trimBlankLines(text: string): string {
|
|
265
|
+
return text.replace(/^(?:[ \t]*\n)+/, "").trimEnd();
|
|
266
|
+
}
|
package/src/runtime/policy.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
|
|
6
6
|
import { FRAMEWORK_NAME } from "./framework";
|
|
7
|
+
import { defaultSecretPolicy, parseSecretsConfig, type SecretPolicy } from "./secret-patterns";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Predicates that are never projected: they are read live from the companion
|
|
@@ -14,12 +15,18 @@ export interface CompanionPolicy {
|
|
|
14
15
|
/** Names of the Capabilities the companion has enabled. */
|
|
15
16
|
enabledCapabilities: string[];
|
|
16
17
|
gitAutoMode: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Secret guardrails. Unlike the other fields, a missing or broken config
|
|
20
|
+
* yields guardrails on: protection never fails open.
|
|
21
|
+
*/
|
|
22
|
+
secrets: SecretPolicy;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
export const emptyCompanionPolicy = (): CompanionPolicy => ({
|
|
20
26
|
allowedAgents: [],
|
|
21
27
|
enabledCapabilities: [],
|
|
22
28
|
gitAutoMode: false,
|
|
29
|
+
secrets: defaultSecretPolicy(),
|
|
23
30
|
});
|
|
24
31
|
|
|
25
32
|
export function companionFrameworkConfigPath(companionPath: string): string {
|
|
@@ -40,20 +47,10 @@ function capabilityNames(value: unknown): string[] {
|
|
|
40
47
|
.filter((name): name is string => typeof name === "string");
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
/**
|
|
44
|
-
* A base-set key from `audiences.base`, the shape a companion is saved in. The
|
|
45
|
-
* lenient twin of the setup normalizer, which `runtime/` cannot import.
|
|
46
|
-
*/
|
|
47
|
-
function baseAudienceKey(config: Record<string, unknown>, key: string): unknown {
|
|
48
|
-
const audiences = config.audiences;
|
|
49
|
-
if (!audiences || typeof audiences !== "object") return undefined;
|
|
50
|
-
const base = (audiences as Record<string, unknown>).base;
|
|
51
|
-
return base && typeof base === "object" ? (base as Record<string, unknown>)[key] : undefined;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
50
|
/**
|
|
55
51
|
* Synchronous so it composes into `readCompanionRuntimeContext`. A missing or
|
|
56
|
-
* unparseable companion config yields an inert policy rather than throwing
|
|
52
|
+
* unparseable companion config yields an inert policy rather than throwing,
|
|
53
|
+
* with secret guardrails active.
|
|
57
54
|
*/
|
|
58
55
|
export function readCompanionPolicy(companionPath: string): CompanionPolicy {
|
|
59
56
|
let parsed: unknown;
|
|
@@ -67,13 +64,17 @@ export function readCompanionPolicy(companionPath: string): CompanionPolicy {
|
|
|
67
64
|
const config = parsed as Record<string, unknown>;
|
|
68
65
|
return {
|
|
69
66
|
allowedAgents: stringList(config.allowedAgents),
|
|
70
|
-
enabledCapabilities: capabilityNames(
|
|
71
|
-
config.capabilities ?? baseAudienceKey(config, "capabilities"),
|
|
72
|
-
),
|
|
67
|
+
enabledCapabilities: capabilityNames(config.capabilities),
|
|
73
68
|
gitAutoMode: config.git === "auto",
|
|
69
|
+
secrets: parseSecretsConfig(config.secrets),
|
|
74
70
|
};
|
|
75
71
|
}
|
|
76
72
|
|
|
73
|
+
/** The companion's live secret policy, read on every call. */
|
|
74
|
+
export function resolveSecretPolicy(companionPath: string): SecretPolicy {
|
|
75
|
+
return readCompanionPolicy(companionPath).secrets;
|
|
76
|
+
}
|
|
77
|
+
|
|
77
78
|
export function isCapabilityEnabled(policy: CompanionPolicy, name: string): boolean {
|
|
78
79
|
return policy.enabledCapabilities.includes(name);
|
|
79
80
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks the hook copies a Claude wrap inlines into a Working Repository's
|
|
3
|
+
* settings. A managed launch loads the bundled plugin's copy of the same hooks
|
|
4
|
+
* against the same environment, so nothing inside the process tells the two
|
|
5
|
+
* apart but this flag.
|
|
6
|
+
*/
|
|
7
|
+
export const PROJECTED_FLAG = "--projected";
|
|
@@ -38,6 +38,7 @@ export function buildProjectedGuidance(
|
|
|
38
38
|
companionPath: projection.companionPath,
|
|
39
39
|
repository: { id: projection.repositoryId, path: projection.repositoryPath },
|
|
40
40
|
capabilities: policy.enabledCapabilities.map((name) => ({ name })),
|
|
41
|
+
secretGuardrails: policy.secrets.guardrails,
|
|
41
42
|
},
|
|
42
43
|
/** The projected path, not the running install's: a wrap may be another mate's. */
|
|
43
44
|
{ wrapperBinPath: projection.wrapperBinPath },
|
|
@@ -62,6 +62,30 @@ export interface RenderedRuntimeDocument {
|
|
|
62
62
|
readonly regions: readonly ManagedRegion[];
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** What a working-target render returns: its warnings are data for the caller, never printed. */
|
|
66
|
+
export interface RenderedRuntimeDocuments {
|
|
67
|
+
documents: RenderedRuntimeDocument[];
|
|
68
|
+
warnings: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function nonEmpty(region: ManagedRegion): boolean {
|
|
72
|
+
if (region.kind === "list") return region.values.length > 0;
|
|
73
|
+
/** A stripping map still heals what an older Mate left, with no entry of its own. */
|
|
74
|
+
if (region.kind === "map") {
|
|
75
|
+
return Object.keys(region.entries).length > 0 || region.stripPrefix !== undefined;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One rendered document of its non-empty regions, or none when every region is empty. */
|
|
81
|
+
export function renderedDocument(
|
|
82
|
+
documentPath: string,
|
|
83
|
+
regions: ManagedRegion[],
|
|
84
|
+
): RenderedRuntimeDocument[] {
|
|
85
|
+
const kept = regions.filter(nonEmpty);
|
|
86
|
+
return kept.length > 0 ? [{ path: documentPath, regions: kept }] : [];
|
|
87
|
+
}
|
|
88
|
+
|
|
65
89
|
/** An owned companion copy: the hash of what Mate last wrote there. */
|
|
66
90
|
export interface CopyRecord {
|
|
67
91
|
runtime: WrapRuntime;
|
|
@@ -80,8 +104,6 @@ export interface Manifest {
|
|
|
80
104
|
readonly runtimes: Readonly<Record<string, WrapRuntime>>;
|
|
81
105
|
readonly copies: Readonly<Record<string, CopyRecord>>;
|
|
82
106
|
readonly links: Readonly<Record<string, LinkRecord>>;
|
|
83
|
-
/** The audience `mate wrap --audience` recorded; the only source of a bare session's audience. */
|
|
84
|
-
readonly audience?: string;
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
export const EMPTY_MANIFEST: Manifest = Object.freeze({
|
|
@@ -126,23 +148,9 @@ function manifestOf(parsed: Record<string, unknown>): Manifest {
|
|
|
126
148
|
runtimes: isRecord(parsed.runtimes) ? (parsed.runtimes as Manifest["runtimes"]) : {},
|
|
127
149
|
copies: isRecord(parsed.copies) ? (parsed.copies as Manifest["copies"]) : {},
|
|
128
150
|
links: isRecord(parsed.links) ? (parsed.links as Manifest["links"]) : {},
|
|
129
|
-
...(typeof parsed.audience === "string" && parsed.audience
|
|
130
|
-
? { audience: parsed.audience }
|
|
131
|
-
: {}),
|
|
132
151
|
};
|
|
133
152
|
}
|
|
134
153
|
|
|
135
|
-
/** The audience the checkout is wrapped for; `null` for base only. */
|
|
136
|
-
export function recordedAudience(manifest: Manifest): string | null {
|
|
137
|
-
return manifest.audience ?? null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Records the checkout's audience, or none with `null`. */
|
|
141
|
-
export function withAudience(manifest: Manifest, audience: string | null): Manifest {
|
|
142
|
-
const { audience: _previous, ...rest } = manifest;
|
|
143
|
-
return audience ? { ...rest, audience } : rest;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
154
|
function recordedRuntime(manifest: Manifest, documentPath: string): WrapRuntime {
|
|
147
155
|
return manifest.runtimes[documentPath] ?? documentRuntime(documentPath);
|
|
148
156
|
}
|
|
@@ -166,7 +174,6 @@ export function serializeManifest(manifest: Manifest): string | null {
|
|
|
166
174
|
...(recorded.length > 0 ? { runtimes } : {}),
|
|
167
175
|
...(copied.length > 0 ? { copies: manifest.copies } : {}),
|
|
168
176
|
...(linked.length > 0 ? { links: manifest.links } : {}),
|
|
169
|
-
...(manifest.audience ? { audience: manifest.audience } : {}),
|
|
170
177
|
},
|
|
171
178
|
null,
|
|
172
179
|
2,
|
|
@@ -202,14 +209,24 @@ export function copies(manifest: Manifest, runtime?: WrapRuntime): Record<string
|
|
|
202
209
|
);
|
|
203
210
|
}
|
|
204
211
|
|
|
212
|
+
/** Narrows link records to one link root, e.g. `.claude/skills/`. */
|
|
213
|
+
export interface LinkScope {
|
|
214
|
+
prefix?: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
205
217
|
/** Links Mate created, keyed by repo-relative POSIX path. */
|
|
206
|
-
export function links(
|
|
218
|
+
export function links(
|
|
219
|
+
manifest: Manifest,
|
|
220
|
+
runtime?: WrapRuntime,
|
|
221
|
+
scope: LinkScope = {},
|
|
222
|
+
): Record<string, LinkRecord> {
|
|
207
223
|
return Object.fromEntries(
|
|
208
224
|
Object.entries(manifest.links).filter(
|
|
209
|
-
([, record]) =>
|
|
225
|
+
([key, record]) =>
|
|
210
226
|
isRecord(record) &&
|
|
211
227
|
typeof record.target === "string" &&
|
|
212
|
-
(!runtime || record.runtime === runtime)
|
|
228
|
+
(!runtime || record.runtime === runtime) &&
|
|
229
|
+
key.startsWith(scope.prefix ?? ""),
|
|
213
230
|
),
|
|
214
231
|
);
|
|
215
232
|
}
|
|
@@ -232,11 +249,23 @@ export function recordedMapKeys(
|
|
|
232
249
|
documentPath: string,
|
|
233
250
|
at: readonly string[],
|
|
234
251
|
): string[] {
|
|
235
|
-
return (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
252
|
+
return Object.keys(recordedMapEntries(manifest, documentPath, at));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The entries of the `map` regions recorded at `at` in one document, checked like {@link recordedMapKeys}. */
|
|
256
|
+
export function recordedMapEntries(
|
|
257
|
+
manifest: Manifest,
|
|
258
|
+
documentPath: string,
|
|
259
|
+
at: readonly string[],
|
|
260
|
+
): Record<string, unknown> {
|
|
261
|
+
return Object.assign(
|
|
262
|
+
{},
|
|
263
|
+
...(documentRegions(manifest, documentPath) ?? []).flatMap((region: unknown) =>
|
|
264
|
+
isRecord(region) && region.kind === "map" && isAt(region.at, at) && isRecord(region.entries)
|
|
265
|
+
? [region.entries]
|
|
266
|
+
: [],
|
|
267
|
+
),
|
|
268
|
+
) as Record<string, unknown>;
|
|
240
269
|
}
|
|
241
270
|
|
|
242
271
|
/** Every value of the `list` regions recorded under `prefix` in one document. */
|
|
@@ -279,14 +308,15 @@ export function withoutDocuments(manifest: Manifest, documentPaths: readonly str
|
|
|
279
308
|
};
|
|
280
309
|
}
|
|
281
310
|
|
|
282
|
-
/** Replaces one runtime's records
|
|
311
|
+
/** Replaces one runtime's records under `prefix`, leaving every other record as it is. */
|
|
283
312
|
function replaceRuntimeRecords<T>(
|
|
284
313
|
existing: Readonly<Record<string, T>>,
|
|
285
314
|
runtime: WrapRuntime,
|
|
286
315
|
records: Readonly<Record<string, T>>,
|
|
316
|
+
prefix = "",
|
|
287
317
|
): Record<string, T> {
|
|
288
318
|
const kept = Object.entries(existing).filter(
|
|
289
|
-
([, record]) => !(isRecord(record) && record.runtime === runtime),
|
|
319
|
+
([key, record]) => !(isRecord(record) && record.runtime === runtime && key.startsWith(prefix)),
|
|
290
320
|
);
|
|
291
321
|
return { ...Object.fromEntries(kept), ...records };
|
|
292
322
|
}
|
|
@@ -303,8 +333,12 @@ export function withLinks(
|
|
|
303
333
|
manifest: Manifest,
|
|
304
334
|
runtime: WrapRuntime,
|
|
305
335
|
records: Readonly<Record<string, LinkRecord>>,
|
|
336
|
+
scope: LinkScope = {},
|
|
306
337
|
): Manifest {
|
|
307
|
-
return {
|
|
338
|
+
return {
|
|
339
|
+
...manifest,
|
|
340
|
+
links: replaceRuntimeRecords(manifest.links, runtime, records, scope.prefix),
|
|
341
|
+
};
|
|
308
342
|
}
|
|
309
343
|
|
|
310
344
|
export function manifestPath(repoPath: string): string {
|