@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,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Thin shim for src/hooks/companion-instructions.ts, loaded through ts-loader.mjs: the
|
|
4
|
+
* installed plugin sits inside node_modules, where node's native type stripping is off.
|
|
5
|
+
*/
|
|
6
|
+
import "./ts-loader.mjs";
|
|
7
|
+
|
|
8
|
+
const { run } = await import("../../src/hooks/companion-instructions.ts");
|
|
9
|
+
process.exitCode = await run();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Mate hooks \u2014 PreToolUse artifact-path guardrail, SessionStart banner, companion guidance
|
|
2
|
+
"description": "Mate hooks \u2014 PreToolUse artifact-path and secret guardrail, SessionStart banner, companion guidance, session prepare then companion instructions, SubagentStart companion instructions",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
6
|
-
"matcher": "Write|Edit|MultiEdit|Bash",
|
|
6
|
+
"matcher": "Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Bash",
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
@@ -38,6 +38,16 @@
|
|
|
38
38
|
}
|
|
39
39
|
]
|
|
40
40
|
}
|
|
41
|
+
],
|
|
42
|
+
"SubagentStart": [
|
|
43
|
+
{
|
|
44
|
+
"hooks": [
|
|
45
|
+
{
|
|
46
|
+
"type": "command",
|
|
47
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/companion-instructions.mjs\""
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
41
51
|
]
|
|
42
52
|
}
|
|
43
53
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
|
|
3
4
|
import type { Context } from "@opencode/plugin/promise/plugin";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
type CompanionSkill = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
path: string;
|
|
11
|
+
content: string;
|
|
12
|
+
};
|
|
6
13
|
|
|
7
14
|
function companionSkillPaths(companionPath: string): string[] {
|
|
8
15
|
return [
|
|
@@ -16,28 +23,87 @@ function isWithin(root: string, candidate: string): boolean {
|
|
|
16
23
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
17
24
|
}
|
|
18
25
|
|
|
26
|
+
function parseFrontmatter(content: string): { name?: string; description?: string } {
|
|
27
|
+
const match = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
28
|
+
if (!match) return {};
|
|
29
|
+
|
|
30
|
+
const scalar = (key: string) => {
|
|
31
|
+
const value = match[1].match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim();
|
|
32
|
+
return value?.replace(
|
|
33
|
+
/^(?:"([\s\S]*)"|'([\s\S]*)')$/,
|
|
34
|
+
(_all, doubleQuoted, singleQuoted) => doubleQuoted ?? singleQuoted,
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return { name: scalar("name"), description: scalar("description") };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function findSkillFiles(directory: string): Promise<string[]> {
|
|
42
|
+
try {
|
|
43
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
44
|
+
const files: string[] = [];
|
|
45
|
+
const subdirectories: Promise<string[]>[] = [];
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const entryPath = path.join(directory, entry.name);
|
|
48
|
+
if (entry.isFile() && entry.name === "SKILL.md") files.push(entryPath);
|
|
49
|
+
else if (entry.isDirectory()) subdirectories.push(findSkillFiles(entryPath));
|
|
50
|
+
}
|
|
51
|
+
const nested = await Promise.all(subdirectories);
|
|
52
|
+
return [...files, ...nested.flat()];
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function skillID(name: string): string {
|
|
59
|
+
return (
|
|
60
|
+
name
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
63
|
+
.replace(/^-+|-+$/g, "") || "mate-skill"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function loadCompanionSkills(companionPath: string): Promise<CompanionSkill[]> {
|
|
68
|
+
const files = (await Promise.all(companionSkillPaths(companionPath).map(findSkillFiles))).flat();
|
|
69
|
+
const skills = await Promise.all(
|
|
70
|
+
files.map(async (filePath): Promise<CompanionSkill | null> => {
|
|
71
|
+
try {
|
|
72
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
73
|
+
const metadata = parseFrontmatter(content);
|
|
74
|
+
const name = metadata.name || path.basename(path.dirname(filePath));
|
|
75
|
+
return {
|
|
76
|
+
id: skillID(name),
|
|
77
|
+
name,
|
|
78
|
+
...(metadata.description ? { description: metadata.description } : {}),
|
|
79
|
+
path: filePath,
|
|
80
|
+
content,
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
/** An unreadable companion skill must not prevent the session from loading. */
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
return skills.filter((skill) => skill !== null);
|
|
89
|
+
}
|
|
90
|
+
|
|
19
91
|
/**
|
|
20
92
|
* Grants the session access to the companion and registers its skills. The
|
|
21
93
|
* permission hook only widens access for companion resources; every other
|
|
22
94
|
* evaluation keeps OpenCode's own verdict.
|
|
23
95
|
*/
|
|
24
|
-
export async function registerCompanionAccess(
|
|
25
|
-
api: Context,
|
|
26
|
-
companionPath: string,
|
|
27
|
-
extraRoots: readonly string[] = [],
|
|
28
|
-
): Promise<void> {
|
|
96
|
+
export async function registerCompanionAccess(api: Context, companionPath: string): Promise<void> {
|
|
29
97
|
const permissionRoot = path.resolve(companionPath);
|
|
30
|
-
/** An audience layer's files are as readable as the companion's, for this launch only. */
|
|
31
|
-
const allowedRoots = [permissionRoot, ...extraRoots.map((root) => path.resolve(root))];
|
|
32
98
|
|
|
33
99
|
await api.permission.hook("evaluate", (event) => {
|
|
34
|
-
if (event.resources.some((resource) =>
|
|
100
|
+
if (event.resources.some((resource) => isWithin(permissionRoot, resource))) {
|
|
35
101
|
event.effect = "allow";
|
|
36
102
|
delete event.message;
|
|
37
103
|
}
|
|
38
104
|
});
|
|
39
105
|
|
|
40
|
-
const skills = await
|
|
106
|
+
const skills = await loadCompanionSkills(permissionRoot);
|
|
41
107
|
if (skills.length === 0) return;
|
|
42
108
|
|
|
43
109
|
await api.skill.transform((editor) => {
|
|
@@ -77,7 +77,6 @@ function buildSystemPrompt(
|
|
|
77
77
|
context: CompanionContext,
|
|
78
78
|
guidance: MateGuidanceFile,
|
|
79
79
|
wrapperBinPath: string,
|
|
80
|
-
layerGuidance: string,
|
|
81
80
|
): string[] {
|
|
82
81
|
const lines = [materializeCompanionGuidance(guidance.companionGuidance, context, wrapperBinPath)];
|
|
83
82
|
|
|
@@ -89,11 +88,6 @@ function buildSystemPrompt(
|
|
|
89
88
|
lines.push("", `<agents.md>${context.agentsMd.trim()}</agents.md>`);
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
/** The audience's guidance follows the companion's, never replaces it. */
|
|
93
|
-
if (layerGuidance.trim()) {
|
|
94
|
-
lines.push("", layerGuidance.trim());
|
|
95
|
-
}
|
|
96
|
-
|
|
97
91
|
return lines;
|
|
98
92
|
}
|
|
99
93
|
|
|
@@ -188,7 +182,6 @@ export async function registerCompanion(
|
|
|
188
182
|
api: Context,
|
|
189
183
|
context: CompanionContext,
|
|
190
184
|
env: LaunchEnvironment = process.env,
|
|
191
|
-
layerGuidance = "",
|
|
192
185
|
): Promise<void> {
|
|
193
186
|
const guidance = loadGuidance(env, api.location.directory);
|
|
194
187
|
if (!guidance) return;
|
|
@@ -199,7 +192,7 @@ export async function registerCompanion(
|
|
|
199
192
|
return;
|
|
200
193
|
}
|
|
201
194
|
|
|
202
|
-
const companion = buildSystemPrompt(context, guidance, wrapperBinPath
|
|
195
|
+
const companion = buildSystemPrompt(context, guidance, wrapperBinPath).join("\n");
|
|
203
196
|
|
|
204
197
|
await api.session.hook("context", (event) => {
|
|
205
198
|
const merged = mergeCompanionSystem(event.system, companion);
|
|
@@ -11,11 +11,6 @@ import { spawnPrepareRunner } from "../../src/runtime/session-prepare-runner";
|
|
|
11
11
|
import { readWrapStateSync } from "../../src/runtime/wrap-state";
|
|
12
12
|
|
|
13
13
|
import { registerCompanionAccess } from "./add-dir";
|
|
14
|
-
import {
|
|
15
|
-
readTrustedAudienceLayer,
|
|
16
|
-
readWrappedAudienceLayer,
|
|
17
|
-
registerAudienceLayer,
|
|
18
|
-
} from "./audience-layer";
|
|
19
14
|
import { missingRuntimeDirectoryMessage, registerCompanion, resolveMateBin } from "./companion";
|
|
20
15
|
import { pluginLaunchEnvironment, type LaunchEnvironment } from "./launch-environment";
|
|
21
16
|
|
|
@@ -56,17 +51,9 @@ export const MateOpenCodePlugin = Plugin.define({
|
|
|
56
51
|
const context = readContext(env, api.location.directory);
|
|
57
52
|
if (!context.companionPath) return;
|
|
58
53
|
|
|
59
|
-
|
|
60
|
-
* A layer comes from a payload this launch trusts, else from the wrap of
|
|
61
|
-
* the session's checkout; an inherited environment never names one.
|
|
62
|
-
*/
|
|
63
|
-
const layer =
|
|
64
|
-
(await readTrustedAudienceLayer(env)) ??
|
|
65
|
-
(await readWrappedAudienceLayer(api.location.directory, context.companionPath));
|
|
66
|
-
await registerCompanionAccess(api, context.companionPath, layer ? [layer.root] : []);
|
|
54
|
+
await registerCompanionAccess(api, context.companionPath);
|
|
67
55
|
await registerCompanionHooks(api, context, env);
|
|
68
|
-
await registerCompanion(api, context, env
|
|
69
|
-
if (layer) await registerAudienceLayer(api, layer.contents);
|
|
56
|
+
await registerCompanion(api, context, env);
|
|
70
57
|
/** Last: the guardrails above are live while the local phase holds `setup`. */
|
|
71
58
|
const prepareCleanup = await registerPrepare(api, context.companionPath, env);
|
|
72
59
|
return async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniqbit/mate-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-canary.0",
|
|
4
4
|
"description": "Core framework and plugin APIs for Mate.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"./runtime": "./src/runtime/index.ts",
|
|
25
25
|
"./opencode": "./src/opencode/index.ts",
|
|
26
26
|
"./opencode/tui": "./src/opencode/tui.tsx",
|
|
27
|
-
"./tui": "./src/tui.ts"
|
|
27
|
+
"./tui": "./src/tui.ts",
|
|
28
|
+
"./brand": "./src/brand/index.ts"
|
|
28
29
|
},
|
|
29
30
|
"publishConfig": {
|
|
30
31
|
"access": "public",
|
|
@@ -32,6 +33,8 @@
|
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"test": "CI=1 bun ../../test/run-isolated.ts",
|
|
36
|
+
"test:unit": "CI=1 bun ../../test/run-isolated.ts --suite=unit",
|
|
37
|
+
"test:integration": "CI=1 bun ../../test/run-isolated.ts --suite=integration",
|
|
35
38
|
"typecheck": "bunx tsc --noEmit -p ../../tsconfig.json",
|
|
36
39
|
"lint": "oxlint src/ opencode-plugin/",
|
|
37
40
|
"lint:fix": "oxlint src/ opencode-plugin/ --fix",
|
|
@@ -39,10 +42,14 @@
|
|
|
39
42
|
"format:check": "oxfmt src/ opencode-plugin/ \"!**/*.generated.*\" --check"
|
|
40
43
|
},
|
|
41
44
|
"dependencies": {
|
|
45
|
+
"@fission-ai/openspec": "1.15.0",
|
|
42
46
|
"@opencode/plugin": "^2.0.23",
|
|
43
47
|
"@opentui/core": "^0.5.14",
|
|
44
48
|
"@opentui/keymap": "^0.5.14",
|
|
45
49
|
"@opentui/solid": "^0.5.14",
|
|
50
|
+
"@secretlint/core": "13.0.7",
|
|
51
|
+
"@secretlint/secretlint-rule-preset-recommend": "13.0.7",
|
|
52
|
+
"@secretlint/types": "13.0.7",
|
|
46
53
|
"@tanstack/highlight": "0.0.10",
|
|
47
54
|
"@xterm/addon-clipboard": "^0.2.0",
|
|
48
55
|
"@xterm/addon-fit": "^0.11.0",
|
|
@@ -51,6 +58,7 @@
|
|
|
51
58
|
"@xterm/xterm": "^6.0.0",
|
|
52
59
|
"hono": "^4.13.5",
|
|
53
60
|
"ink": "^7.1.0",
|
|
61
|
+
"marked": "^18.1.1",
|
|
54
62
|
"react": "^19.2.7",
|
|
55
63
|
"react-doctor": "0.9.13",
|
|
56
64
|
"semver": "^7.8.5",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Generated by scripts/brand/generate.ts from assets/logo-kit. Do not edit.
|
|
2
|
+
import type { LogoArt, LogoForm } from "./types";
|
|
3
|
+
|
|
4
|
+
export const LOGO_ART: Record<LogoForm, LogoArt> = {
|
|
5
|
+
lockup: {
|
|
6
|
+
block: [
|
|
7
|
+
[{"kind":"pane","text":"▄███████"},{"kind":"space","text":" "},{"kind":"note","text":"████▄"}],
|
|
8
|
+
[{"kind":"pane","text":"████████"},{"kind":"space","text":" "},{"kind":"note","text":"█████"},{"kind":"space","text":" "},{"kind":"word","text":"▄▄"}],
|
|
9
|
+
[{"kind":"pane","text":"████████"},{"kind":"space","text":" "},{"kind":"note","text":"▄▄▄▄▄"},{"kind":"space","text":" "},{"kind":"word","text":"██"}],
|
|
10
|
+
[{"kind":"pane","text":"████████"},{"kind":"space","text":" "},{"kind":"note","text":"█████"},{"kind":"space","text":" "},{"kind":"word","text":"▄█▀▀██▀▀█▄"},{"kind":"space","text":" "},{"kind":"word","text":"▄█▀▀██"},{"kind":"space","text":" "},{"kind":"word","text":"▀██▀▀"},{"kind":"space","text":" "},{"kind":"word","text":"▄█▀▀█▄"}],
|
|
11
|
+
[{"kind":"pane","text":"████████"},{"kind":"space","text":" "},{"kind":"note","text":"▀▀▀▀▀"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██▄▄██"}],
|
|
12
|
+
[{"kind":"pane","text":"████████"},{"kind":"space","text":" "},{"kind":"note","text":"█████"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"}],
|
|
13
|
+
[{"kind":"pane","text":"▀███████"},{"kind":"space","text":" "},{"kind":"note","text":"████▀"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"▀█▄▄██▄"},{"kind":"space","text":" "},{"kind":"word","text":"▀█▄▄"},{"kind":"space","text":" "},{"kind":"word","text":"▀█▄▄▄▄"}],
|
|
14
|
+
],
|
|
15
|
+
ascii: [
|
|
16
|
+
" ####### ####",
|
|
17
|
+
"######## ##### ##",
|
|
18
|
+
"######## ##",
|
|
19
|
+
"######## ##### ######## ##### ##### #####",
|
|
20
|
+
"######## ##### ## ## ## ## ## ## ## ##",
|
|
21
|
+
"######## ## ## ## ## ## ## #######",
|
|
22
|
+
"######## ##### ## ## ## ## ## ## ##",
|
|
23
|
+
" ####### #### ## ## ## ###### ### ######",
|
|
24
|
+
],
|
|
25
|
+
columns: 52,
|
|
26
|
+
},
|
|
27
|
+
symbol: {
|
|
28
|
+
block: [
|
|
29
|
+
[{"kind":"pane","text":"▄███████"},{"kind":"space","text":" "},{"kind":"note","text":"████▄"}],
|
|
30
|
+
[{"kind":"pane","text":"████████"},{"kind":"space","text":" "},{"kind":"note","text":"█████"}],
|
|
31
|
+
[{"kind":"pane","text":"████████"},{"kind":"space","text":" "},{"kind":"note","text":"▄▄▄▄▄"}],
|
|
32
|
+
[{"kind":"pane","text":"████████"},{"kind":"space","text":" "},{"kind":"note","text":"█████"}],
|
|
33
|
+
[{"kind":"pane","text":"████████"},{"kind":"space","text":" "},{"kind":"note","text":"▀▀▀▀▀"}],
|
|
34
|
+
[{"kind":"pane","text":"████████"},{"kind":"space","text":" "},{"kind":"note","text":"█████"}],
|
|
35
|
+
[{"kind":"pane","text":"▀███████"},{"kind":"space","text":" "},{"kind":"note","text":"████▀"}],
|
|
36
|
+
],
|
|
37
|
+
ascii: [
|
|
38
|
+
" ####### ####",
|
|
39
|
+
"######## #####",
|
|
40
|
+
"########",
|
|
41
|
+
"######## #####",
|
|
42
|
+
"######## #####",
|
|
43
|
+
"########",
|
|
44
|
+
"######## #####",
|
|
45
|
+
" ####### ####",
|
|
46
|
+
],
|
|
47
|
+
columns: 14,
|
|
48
|
+
},
|
|
49
|
+
wordmark: {
|
|
50
|
+
block: [
|
|
51
|
+
[{"kind":"space","text":" "},{"kind":"word","text":"▄▄"}],
|
|
52
|
+
[{"kind":"space","text":" "},{"kind":"word","text":"██"}],
|
|
53
|
+
[{"kind":"word","text":"▄█▀▀██▀▀█▄"},{"kind":"space","text":" "},{"kind":"word","text":"▄█▀▀██"},{"kind":"space","text":" "},{"kind":"word","text":"▀██▀▀"},{"kind":"space","text":" "},{"kind":"word","text":"▄█▀▀█▄"}],
|
|
54
|
+
[{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██▄▄██"}],
|
|
55
|
+
[{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"}],
|
|
56
|
+
[{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"██"},{"kind":"space","text":" "},{"kind":"word","text":"▀█▄▄██▄"},{"kind":"space","text":" "},{"kind":"word","text":"▀█▄▄"},{"kind":"space","text":" "},{"kind":"word","text":"▀█▄▄▄▄"}],
|
|
57
|
+
],
|
|
58
|
+
ascii: [
|
|
59
|
+
" ##",
|
|
60
|
+
" ##",
|
|
61
|
+
" ######## ##### ##### #####",
|
|
62
|
+
"## ## ## ## ## ## ## ##",
|
|
63
|
+
"## ## ## ## ## ## #######",
|
|
64
|
+
"## ## ## ## ## ## ##",
|
|
65
|
+
"## ## ## ###### ### ######",
|
|
66
|
+
],
|
|
67
|
+
columns: 34,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { LOGO_ART } from "./art.generated";
|
|
2
|
+
export { BRAND_ACCENT, BRAND_COLORS, type BrandColor, type BrandColorName } from "./palette";
|
|
3
|
+
export { BRAND_SVGS } from "./svg.generated";
|
|
4
|
+
export {
|
|
5
|
+
LOGO_WORD,
|
|
6
|
+
chooseLogo,
|
|
7
|
+
colorEnabled,
|
|
8
|
+
renderLogo,
|
|
9
|
+
type LogoChoice,
|
|
10
|
+
type LogoOptions,
|
|
11
|
+
} from "./terminal";
|
|
12
|
+
export type { BrandSvgs, LogoArt, LogoForm, LogoRow, LogoSegment, LogoSegmentKind } from "./types";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** A brand colour as a 24-bit hex value plus its nearest xterm 256-colour index. */
|
|
2
|
+
export interface BrandColor {
|
|
3
|
+
hex: string;
|
|
4
|
+
ansi256: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** The five colours of the Mate identity, as defined in `assets/logo-kit/BRAND.md`. */
|
|
8
|
+
export const BRAND_COLORS = {
|
|
9
|
+
ink: { hex: "#1F2421", ansi256: 235 },
|
|
10
|
+
mateGreen: { hex: "#2E7D5B", ansi256: 29 },
|
|
11
|
+
cream: { hex: "#F4F0E8", ansi256: 255 },
|
|
12
|
+
lightGreen: { hex: "#7FD0A6", ansi256: 115 },
|
|
13
|
+
night: { hex: "#15191A", ansi256: 234 },
|
|
14
|
+
} as const satisfies Record<string, BrandColor>;
|
|
15
|
+
|
|
16
|
+
export type BrandColorName = keyof typeof BRAND_COLORS;
|
|
17
|
+
|
|
18
|
+
/** The accent used on surfaces whose background is unknown; it reads on both light and dark. */
|
|
19
|
+
export const BRAND_ACCENT = BRAND_COLORS.mateGreen.hex;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Generated by scripts/brand/generate.ts from assets/logo-kit. Do not edit.
|
|
2
|
+
import type { BrandSvgs } from "./types";
|
|
3
|
+
|
|
4
|
+
export const BRAND_SVGS: BrandSvgs = {
|
|
5
|
+
symbolColor: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" width=\"256\" height=\"256\" role=\"img\" aria-labelledby=\"title\"><title id=\"title\">mate logo</title><g id=\"symbol\"><path fill=\"#1F2421\" d=\"M36 16H144V240H36A20 20 0 0 1 16 220V36A20 20 0 0 1 36 16Z\"/><path fill=\"#2E7D5B\" d=\"M160 16H220A20 20 0 0 1 240 36V80H160Z M160 96H240V160H160Z M160 176H240V220A20 20 0 0 1 220 240H160Z\"/></g></svg>",
|
|
6
|
+
symbolOnDark: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" width=\"256\" height=\"256\" role=\"img\" aria-labelledby=\"title\"><title id=\"title\">mate logo</title><g id=\"symbol\"><path fill=\"#F4F0E8\" d=\"M36 16H144V240H36A20 20 0 0 1 16 220V36A20 20 0 0 1 36 16Z\"/><path fill=\"#7FD0A6\" d=\"M160 16H220A20 20 0 0 1 240 36V80H160Z M160 96H240V160H160Z M160 176H240V220A20 20 0 0 1 220 240H160Z\"/></g></svg>",
|
|
7
|
+
favicon: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" width=\"256\" height=\"256\" role=\"img\" aria-labelledby=\"title\"><title id=\"title\">mate logo</title><rect width=\"256\" height=\"256\" rx=\"48\" fill=\"#1F2421\"/><g id=\"symbol\" transform=\"translate(32 32) scale(0.75)\"><path fill=\"#F4F0E8\" d=\"M36 16H144V240H36A20 20 0 0 1 16 220V36A20 20 0 0 1 36 16Z\"/><path fill=\"#7FD0A6\" d=\"M160 16H220A20 20 0 0 1 240 36V80H160Z M160 96H240V160H160Z M160 176H240V220A20 20 0 0 1 220 240H160Z\"/></g></svg>",
|
|
8
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { LOGO_ART } from "./art.generated";
|
|
2
|
+
import { BRAND_COLORS } from "./palette";
|
|
3
|
+
import type { LogoForm, LogoRow } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface LogoOptions {
|
|
6
|
+
/** The largest form wanted; it falls back to smaller forms when it does not fit. */
|
|
7
|
+
form: LogoForm;
|
|
8
|
+
/** Columns available on the target stream. */
|
|
9
|
+
columns: number;
|
|
10
|
+
isTTY: boolean;
|
|
11
|
+
env?: Record<string, string | undefined>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LogoChoice =
|
|
15
|
+
| { kind: "block"; form: LogoForm; rows: readonly LogoRow[]; color: boolean }
|
|
16
|
+
| { kind: "ascii"; form: LogoForm; rows: readonly string[] }
|
|
17
|
+
| { kind: "word" };
|
|
18
|
+
|
|
19
|
+
/** The word that stands in for the logo when no art fits. */
|
|
20
|
+
export const LOGO_WORD = "mate";
|
|
21
|
+
|
|
22
|
+
const FALLBACKS: Record<LogoForm, readonly LogoForm[]> = {
|
|
23
|
+
lockup: ["lockup", "symbol"],
|
|
24
|
+
symbol: ["symbol"],
|
|
25
|
+
wordmark: ["wordmark"],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function asciiWidth(rows: readonly string[]): number {
|
|
29
|
+
return Math.max(...rows.map((row) => row.length));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Whether colour is allowed, following https://no-color.org: any non-empty NO_COLOR disables it. */
|
|
33
|
+
export function colorEnabled(
|
|
34
|
+
isTTY: boolean,
|
|
35
|
+
env: Record<string, string | undefined> = {},
|
|
36
|
+
): boolean {
|
|
37
|
+
return isTTY && !env.NO_COLOR;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Picks the logo encoding for a stream: block art on a TTY (coloured unless NO_COLOR is set),
|
|
42
|
+
* ASCII otherwise, falling back from the lockup to the symbol to the word `mate` as width runs
|
|
43
|
+
* out.
|
|
44
|
+
*/
|
|
45
|
+
export function chooseLogo({ form, columns, isTTY, env = {} }: LogoOptions): LogoChoice {
|
|
46
|
+
for (const candidate of FALLBACKS[form]) {
|
|
47
|
+
const art = LOGO_ART[candidate];
|
|
48
|
+
if (!isTTY) {
|
|
49
|
+
if (asciiWidth(art.ascii) <= columns)
|
|
50
|
+
return { kind: "ascii", form: candidate, rows: art.ascii };
|
|
51
|
+
} else if (art.columns <= columns) {
|
|
52
|
+
return { kind: "block", form: candidate, rows: art.block, color: colorEnabled(isTTY, env) };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { kind: "word" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hexToRgb(hex: string): string {
|
|
59
|
+
const value = Number.parseInt(hex.slice(1), 16);
|
|
60
|
+
return `${(value >> 16) & 255};${(value >> 8) & 255};${value & 255}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const NOTE_ON = `\x1b[38;2;${hexToRgb(BRAND_COLORS.mateGreen.hex)}m`;
|
|
64
|
+
const NOTE_OFF = "\x1b[39m";
|
|
65
|
+
|
|
66
|
+
/** Renders the logo as a string ready to write to a stream, rows separated by newlines. */
|
|
67
|
+
export function renderLogo(options: LogoOptions): string {
|
|
68
|
+
const choice = chooseLogo(options);
|
|
69
|
+
if (choice.kind === "word") return LOGO_WORD;
|
|
70
|
+
if (choice.kind === "ascii") return choice.rows.join("\n");
|
|
71
|
+
|
|
72
|
+
return choice.rows
|
|
73
|
+
.map((row) =>
|
|
74
|
+
row
|
|
75
|
+
.map((segment) =>
|
|
76
|
+
choice.color && segment.kind === "note"
|
|
77
|
+
? `${NOTE_ON}${segment.text}${NOTE_OFF}`
|
|
78
|
+
: segment.text,
|
|
79
|
+
)
|
|
80
|
+
.join(""),
|
|
81
|
+
)
|
|
82
|
+
.join("\n");
|
|
83
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run of logo cells belongs to. Notes are the three bars of the right pane and the
|
|
3
|
+
* only cells coloured on an unknown background; panes and the wordmark keep the default
|
|
4
|
+
* foreground.
|
|
5
|
+
*/
|
|
6
|
+
export type LogoSegmentKind = "pane" | "note" | "word";
|
|
7
|
+
|
|
8
|
+
/** A run of adjacent cells that share a kind. Spaces between runs are their own segments. */
|
|
9
|
+
export interface LogoSegment {
|
|
10
|
+
kind: LogoSegmentKind | "space";
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LogoRow = readonly LogoSegment[];
|
|
15
|
+
|
|
16
|
+
/** One logo form in every terminal encoding the kit provides. */
|
|
17
|
+
export interface LogoArt {
|
|
18
|
+
/** Unicode half-block art, one entry per row. */
|
|
19
|
+
readonly block: readonly LogoRow[];
|
|
20
|
+
/** 7-bit ASCII art for streams that are not a TTY. */
|
|
21
|
+
readonly ascii: readonly string[];
|
|
22
|
+
/** Width in columns of the block art. */
|
|
23
|
+
readonly columns: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type LogoForm = "lockup" | "symbol" | "wordmark";
|
|
27
|
+
|
|
28
|
+
/** The SVG markup a web surface embeds, generated verbatim from the kit. */
|
|
29
|
+
export interface BrandSvgs {
|
|
30
|
+
readonly symbolColor: string;
|
|
31
|
+
readonly symbolOnDark: string;
|
|
32
|
+
readonly favicon: string;
|
|
33
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { usage } from "../../usage";
|
|
2
|
+
import { runArtifactArchiveCommand } from "./delivery/archive";
|
|
3
|
+
import { runArtifactLinkCommand } from "./delivery/link";
|
|
2
4
|
import { runArtifactPublishCommand } from "./finish";
|
|
3
5
|
import { runArtifactPendingCommand } from "./pending";
|
|
4
6
|
|
|
@@ -30,6 +32,12 @@ export async function runArtifactCommand(
|
|
|
30
32
|
case "pending":
|
|
31
33
|
await runArtifactPendingCommand(argv);
|
|
32
34
|
return;
|
|
35
|
+
case "link":
|
|
36
|
+
await runArtifactLinkCommand(argv);
|
|
37
|
+
return;
|
|
38
|
+
case "archive":
|
|
39
|
+
await runArtifactArchiveCommand(argv);
|
|
40
|
+
return;
|
|
33
41
|
default:
|
|
34
42
|
console.error(`Unknown artifact command: ${subcommand ?? ""}`);
|
|
35
43
|
console.error(usage());
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
archiveChange,
|
|
3
|
+
type ArchiveChangeDeps,
|
|
4
|
+
type ArchiveResult,
|
|
5
|
+
} from "../../../../lib/delivery/archive-change";
|
|
6
|
+
import type { DeliveryRefusal } from "../../../../lib/delivery/record-delivery";
|
|
7
|
+
import { type BooleanFlagSet, parseFlags, positionalArgs } from "../../../parse-flags";
|
|
8
|
+
import {
|
|
9
|
+
branchFlagsFrom,
|
|
10
|
+
canonicalRepository,
|
|
11
|
+
deliveryEditFrom,
|
|
12
|
+
DIRECT_NEEDS_BRANCH,
|
|
13
|
+
repositoryFlag,
|
|
14
|
+
valueFlagError,
|
|
15
|
+
} from "./branch-flags";
|
|
16
|
+
import { resolveDeliveryEnvironment, type DeliveryEnvironmentDeps } from "./environment";
|
|
17
|
+
import {
|
|
18
|
+
commandOutput,
|
|
19
|
+
renderDelivery,
|
|
20
|
+
sharedRefusalMessage,
|
|
21
|
+
unscopedDeliveryWarning,
|
|
22
|
+
} from "./output";
|
|
23
|
+
|
|
24
|
+
export interface ArchiveCommandDeps
|
|
25
|
+
extends DeliveryEnvironmentDeps, Omit<ArchiveChangeDeps, "onWarnings"> {
|
|
26
|
+
stdout?: (line: string) => void;
|
|
27
|
+
stderr?: (line: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const BOOLEAN_FLAGS: BooleanFlagSet = new Set(["json", "direct", "skip-branch"]);
|
|
31
|
+
|
|
32
|
+
function refusalMessage(refusal: DeliveryRefusal): string {
|
|
33
|
+
if (refusal.reason === "unknown-change") {
|
|
34
|
+
return `mate: no active change named "${refusal.change}" to archive.`;
|
|
35
|
+
}
|
|
36
|
+
return sharedRefusalMessage(refusal);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Warnings are not repeated here: they go to stderr before anything is archived. */
|
|
40
|
+
function renderHuman(result: ArchiveResult): string[] {
|
|
41
|
+
return [
|
|
42
|
+
result.message,
|
|
43
|
+
...result.record.deliveries.map(renderDelivery),
|
|
44
|
+
` delivery: ${result.settlement.status}`,
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @command mate artifact archive <change>
|
|
50
|
+
* @description Archives an active change whether or not its delivery is merged, so a
|
|
51
|
+
* feature's later changes build on its canonical specs. Writes overlap, dependency, and
|
|
52
|
+
* incomplete-planning warnings to stderr before archiving, refreshes merge state from the forge when a token
|
|
53
|
+
* is available, always leaves a `delivery.yaml` (unlinked when none was recorded), then
|
|
54
|
+
* runs `openspec archive` with the bundled CLI. Merge state is reported, never a refusal.
|
|
55
|
+
* @flags
|
|
56
|
+
* - `--repository <org/repo>` — the delivery `--branch` or `--not-applicable` records; defaults
|
|
57
|
+
* to the Working Repository.
|
|
58
|
+
* - `--branch <branch>` / `--direct` — record the delivery branch first, as `artifact link`.
|
|
59
|
+
* - `--not-applicable <reason>` — record that no branch delivers this change.
|
|
60
|
+
* - `--skip-branch` — archive with an unlinked record when the user skips the question.
|
|
61
|
+
* - `--json` — emit an {@link ArchiveResult}.
|
|
62
|
+
* @remarks Without a recorded delivery and none of the branch flags, nothing is archived:
|
|
63
|
+
* the result is `needs-branch`, a question rather than a refusal (exit 0), with
|
|
64
|
+
* `change/<change-name>` suggested when that branch exists, so the caller can ask the user.
|
|
65
|
+
*/
|
|
66
|
+
export async function runArtifactArchiveCommand(
|
|
67
|
+
argv: string[],
|
|
68
|
+
deps: ArchiveCommandDeps = {},
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
const { emitOut, emitErr, refuse } = commandOutput(deps);
|
|
71
|
+
const flags = parseFlags(argv, BOOLEAN_FLAGS);
|
|
72
|
+
const flagError = valueFlagError(flags, ["repository", "branch", "not-applicable"]);
|
|
73
|
+
if (flagError) return refuse(flagError);
|
|
74
|
+
const json = flags.json === true;
|
|
75
|
+
const [change] = positionalArgs(argv, BOOLEAN_FLAGS);
|
|
76
|
+
const branchFlags = branchFlagsFrom(flags);
|
|
77
|
+
const { branch, notApplicable } = branchFlags;
|
|
78
|
+
const skip = flags["skip-branch"] === true;
|
|
79
|
+
|
|
80
|
+
if (!change) return refuse("mate: artifact archive requires a change name.");
|
|
81
|
+
if ([branch !== undefined, notApplicable !== undefined, skip].filter(Boolean).length > 1) {
|
|
82
|
+
return refuse("mate: pass only one of --branch, --not-applicable, or --skip-branch.");
|
|
83
|
+
}
|
|
84
|
+
if (branchFlags.direct && branch === undefined) return refuse(DIRECT_NEEDS_BRANCH);
|
|
85
|
+
const requested = repositoryFlag(flags);
|
|
86
|
+
if ("error" in requested) return refuse(requested.error);
|
|
87
|
+
if (requested.repository !== undefined && branch === undefined && notApplicable === undefined) {
|
|
88
|
+
return refuse("mate: --repository names the delivery --branch or --not-applicable records.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const environment = await resolveDeliveryEnvironment(deps);
|
|
92
|
+
if ("error" in environment) return refuse(environment.error);
|
|
93
|
+
const repository = canonicalRepository(environment, requested.repository);
|
|
94
|
+
const delivery = deliveryEditFrom(branchFlags);
|
|
95
|
+
const outcome = await archiveChange(
|
|
96
|
+
environment,
|
|
97
|
+
{
|
|
98
|
+
change,
|
|
99
|
+
...(repository ? { repository } : {}),
|
|
100
|
+
...(delivery ? { delivery } : {}),
|
|
101
|
+
skipBranch: skip,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
...deps,
|
|
105
|
+
onWarnings: (warnings, unscoped) => {
|
|
106
|
+
for (const warning of warnings) emitErr(`warning: ${warning.message}`);
|
|
107
|
+
for (const entry of unscoped) emitErr(unscopedDeliveryWarning(change, entry));
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
if ("refusal" in outcome) return refuse(refusalMessage(outcome.refusal));
|
|
112
|
+
|
|
113
|
+
const { result } = outcome;
|
|
114
|
+
if (result.status === "failed") process.exitCode = 1;
|
|
115
|
+
if (json) emitOut(JSON.stringify(result));
|
|
116
|
+
else for (const line of renderHuman(result)) emitOut(line);
|
|
117
|
+
}
|