@uniqbit/mate-core 0.18.5 → 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/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 +177 -138
- 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 +176 -3
- 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/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 +5 -13
- 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 +4 -0
- 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 +9 -17
- 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 +48 -40
- package/src/lib/orchestrator/adapters/opencode.ts +2 -0
- 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 -137
- package/src/lib/orchestrator/projection-entries.ts +45 -14
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -0
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +45 -5
- 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 -1
- 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/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 +15 -1
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -11
- 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 -2
- 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/{claude/mate-artifact-publish → agents/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/{agents/mate-artifact-publish → claude/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 +151 -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/install.ts +24 -28
- package/src/tools/setup/engine.ts +97 -21
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +35 -0
- 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 +284 -8
- package/src/tools/setup/providers/opencode.ts +180 -1
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +126 -10
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- package/src/tools/setup/runtime-documents.ts +0 -291
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { pathIsDirectory } from "./fs-utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where a change lives in a Companion Repository: the active `openspec/changes/<name>/`
|
|
8
|
+
* directory, its dated archive `openspec/changes/archive/YYYY-MM-DD-<name>/`, and the
|
|
9
|
+
* canonical specs its delta specs map to. Only directories count, and only names are read.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Companion-relative POSIX paths. */
|
|
13
|
+
export const CHANGES_RELATIVE_DIR = "openspec/changes";
|
|
14
|
+
export const ARCHIVE_RELATIVE_DIR = "openspec/changes/archive";
|
|
15
|
+
export const SPECS_RELATIVE_DIR = "openspec/specs";
|
|
16
|
+
|
|
17
|
+
const ARCHIVE_DIR_NAME = "archive";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One path segment: no separator, NUL, or leading dot (so never `.`, `..`, or hidden).
|
|
21
|
+
* Wider than OpenSpec's kebab-case, so archives made under older rules still resolve.
|
|
22
|
+
*/
|
|
23
|
+
const SEGMENT = /^[^./\\\0][^/\\\0]*$/;
|
|
24
|
+
|
|
25
|
+
/** The `YYYY-MM-DD-<name>` anchor `openspec archive` gives an archived change. */
|
|
26
|
+
const ANCHOR = /^(\d{4}-\d{2}-\d{2})-(.+)$/;
|
|
27
|
+
|
|
28
|
+
export function changesDirOf(companionPath: string): string {
|
|
29
|
+
return path.join(companionPath, ...CHANGES_RELATIVE_DIR.split("/"));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function archiveDirOf(companionPath: string): string {
|
|
33
|
+
return path.join(companionPath, ...ARCHIVE_RELATIVE_DIR.split("/"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A change name or archive anchor: one path segment that is not the archive directory. */
|
|
37
|
+
export function isChangeName(candidate: string): boolean {
|
|
38
|
+
return SEGMENT.test(candidate) && candidate !== ARCHIVE_DIR_NAME;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Anchor {
|
|
42
|
+
date: string;
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `2026-01-02-acme` → `{date: "2026-01-02", name: "acme"}`; null for anything else, `2026-01-02-` included. */
|
|
47
|
+
export function parseAnchor(candidate: string): Anchor | null {
|
|
48
|
+
const match = ANCHOR.exec(candidate);
|
|
49
|
+
if (!match || !isChangeName(match[2])) return null;
|
|
50
|
+
return { date: match[1], name: match[2] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The change name of an archive anchor (`2026-01-02-acme` → `acme`); other names pass through. */
|
|
54
|
+
export function changeNameOf(anchorOrName: string): string {
|
|
55
|
+
return parseAnchor(anchorOrName)?.name ?? anchorOrName;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function directoryNames(dir: string): Promise<string[]> {
|
|
59
|
+
try {
|
|
60
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
61
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Active change names, sorted. */
|
|
68
|
+
export async function activeChanges(companionPath: string): Promise<string[]> {
|
|
69
|
+
return (await directoryNames(changesDirOf(companionPath))).filter(isChangeName).toSorted();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Archive anchors, optionally only those of change `name`, sorted oldest first. */
|
|
73
|
+
export async function archiveAnchors(companionPath: string, name?: string): Promise<string[]> {
|
|
74
|
+
return (await directoryNames(archiveDirOf(companionPath)))
|
|
75
|
+
.filter((anchor) => {
|
|
76
|
+
const parsed = parseAnchor(anchor);
|
|
77
|
+
return parsed !== null && (name === undefined || parsed.name === name);
|
|
78
|
+
})
|
|
79
|
+
.toSorted();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ChangeLocation {
|
|
83
|
+
dir: string;
|
|
84
|
+
archived: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolves a change name or anchor to its directory: the archive an anchor names exactly,
|
|
89
|
+
* else the active change, else the newest archive of that name. Anything that is not a
|
|
90
|
+
* single change-name segment resolves to nothing.
|
|
91
|
+
*/
|
|
92
|
+
export async function locateChange(
|
|
93
|
+
companionPath: string,
|
|
94
|
+
target: string,
|
|
95
|
+
): Promise<ChangeLocation | null> {
|
|
96
|
+
if (!isChangeName(target)) return null;
|
|
97
|
+
const archiveDir = archiveDirOf(companionPath);
|
|
98
|
+
if (parseAnchor(target) && (await pathIsDirectory(path.join(archiveDir, target)))) {
|
|
99
|
+
return { dir: path.join(archiveDir, target), archived: true };
|
|
100
|
+
}
|
|
101
|
+
const active = path.join(changesDirOf(companionPath), target);
|
|
102
|
+
if (await pathIsDirectory(active)) return { dir: active, archived: false };
|
|
103
|
+
const newest = (await archiveAnchors(companionPath, target)).at(-1);
|
|
104
|
+
return newest ? { dir: path.join(archiveDir, newest), archived: true } : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface DeltaSpec {
|
|
108
|
+
/** POSIX path under the change's `specs/`, e.g. `identity/user-auth/spec.md`. */
|
|
109
|
+
file: string;
|
|
110
|
+
/** The companion-relative canonical path the file maps to. */
|
|
111
|
+
canonical: string;
|
|
112
|
+
/** The capability path of a `spec.md` below `specs/`; null for any other file. */
|
|
113
|
+
capability: string | null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Every file below `dir` at any depth, as sorted POSIX paths relative to it. Reads directory
|
|
118
|
+
* entry names only, never contents; an unreadable directory contributes nothing.
|
|
119
|
+
*/
|
|
120
|
+
export async function filesBelow(dir: string): Promise<string[]> {
|
|
121
|
+
const walk = async (current: string, relative: string): Promise<string[]> => {
|
|
122
|
+
let entries;
|
|
123
|
+
try {
|
|
124
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
125
|
+
} catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const nested = await Promise.all(
|
|
129
|
+
entries.map(async (entry) => {
|
|
130
|
+
const entryRelative = relative ? `${relative}/${entry.name}` : entry.name;
|
|
131
|
+
if (entry.isDirectory()) return walk(path.join(current, entry.name), entryRelative);
|
|
132
|
+
return entry.isFile() ? [entryRelative] : [];
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
return nested.flat();
|
|
136
|
+
};
|
|
137
|
+
return (await walk(dir, "")).toSorted();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Every file under the change's `specs/`, at any depth, sorted. Reads directory entry
|
|
142
|
+
* names only, never contents. A change without `specs/` has none.
|
|
143
|
+
*/
|
|
144
|
+
export async function deltaSpecs(changeDir: string): Promise<DeltaSpec[]> {
|
|
145
|
+
const files = await filesBelow(path.join(changeDir, "specs"));
|
|
146
|
+
return files.map((file) => {
|
|
147
|
+
const capability = path.posix.dirname(file);
|
|
148
|
+
return {
|
|
149
|
+
file,
|
|
150
|
+
canonical: `${SPECS_RELATIVE_DIR}/${file}`,
|
|
151
|
+
capability: path.posix.basename(file) === "spec.md" && capability !== "." ? capability : null,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Box, Text } from "ink";
|
|
2
2
|
|
|
3
|
+
import { BRAND_ACCENT } from "../../brand/palette";
|
|
4
|
+
|
|
3
5
|
interface WizardHeaderProps {
|
|
4
6
|
title: string;
|
|
5
7
|
step: number;
|
|
@@ -12,7 +14,7 @@ export function WizardHeader({ title, step, totalSteps }: WizardHeaderProps) {
|
|
|
12
14
|
return (
|
|
13
15
|
<Box borderStyle="round" width="100%">
|
|
14
16
|
<Box flexDirection="column" flexGrow={1} marginLeft={1}>
|
|
15
|
-
<Text bold color=
|
|
17
|
+
<Text bold color={BRAND_ACCENT}>
|
|
16
18
|
mate framework
|
|
17
19
|
</Text>
|
|
18
20
|
<Box justifyContent="space-between">
|
|
@@ -54,9 +54,17 @@ function validateContextModeNodeRuntime(): void {
|
|
|
54
54
|
export async function validateContextModePackage(companionPath: string): Promise<void> {
|
|
55
55
|
validateContextModeNodeRuntime();
|
|
56
56
|
const root = getContextModePackageRoot(companionPath);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
let manifestText: string;
|
|
58
|
+
try {
|
|
59
|
+
manifestText = await fs.readFile(path.join(root, "package.json"), "utf8");
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Expected ${getContextModePackageReference()} at ${root}; it is not installed. Re-run \`mate companion setup\`.`,
|
|
64
|
+
{ cause: error },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const manifest = JSON.parse(manifestText) as { version?: string };
|
|
60
68
|
if (manifest.version !== CONTEXT_MODE_VERSION) {
|
|
61
69
|
throw new Error(
|
|
62
70
|
`Expected ${getContextModePackageReference()} at ${root}; found ${manifest.version ?? "an unknown version"}. Re-run \`mate companion setup\`.`,
|
|
@@ -72,6 +80,15 @@ export async function validateContextModePackage(companionPath: string): Promise
|
|
|
72
80
|
);
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
export const contextModePackageDeps = {
|
|
84
|
+
runInstall: (cwd: string) =>
|
|
85
|
+
spawnSync(
|
|
86
|
+
"npm",
|
|
87
|
+
["install", "--no-audit", "--no-fund", "--loglevel=error", "--registry", PUBLIC_NPM_REGISTRY],
|
|
88
|
+
{ cwd, encoding: "utf8" as const, env: process.env },
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
|
|
75
92
|
export async function installContextModePackage(companionPath: string): Promise<void> {
|
|
76
93
|
validateContextModeNodeRuntime();
|
|
77
94
|
const installDir = getContextModeInstallDir(companionPath);
|
|
@@ -85,15 +102,11 @@ export async function installContextModePackage(companionPath: string): Promise<
|
|
|
85
102
|
) + "\n",
|
|
86
103
|
"utf8",
|
|
87
104
|
);
|
|
88
|
-
const result =
|
|
89
|
-
"npm",
|
|
90
|
-
["install", "--no-audit", "--no-fund", "--silent", "--registry", PUBLIC_NPM_REGISTRY],
|
|
91
|
-
{ cwd: installDir, encoding: "utf8", env: process.env },
|
|
92
|
-
);
|
|
105
|
+
const result = contextModePackageDeps.runInstall(installDir);
|
|
93
106
|
if (result.error || result.status !== 0) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
107
|
+
const cause =
|
|
108
|
+
result.error?.message ?? (result.stderr?.trim() || `npm exited with ${result.status}`);
|
|
109
|
+
throw new Error(`Could not install ${getContextModePackageReference()}: ${cause}`);
|
|
97
110
|
}
|
|
98
111
|
await validateContextModePackage(companionPath);
|
|
99
112
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { locateChange } from "../change-layout";
|
|
2
|
+
import { runOpenSpec, type RunOpenSpec } from "../openspec-cli";
|
|
3
|
+
import { companionRelative, type DeliveryEnvironment } from "./environment";
|
|
4
|
+
import { archiveWarnings, planningWarning, type ArchiveWarning } from "./overlap";
|
|
5
|
+
import {
|
|
6
|
+
isPublishable,
|
|
7
|
+
settleDelivery,
|
|
8
|
+
type DeliveryRecord,
|
|
9
|
+
type DeliverySettlement,
|
|
10
|
+
type UnscopedDelivery,
|
|
11
|
+
} from "./record";
|
|
12
|
+
import {
|
|
13
|
+
recordDelivery,
|
|
14
|
+
type DeliveryEdit,
|
|
15
|
+
type DeliveryRefusal,
|
|
16
|
+
type RecordDeliveryDeps,
|
|
17
|
+
} from "./record-delivery";
|
|
18
|
+
import type { BranchOutcome } from "./refresh";
|
|
19
|
+
|
|
20
|
+
export type ArchiveStatus = "archived" | "needs-branch" | "failed";
|
|
21
|
+
|
|
22
|
+
export interface ArchiveResult {
|
|
23
|
+
status: ArchiveStatus;
|
|
24
|
+
change: string;
|
|
25
|
+
/** Companion-relative archive directory, once archived. */
|
|
26
|
+
path: string | null;
|
|
27
|
+
record: DeliveryRecord;
|
|
28
|
+
settlement: DeliverySettlement;
|
|
29
|
+
outcomes: BranchOutcome[];
|
|
30
|
+
warnings: ArchiveWarning[];
|
|
31
|
+
/** `needs-branch` only: the branch to suggest, when it exists. */
|
|
32
|
+
suggestedBranch?: string | null;
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ArchiveRequest {
|
|
37
|
+
change: string;
|
|
38
|
+
/** The repository `delivery` is recorded for; defaults to the Working Repository. */
|
|
39
|
+
repository?: string;
|
|
40
|
+
/** Recorded before the refresh, as `artifact link` would. */
|
|
41
|
+
delivery?: DeliveryEdit["delivery"];
|
|
42
|
+
/** Archive with an unlinked record instead of answering `needs-branch`. */
|
|
43
|
+
skipBranch?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ArchiveOutcome = { refusal: DeliveryRefusal } | { result: ArchiveResult };
|
|
47
|
+
|
|
48
|
+
export interface ArchiveChangeDeps extends RecordDeliveryDeps {
|
|
49
|
+
runOpenSpec?: RunOpenSpec;
|
|
50
|
+
/**
|
|
51
|
+
* Called once before `openspec archive` starts, so warnings are visible even when the
|
|
52
|
+
* archive fails or hangs. Never called for `needs-branch` or a refusal.
|
|
53
|
+
*/
|
|
54
|
+
onWarnings?: (warnings: ArchiveWarning[], unscoped: UnscopedDelivery[]) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The Archive change stage: records and refreshes the delivery, collects overlap,
|
|
59
|
+
* dependency, and incomplete-planning warnings, then runs `openspec archive`. Without a
|
|
60
|
+
* recorded delivery and no `skipBranch`, nothing is archived and the result is
|
|
61
|
+
* `needs-branch`. Merge state is reported, never a refusal.
|
|
62
|
+
*/
|
|
63
|
+
export async function archiveChange(
|
|
64
|
+
environment: DeliveryEnvironment,
|
|
65
|
+
request: ArchiveRequest,
|
|
66
|
+
deps: ArchiveChangeDeps = {},
|
|
67
|
+
): Promise<ArchiveOutcome> {
|
|
68
|
+
const { change } = request;
|
|
69
|
+
const outcome = await recordDelivery(
|
|
70
|
+
environment,
|
|
71
|
+
{
|
|
72
|
+
changes: [change],
|
|
73
|
+
...(request.repository ? { repository: request.repository } : {}),
|
|
74
|
+
...(request.delivery ? { edit: { delivery: request.delivery } } : {}),
|
|
75
|
+
refresh: true,
|
|
76
|
+
// The settlement covers every repository, so each delivery's merge state is refreshed.
|
|
77
|
+
refreshAll: true,
|
|
78
|
+
requireDelivery: !request.skipBranch,
|
|
79
|
+
},
|
|
80
|
+
deps,
|
|
81
|
+
);
|
|
82
|
+
if ("refusal" in outcome) return outcome;
|
|
83
|
+
|
|
84
|
+
const runner = deps.runOpenSpec ?? runOpenSpec;
|
|
85
|
+
const cwd = environment.companionPath;
|
|
86
|
+
const [overlap, status] = await Promise.all([
|
|
87
|
+
archiveWarnings(environment.companionPath, change),
|
|
88
|
+
runner(["status", "--change", change, "--json"], { cwd }),
|
|
89
|
+
]);
|
|
90
|
+
const planning = status.code === 0 ? planningWarning(change, status.stdout) : null;
|
|
91
|
+
const warnings = [...overlap, ...(planning ? [planning] : [])];
|
|
92
|
+
|
|
93
|
+
if ("needsBranch" in outcome) {
|
|
94
|
+
const { record, suggestedBranch } = outcome.needsBranch;
|
|
95
|
+
return {
|
|
96
|
+
result: {
|
|
97
|
+
status: "needs-branch",
|
|
98
|
+
change,
|
|
99
|
+
path: null,
|
|
100
|
+
record,
|
|
101
|
+
settlement: settleDelivery(record),
|
|
102
|
+
outcomes: [],
|
|
103
|
+
warnings,
|
|
104
|
+
suggestedBranch,
|
|
105
|
+
message: `No delivery branch is recorded for ${change}. Re-run with --branch <branch>, --not-applicable <reason>, or --skip-branch.`,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const [{ record, settlement, outcomes }] = outcome.results;
|
|
111
|
+
deps.onWarnings?.(warnings, settlement.unscoped);
|
|
112
|
+
|
|
113
|
+
const run = await runner(["archive", change, "--yes"], { cwd });
|
|
114
|
+
if (run.code !== 0) {
|
|
115
|
+
const detail = (run.stderr.trim() || run.stdout.trim()).split("\n").slice(-5).join("\n");
|
|
116
|
+
return {
|
|
117
|
+
result: {
|
|
118
|
+
status: "failed",
|
|
119
|
+
change,
|
|
120
|
+
path: null,
|
|
121
|
+
record,
|
|
122
|
+
settlement,
|
|
123
|
+
outcomes,
|
|
124
|
+
warnings,
|
|
125
|
+
message: `openspec archive ${change} failed (exit ${run.code ?? "unknown"}): ${detail}`,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const archived = await locateChange(environment.companionPath, change);
|
|
131
|
+
return {
|
|
132
|
+
result: {
|
|
133
|
+
status: "archived",
|
|
134
|
+
change,
|
|
135
|
+
path: archived?.archived ? companionRelative(environment, archived.dir) : null,
|
|
136
|
+
record,
|
|
137
|
+
settlement,
|
|
138
|
+
outcomes,
|
|
139
|
+
warnings,
|
|
140
|
+
message: archivedMessage(change, settlement),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function archivedMessage(change: string, settlement: DeliverySettlement): string {
|
|
146
|
+
if (isPublishable(settlement.status)) {
|
|
147
|
+
return `Archived ${change}; it can be published with mate-publish-artifact.`;
|
|
148
|
+
}
|
|
149
|
+
const missing =
|
|
150
|
+
settlement.missing.length > 0
|
|
151
|
+
? ` No delivery is recorded for ${settlement.missing.join(", ")}, which its scopes name.`
|
|
152
|
+
: "";
|
|
153
|
+
return `Archived ${change}; publication waits until its delivery is merged (${settlement.status}).${missing}`;
|
|
154
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
import type { CapabilityConfig } from "../orchestrator/types";
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
export type ProtectionReason = "default" | "pattern";
|
|
9
|
+
|
|
10
|
+
export interface BranchProtection {
|
|
11
|
+
protected: boolean;
|
|
12
|
+
reason: ProtectionReason | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProtectionOptions {
|
|
16
|
+
defaultBranch: string;
|
|
17
|
+
/** Configured glob patterns; `*` stays within one path segment, `**` crosses segments. */
|
|
18
|
+
patterns: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function globToRegExp(pattern: string): RegExp {
|
|
22
|
+
const source = pattern
|
|
23
|
+
.split("**")
|
|
24
|
+
.map((part) =>
|
|
25
|
+
part
|
|
26
|
+
.split("*")
|
|
27
|
+
.map((literal) => literal.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
28
|
+
.join("[^/]*"),
|
|
29
|
+
)
|
|
30
|
+
.join(".*");
|
|
31
|
+
return new RegExp(`^${source}$`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A branch is protected when it is the default branch or matches a pattern. */
|
|
35
|
+
export function classifyBranch(branch: string, options: ProtectionOptions): BranchProtection {
|
|
36
|
+
if (branch === options.defaultBranch) return { protected: true, reason: "default" };
|
|
37
|
+
if (options.patterns.some((pattern) => globToRegExp(pattern).test(branch))) {
|
|
38
|
+
return { protected: true, reason: "pattern" };
|
|
39
|
+
}
|
|
40
|
+
return { protected: false, reason: null };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Protected-branch patterns from the companion's openspec capability config. */
|
|
44
|
+
export function configuredProtectedPatterns(capabilities: readonly CapabilityConfig[]): string[] {
|
|
45
|
+
const openspec = capabilities.find((capability) => capability.name === "openspec");
|
|
46
|
+
const patterns = openspec?.protectedBranches;
|
|
47
|
+
return Array.isArray(patterns)
|
|
48
|
+
? patterns.filter((pattern): pattern is string => typeof pattern === "string")
|
|
49
|
+
: [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Read-only local Git queries against a Working Repository. Nothing here touches the network. */
|
|
53
|
+
export interface WorkingRepoGit {
|
|
54
|
+
/** Checked-out branch, or null in a detached HEAD. */
|
|
55
|
+
currentBranch(): Promise<string | null>;
|
|
56
|
+
/** `origin/HEAD`, then an existing `origin/main` or `origin/master`, then `init.defaultBranch`, then `main`. */
|
|
57
|
+
defaultBranch(): Promise<string>;
|
|
58
|
+
/** The `origin` remote URL, or null without one. */
|
|
59
|
+
remoteUrl(): Promise<string | null>;
|
|
60
|
+
/** One-line summaries of commits on HEAD that no remote-tracking branch contains. */
|
|
61
|
+
unpushedCommits(): Promise<string[]>;
|
|
62
|
+
/** True when the branch exists locally or as `origin/<branch>`. */
|
|
63
|
+
branchExists(branch: string): Promise<boolean>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function workingRepoGit(repoPath: string): WorkingRepoGit {
|
|
67
|
+
const run = async (...args: string[]): Promise<string | null> => {
|
|
68
|
+
try {
|
|
69
|
+
const { stdout } = await execFileAsync("git", args, { cwd: repoPath });
|
|
70
|
+
return stdout.trim();
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const remoteTip = async (branch: string): Promise<string | null> =>
|
|
77
|
+
(await run("rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branch}`)) || null;
|
|
78
|
+
const defaultBranch = async (): Promise<string> => {
|
|
79
|
+
const remoteHead = await run("symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD");
|
|
80
|
+
if (remoteHead) return remoteHead.replace(/^origin\//, "");
|
|
81
|
+
for (const candidate of ["main", "master"]) {
|
|
82
|
+
// oxlint-disable-next-line no-await-in-loop -- the steps are a decision chain
|
|
83
|
+
if (await remoteTip(candidate)) return candidate;
|
|
84
|
+
}
|
|
85
|
+
return (await run("config", "--get", "init.defaultBranch")) || "main";
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const currentBranch = async (): Promise<string | null> =>
|
|
89
|
+
(await run("symbolic-ref", "--quiet", "--short", "HEAD")) || null;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
currentBranch,
|
|
93
|
+
defaultBranch,
|
|
94
|
+
async remoteUrl() {
|
|
95
|
+
return (await run("remote", "get-url", "origin")) || null;
|
|
96
|
+
},
|
|
97
|
+
async unpushedCommits() {
|
|
98
|
+
const log = await run("log", "--oneline", "HEAD", "--not", "--remotes");
|
|
99
|
+
return log ? log.split("\n").filter(Boolean) : [];
|
|
100
|
+
},
|
|
101
|
+
async branchExists(branch: string) {
|
|
102
|
+
for (const ref of [`refs/heads/${branch}`, `refs/remotes/origin/${branch}`]) {
|
|
103
|
+
if ((await run("rev-parse", "--verify", "--quiet", ref)) !== null) return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { WorkingRepoGit } from "./branches";
|
|
4
|
+
import type { ForgeRepo } from "./forge";
|
|
5
|
+
|
|
6
|
+
export interface WorkingRepoInfo {
|
|
7
|
+
path: string;
|
|
8
|
+
/** Full repository path from the `origin` remote (`acme/platform/api`), or null without one. */
|
|
9
|
+
slug: string | null;
|
|
10
|
+
forgeRepo: ForgeRepo | null;
|
|
11
|
+
git: WorkingRepoGit;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The companion, the Working Repository with its remote identity, and protected-branch patterns. */
|
|
15
|
+
export interface DeliveryEnvironment {
|
|
16
|
+
companionPath: string;
|
|
17
|
+
workingRepo: WorkingRepoInfo | null;
|
|
18
|
+
protectedPatterns: string[];
|
|
19
|
+
/**
|
|
20
|
+
* Host of a repository other than the Working Repository, from a linked checkout's
|
|
21
|
+
* `origin` remote, else the Working Repository's host; null when neither is known.
|
|
22
|
+
*/
|
|
23
|
+
hostOf?: (repository: string) => Promise<string | null>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `dir` relative to the companion, with forward slashes. */
|
|
27
|
+
export function companionRelative(environment: DeliveryEnvironment, dir: string): string {
|
|
28
|
+
return path.relative(environment.companionPath, dir).split(path.sep).join("/");
|
|
29
|
+
}
|