peaks-cli 1.0.11 → 1.0.13
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/bin/peaks.js +0 -0
- package/dist/src/cli/commands/core-artifact-commands.js +23 -0
- package/dist/src/cli/commands/mcp-commands.d.ts +3 -0
- package/dist/src/cli/commands/mcp-commands.js +144 -0
- package/dist/src/cli/commands/openspec-commands.d.ts +3 -0
- package/dist/src/cli/commands/openspec-commands.js +169 -0
- package/dist/src/cli/commands/project-commands.d.ts +3 -0
- package/dist/src/cli/commands/project-commands.js +37 -0
- package/dist/src/cli/commands/request-commands.d.ts +3 -0
- package/dist/src/cli/commands/request-commands.js +140 -0
- package/dist/src/cli/commands/understand-commands.d.ts +3 -0
- package/dist/src/cli/commands/understand-commands.js +78 -0
- package/dist/src/cli/program.js +10 -0
- package/dist/src/services/artifacts/request-artifact-service.d.ts +58 -0
- package/dist/src/services/artifacts/request-artifact-service.js +432 -0
- package/dist/src/services/dashboard/project-dashboard-service.d.ts +64 -0
- package/dist/src/services/dashboard/project-dashboard-service.js +112 -0
- package/dist/src/services/doctor/doctor-service.d.ts +7 -0
- package/dist/src/services/doctor/doctor-service.js +139 -0
- package/dist/src/services/mcp/mcp-apply-service.d.ts +31 -0
- package/dist/src/services/mcp/mcp-apply-service.js +112 -0
- package/dist/src/services/mcp/mcp-call-service.d.ts +17 -0
- package/dist/src/services/mcp/mcp-call-service.js +34 -0
- package/dist/src/services/mcp/mcp-client-service.d.ts +14 -0
- package/dist/src/services/mcp/mcp-client-service.js +49 -0
- package/dist/src/services/mcp/mcp-install-registry.d.ts +11 -0
- package/dist/src/services/mcp/mcp-install-registry.js +38 -0
- package/dist/src/services/mcp/mcp-plan-service.d.ts +29 -0
- package/dist/src/services/mcp/mcp-plan-service.js +109 -0
- package/dist/src/services/mcp/mcp-protocol.d.ts +24 -0
- package/dist/src/services/mcp/mcp-protocol.js +41 -0
- package/dist/src/services/mcp/mcp-scan-service.d.ts +8 -0
- package/dist/src/services/mcp/mcp-scan-service.js +214 -0
- package/dist/src/services/mcp/mcp-stdio-transport.d.ts +10 -0
- package/dist/src/services/mcp/mcp-stdio-transport.js +50 -0
- package/dist/src/services/mcp/mcp-types.d.ts +31 -0
- package/dist/src/services/mcp/mcp-types.js +1 -0
- package/dist/src/services/openspec/openspec-archive-service.d.ts +12 -0
- package/dist/src/services/openspec/openspec-archive-service.js +28 -0
- package/dist/src/services/openspec/openspec-bridge-service.d.ts +16 -0
- package/dist/src/services/openspec/openspec-bridge-service.js +76 -0
- package/dist/src/services/openspec/openspec-render-service.d.ts +38 -0
- package/dist/src/services/openspec/openspec-render-service.js +130 -0
- package/dist/src/services/openspec/openspec-scan-service.d.ts +6 -0
- package/dist/src/services/openspec/openspec-scan-service.js +123 -0
- package/dist/src/services/openspec/openspec-types.d.ts +39 -0
- package/dist/src/services/openspec/openspec-types.js +1 -0
- package/dist/src/services/openspec/openspec-validate-service.d.ts +27 -0
- package/dist/src/services/openspec/openspec-validate-service.js +77 -0
- package/dist/src/services/recommendations/capability-seed-items.js +1 -0
- package/dist/src/services/skills/skill-runbook-service.d.ts +11 -0
- package/dist/src/services/skills/skill-runbook-service.js +60 -0
- package/dist/src/services/standards/project-standards-service.js +4 -9
- package/dist/src/services/understand/understand-scan-service.d.ts +28 -0
- package/dist/src/services/understand/understand-scan-service.js +157 -0
- package/dist/src/services/understand/understand-types.d.ts +24 -0
- package/dist/src/services/understand/understand-types.js +1 -0
- package/dist/src/shared/json-schema-mini.d.ts +10 -0
- package/dist/src/shared/json-schema-mini.js +113 -0
- package/dist/src/shared/paths.d.ts +1 -1
- package/dist/src/shared/paths.js +9 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -6
- package/schemas/doctor-report.schema.json +34 -0
- package/schemas/mcp-apply-result.schema.json +46 -0
- package/schemas/mcp-install-plan.schema.json +71 -0
- package/schemas/mcp-install-spec.schema.json +29 -0
- package/schemas/mcp-server.schema.json +29 -0
- package/schemas/openspec-change-summary.schema.json +68 -0
- package/schemas/openspec-render-request.schema.json +61 -0
- package/schemas/openspec-validation-result.schema.json +36 -0
- package/skills/peaks-prd/SKILL.md +59 -8
- package/skills/peaks-prd/references/artifact-per-request.md +78 -0
- package/skills/peaks-prd/references/workflow.md +7 -5
- package/skills/peaks-qa/SKILL.md +73 -7
- package/skills/peaks-qa/references/artifact-contracts.md +1 -1
- package/skills/peaks-qa/references/artifact-per-request.md +83 -0
- package/skills/peaks-qa/references/openspec-validation-gate.md +55 -0
- package/skills/peaks-qa/references/regression-gates.md +1 -1
- package/skills/peaks-rd/SKILL.md +94 -7
- package/skills/peaks-rd/references/artifact-per-request.md +90 -0
- package/skills/peaks-rd/references/openspec-mcp-cli.md +65 -0
- package/skills/peaks-sc/SKILL.md +44 -0
- package/skills/peaks-sc/references/openspec-commit-boundaries.md +33 -0
- package/skills/peaks-solo/SKILL.md +87 -4
- package/skills/peaks-solo/references/browser-workflow.md +114 -0
- package/skills/peaks-solo/references/external-skill-invocation.md +70 -0
- package/skills/peaks-solo/references/openspec-mcp-workflow.md +53 -0
- package/skills/peaks-solo/references/workflow.md +1 -1
- package/skills/peaks-txt/SKILL.md +42 -0
- package/skills/peaks-ui/SKILL.md +57 -33
- package/skills/peaks-ui/references/artifact-per-request.md +71 -0
- package/skills/peaks-ui/references/workflow.md +8 -11
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function buildRequest(id, method, params) {
|
|
2
|
+
if (params === undefined) {
|
|
3
|
+
return { jsonrpc: '2.0', id, method };
|
|
4
|
+
}
|
|
5
|
+
return { jsonrpc: '2.0', id, method, params };
|
|
6
|
+
}
|
|
7
|
+
export function serializeMessage(message) {
|
|
8
|
+
return `${JSON.stringify(message)}\n`;
|
|
9
|
+
}
|
|
10
|
+
function isJsonRpcResponse(value) {
|
|
11
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const record = value;
|
|
15
|
+
if (record.jsonrpc !== '2.0') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return typeof record.id === 'number';
|
|
19
|
+
}
|
|
20
|
+
export function parseMessages(buffer) {
|
|
21
|
+
const lastNewline = buffer.lastIndexOf('\n');
|
|
22
|
+
const remainder = lastNewline === -1 ? buffer : buffer.slice(lastNewline + 1);
|
|
23
|
+
const completePart = lastNewline === -1 ? '' : buffer.slice(0, lastNewline);
|
|
24
|
+
const messages = [];
|
|
25
|
+
for (const line of completePart.split('\n')) {
|
|
26
|
+
if (line.length === 0) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(line);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (isJsonRpcResponse(parsed)) {
|
|
37
|
+
messages.push(parsed);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { messages, remainder };
|
|
41
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { McpScanReport } from './mcp-types.js';
|
|
2
|
+
export type McpScanOptions = {
|
|
3
|
+
globalSettingsPath?: string;
|
|
4
|
+
projectRoot?: string;
|
|
5
|
+
managedMarkerPath?: string;
|
|
6
|
+
pluginsRegistryPath?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function scanMcpServers(options?: McpScanOptions): Promise<McpScanReport>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pathExists, readText } from '../../shared/fs.js';
|
|
4
|
+
import { getErrorMessage } from '../../shared/result.js';
|
|
5
|
+
function defaultGlobalSettingsPath() {
|
|
6
|
+
return join(homedir(), '.claude', 'settings.json');
|
|
7
|
+
}
|
|
8
|
+
function defaultManagedMarkerPath() {
|
|
9
|
+
return join(homedir(), '.peaks', 'mcp-managed.json');
|
|
10
|
+
}
|
|
11
|
+
function defaultPluginsRegistryPath() {
|
|
12
|
+
return join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
13
|
+
}
|
|
14
|
+
function projectSettingsPath(projectRoot) {
|
|
15
|
+
return join(projectRoot, '.claude', 'settings.json');
|
|
16
|
+
}
|
|
17
|
+
async function readSettings(path) {
|
|
18
|
+
if (!(await pathExists(path))) {
|
|
19
|
+
return { exists: false, parsed: { parsed: null } };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const raw = await readText(path);
|
|
23
|
+
const value = JSON.parse(raw);
|
|
24
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
25
|
+
return { exists: true, parsed: { parsed: null } };
|
|
26
|
+
}
|
|
27
|
+
return { exists: true, parsed: { parsed: value } };
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return { exists: true, parsed: { parsed: null, parseError: getErrorMessage(error) } };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function loadManagedNames(path) {
|
|
34
|
+
if (!(await pathExists(path))) {
|
|
35
|
+
return new Set();
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(await readText(path));
|
|
39
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
40
|
+
return new Set();
|
|
41
|
+
}
|
|
42
|
+
const servers = value.servers;
|
|
43
|
+
if (!Array.isArray(servers)) {
|
|
44
|
+
return new Set();
|
|
45
|
+
}
|
|
46
|
+
return new Set(servers.filter((entry) => typeof entry === 'string'));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return new Set();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function toStringArray(value) {
|
|
53
|
+
if (!Array.isArray(value)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return value.filter((entry) => typeof entry === 'string');
|
|
57
|
+
}
|
|
58
|
+
function toEnvKeys(value) {
|
|
59
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return Object.keys(value);
|
|
63
|
+
}
|
|
64
|
+
function buildServerConfig(name, raw, scope, source, pluginName) {
|
|
65
|
+
const command = raw.command;
|
|
66
|
+
if (typeof command !== 'string' || command.length === 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const config = {
|
|
70
|
+
name,
|
|
71
|
+
command,
|
|
72
|
+
args: toStringArray(raw.args),
|
|
73
|
+
envKeys: toEnvKeys(raw.env),
|
|
74
|
+
scope,
|
|
75
|
+
source
|
|
76
|
+
};
|
|
77
|
+
if (pluginName !== undefined) {
|
|
78
|
+
config.pluginName = pluginName;
|
|
79
|
+
}
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
82
|
+
function resolveServers(settings, scope, managedNames) {
|
|
83
|
+
if (settings === null) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const mcpServers = settings.mcpServers;
|
|
87
|
+
if (mcpServers === null || typeof mcpServers !== 'object' || Array.isArray(mcpServers)) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const servers = [];
|
|
91
|
+
for (const [name, raw] of Object.entries(mcpServers)) {
|
|
92
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const source = managedNames.has(name) ? 'peaks' : 'unknown';
|
|
96
|
+
const config = buildServerConfig(name, raw, scope, source);
|
|
97
|
+
if (config !== null) {
|
|
98
|
+
servers.push(config);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return servers;
|
|
102
|
+
}
|
|
103
|
+
function toScopeReport(path, exists, parseError) {
|
|
104
|
+
return parseError === undefined ? { path, exists } : { path, exists, parseError };
|
|
105
|
+
}
|
|
106
|
+
function extractMcpServersBlock(parsed) {
|
|
107
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const wrapped = parsed.mcpServers;
|
|
111
|
+
if (wrapped !== undefined && wrapped !== null && typeof wrapped === 'object' && !Array.isArray(wrapped)) {
|
|
112
|
+
return wrapped;
|
|
113
|
+
}
|
|
114
|
+
return parsed;
|
|
115
|
+
}
|
|
116
|
+
async function readPluginMcpServers(installPath, pluginName) {
|
|
117
|
+
const mcpFilePath = join(installPath, '.mcp.json');
|
|
118
|
+
if (!(await pathExists(mcpFilePath))) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
let parsed;
|
|
122
|
+
try {
|
|
123
|
+
parsed = JSON.parse(await readText(mcpFilePath));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const block = extractMcpServersBlock(parsed);
|
|
129
|
+
if (block === null) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const servers = [];
|
|
133
|
+
for (const [name, raw] of Object.entries(block)) {
|
|
134
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const config = buildServerConfig(name, raw, 'plugin', 'plugin', pluginName);
|
|
138
|
+
if (config !== null) {
|
|
139
|
+
servers.push(config);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return servers;
|
|
143
|
+
}
|
|
144
|
+
async function scanPluginMcpServers(registryPath) {
|
|
145
|
+
const exists = await pathExists(registryPath);
|
|
146
|
+
const baseReport = {
|
|
147
|
+
path: registryPath,
|
|
148
|
+
exists,
|
|
149
|
+
pluginsScanned: 0,
|
|
150
|
+
pluginsWithMcp: 0
|
|
151
|
+
};
|
|
152
|
+
if (!exists) {
|
|
153
|
+
return { servers: [], report: baseReport };
|
|
154
|
+
}
|
|
155
|
+
let parsed;
|
|
156
|
+
try {
|
|
157
|
+
parsed = JSON.parse(await readText(registryPath));
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return { servers: [], report: { ...baseReport, parseError: getErrorMessage(error) } };
|
|
161
|
+
}
|
|
162
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
163
|
+
return { servers: [], report: baseReport };
|
|
164
|
+
}
|
|
165
|
+
const pluginsField = parsed.plugins;
|
|
166
|
+
if (pluginsField === null || typeof pluginsField !== 'object' || Array.isArray(pluginsField)) {
|
|
167
|
+
return { servers: [], report: baseReport };
|
|
168
|
+
}
|
|
169
|
+
const servers = [];
|
|
170
|
+
let pluginsScanned = 0;
|
|
171
|
+
let pluginsWithMcp = 0;
|
|
172
|
+
for (const [pluginId, entries] of Object.entries(pluginsField)) {
|
|
173
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const first = entries[0];
|
|
177
|
+
if (first === null || typeof first !== 'object' || Array.isArray(first)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const installPath = first.installPath;
|
|
181
|
+
if (typeof installPath !== 'string' || installPath.length === 0) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
pluginsScanned += 1;
|
|
185
|
+
const found = await readPluginMcpServers(installPath, pluginId);
|
|
186
|
+
if (found.length > 0) {
|
|
187
|
+
pluginsWithMcp += 1;
|
|
188
|
+
servers.push(...found);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { servers, report: { ...baseReport, pluginsScanned, pluginsWithMcp } };
|
|
192
|
+
}
|
|
193
|
+
export async function scanMcpServers(options = {}) {
|
|
194
|
+
const globalPath = options.globalSettingsPath ?? defaultGlobalSettingsPath();
|
|
195
|
+
const managedMarkerPath = options.managedMarkerPath ?? defaultManagedMarkerPath();
|
|
196
|
+
const pluginsRegistryPath = options.pluginsRegistryPath ?? defaultPluginsRegistryPath();
|
|
197
|
+
const managedNames = await loadManagedNames(managedMarkerPath);
|
|
198
|
+
const globalSettings = await readSettings(globalPath);
|
|
199
|
+
const globalScope = toScopeReport(globalPath, globalSettings.exists, globalSettings.parsed.parseError);
|
|
200
|
+
const globalServers = resolveServers(globalSettings.parsed.parsed, 'global', managedNames);
|
|
201
|
+
let projectScope = null;
|
|
202
|
+
const projectServers = [];
|
|
203
|
+
if (options.projectRoot !== undefined) {
|
|
204
|
+
const projectPath = projectSettingsPath(options.projectRoot);
|
|
205
|
+
const projectSettings = await readSettings(projectPath);
|
|
206
|
+
projectScope = toScopeReport(projectPath, projectSettings.exists, projectSettings.parsed.parseError);
|
|
207
|
+
projectServers.push(...resolveServers(projectSettings.parsed.parsed, 'project', managedNames));
|
|
208
|
+
}
|
|
209
|
+
const plugins = await scanPluginMcpServers(pluginsRegistryPath);
|
|
210
|
+
return {
|
|
211
|
+
servers: [...globalServers, ...projectServers, ...plugins.servers],
|
|
212
|
+
scopes: { global: globalScope, project: projectScope, plugins: plugins.report }
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { McpClientTransport } from './mcp-client-service.js';
|
|
2
|
+
import type { McpInstallSpec } from './mcp-install-registry.js';
|
|
3
|
+
export type StdioTransportOptions = {
|
|
4
|
+
command: string;
|
|
5
|
+
args?: string[];
|
|
6
|
+
env?: Record<string, string | undefined>;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function createStdioTransport(options: StdioTransportOptions): McpClientTransport;
|
|
10
|
+
export declare function createStdioTransportFromSpec(spec: McpInstallSpec, env: Record<string, string | undefined>): McpClientTransport;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export function createStdioTransport(options) {
|
|
3
|
+
const env = {};
|
|
4
|
+
for (const [key, value] of Object.entries({ ...process.env, ...(options.env ?? {}) })) {
|
|
5
|
+
if (typeof value === 'string') {
|
|
6
|
+
env[key] = value;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const child = spawn(options.command, options.args ?? [], {
|
|
10
|
+
env,
|
|
11
|
+
cwd: options.cwd,
|
|
12
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
13
|
+
});
|
|
14
|
+
let lineHandler = null;
|
|
15
|
+
child.stdout.setEncoding('utf8');
|
|
16
|
+
child.stdout.on('data', (chunk) => {
|
|
17
|
+
if (lineHandler !== null) {
|
|
18
|
+
lineHandler(chunk);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
send: (line) => new Promise((resolve, reject) => {
|
|
23
|
+
child.stdin.write(line, (error) => {
|
|
24
|
+
if (error !== null && error !== undefined) {
|
|
25
|
+
reject(error);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
resolve();
|
|
29
|
+
});
|
|
30
|
+
}),
|
|
31
|
+
onLine: (handler) => {
|
|
32
|
+
lineHandler = handler;
|
|
33
|
+
},
|
|
34
|
+
close: () => new Promise((resolve) => {
|
|
35
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
36
|
+
resolve();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
child.once('exit', () => resolve());
|
|
40
|
+
child.kill();
|
|
41
|
+
})
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function createStdioTransportFromSpec(spec, env) {
|
|
45
|
+
const transportEnv = {};
|
|
46
|
+
for (const key of spec.envKeys) {
|
|
47
|
+
transportEnv[key] = env[key];
|
|
48
|
+
}
|
|
49
|
+
return createStdioTransport({ command: spec.command, args: spec.args, env: transportEnv });
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type McpServerSource = 'peaks' | 'cc-switch' | 'user' | 'plugin' | 'unknown';
|
|
2
|
+
export type McpServerScope = 'global' | 'project' | 'plugin';
|
|
3
|
+
export type McpServerConfig = {
|
|
4
|
+
name: string;
|
|
5
|
+
command: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
envKeys: string[];
|
|
8
|
+
source: McpServerSource;
|
|
9
|
+
scope: McpServerScope;
|
|
10
|
+
pluginName?: string;
|
|
11
|
+
};
|
|
12
|
+
export type McpSettingsScopeReport = {
|
|
13
|
+
path: string;
|
|
14
|
+
exists: boolean;
|
|
15
|
+
parseError?: string;
|
|
16
|
+
};
|
|
17
|
+
export type McpPluginsReport = {
|
|
18
|
+
path: string;
|
|
19
|
+
exists: boolean;
|
|
20
|
+
parseError?: string;
|
|
21
|
+
pluginsScanned: number;
|
|
22
|
+
pluginsWithMcp: number;
|
|
23
|
+
};
|
|
24
|
+
export type McpScanReport = {
|
|
25
|
+
servers: McpServerConfig[];
|
|
26
|
+
scopes: {
|
|
27
|
+
global: McpSettingsScopeReport;
|
|
28
|
+
project: McpSettingsScopeReport | null;
|
|
29
|
+
plugins: McpPluginsReport;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { OpenSpecScanOptions } from './openspec-scan-service.js';
|
|
2
|
+
export type OpenSpecArchiveOptions = OpenSpecScanOptions & {
|
|
3
|
+
apply?: boolean;
|
|
4
|
+
archiveDirName?: string;
|
|
5
|
+
};
|
|
6
|
+
export type OpenSpecArchiveResult = {
|
|
7
|
+
changeId: string;
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
applied: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function archiveOpenSpecChange(changeId: string, options?: OpenSpecArchiveOptions): Promise<OpenSpecArchiveResult | null>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mkdir, rename } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { isDirectory } from '../../shared/fs.js';
|
|
4
|
+
const CHANGE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
5
|
+
function defaultOpenSpecRoot() {
|
|
6
|
+
return join(process.cwd(), 'openspec');
|
|
7
|
+
}
|
|
8
|
+
export async function archiveOpenSpecChange(changeId, options = {}) {
|
|
9
|
+
if (!CHANGE_ID_PATTERN.test(changeId)) {
|
|
10
|
+
throw new Error(`Invalid changeId: ${changeId} (expected letters, digits, dots, underscores, or dashes)`);
|
|
11
|
+
}
|
|
12
|
+
const openspecRoot = options.openspecRoot ?? defaultOpenSpecRoot();
|
|
13
|
+
const archiveDir = options.archiveDirName ?? 'archive';
|
|
14
|
+
const from = join(openspecRoot, 'changes', changeId);
|
|
15
|
+
const to = join(openspecRoot, 'changes', archiveDir, changeId);
|
|
16
|
+
if (!(await isDirectory(from))) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (options.apply !== true) {
|
|
20
|
+
return { changeId, from, to, applied: false };
|
|
21
|
+
}
|
|
22
|
+
if (await isDirectory(to)) {
|
|
23
|
+
throw new Error(`Refusing to archive: target already exists at ${to}`);
|
|
24
|
+
}
|
|
25
|
+
await mkdir(dirname(to), { recursive: true });
|
|
26
|
+
await rename(from, to);
|
|
27
|
+
return { changeId, from, to, applied: true };
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type OpenSpecScanOptions } from './openspec-scan-service.js';
|
|
2
|
+
export type OpenSpecCommitBoundary = {
|
|
3
|
+
heading: string;
|
|
4
|
+
todos: string[];
|
|
5
|
+
doneItems: string[];
|
|
6
|
+
};
|
|
7
|
+
export type OpenSpecRdInputProjection = {
|
|
8
|
+
changeId: string;
|
|
9
|
+
acceptance: string[];
|
|
10
|
+
whatChanges: string[];
|
|
11
|
+
dependencies: string[];
|
|
12
|
+
risks: string[];
|
|
13
|
+
outOfScope: string[];
|
|
14
|
+
commitBoundaries: OpenSpecCommitBoundary[];
|
|
15
|
+
};
|
|
16
|
+
export declare function projectOpenSpecToRdInput(changeId: string, options?: OpenSpecScanOptions): Promise<OpenSpecRdInputProjection | null>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readText } from '../../shared/fs.js';
|
|
2
|
+
import { loadOpenSpecChange } from './openspec-scan-service.js';
|
|
3
|
+
function splitTaskSections(markdown) {
|
|
4
|
+
const sections = [];
|
|
5
|
+
let current = null;
|
|
6
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
7
|
+
if (line.startsWith('## ')) {
|
|
8
|
+
if (current !== null) {
|
|
9
|
+
sections.push(current);
|
|
10
|
+
}
|
|
11
|
+
current = { heading: line.slice(3).trim(), body: [] };
|
|
12
|
+
}
|
|
13
|
+
else if (current !== null) {
|
|
14
|
+
current.body.push(line);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (current !== null) {
|
|
18
|
+
sections.push(current);
|
|
19
|
+
}
|
|
20
|
+
return sections;
|
|
21
|
+
}
|
|
22
|
+
const TODO_PATTERN = /^- \[ \] (.+?)\s*$/;
|
|
23
|
+
const DONE_PATTERN = /^- \[[xX]\] (.+?)\s*$/;
|
|
24
|
+
function extractItems(body) {
|
|
25
|
+
const todos = [];
|
|
26
|
+
const doneItems = [];
|
|
27
|
+
for (const rawLine of body) {
|
|
28
|
+
const line = rawLine.trim();
|
|
29
|
+
const todoMatch = TODO_PATTERN.exec(line);
|
|
30
|
+
if (todoMatch !== null) {
|
|
31
|
+
todos.push(todoMatch[1]);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const doneMatch = DONE_PATTERN.exec(line);
|
|
35
|
+
if (doneMatch !== null) {
|
|
36
|
+
doneItems.push(doneMatch[1]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { todos, doneItems };
|
|
40
|
+
}
|
|
41
|
+
async function buildCommitBoundaries(tasksPath) {
|
|
42
|
+
if (tasksPath === null) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const markdown = await readText(tasksPath);
|
|
46
|
+
const boundaries = [];
|
|
47
|
+
for (const section of splitTaskSections(markdown)) {
|
|
48
|
+
const { todos, doneItems } = extractItems(section.body);
|
|
49
|
+
if (todos.length === 0 && doneItems.length === 0) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
boundaries.push({ heading: section.heading, todos, doneItems });
|
|
53
|
+
}
|
|
54
|
+
return boundaries;
|
|
55
|
+
}
|
|
56
|
+
export async function projectOpenSpecToRdInput(changeId, options = {}) {
|
|
57
|
+
const detail = await loadOpenSpecChange(changeId, options);
|
|
58
|
+
if (detail === null) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const acceptance = detail.proposal?.acceptanceCriteria ?? [];
|
|
62
|
+
const whatChanges = detail.proposal?.whatChanges ?? [];
|
|
63
|
+
const dependencies = detail.proposal?.dependencies ?? [];
|
|
64
|
+
const risks = detail.proposal?.risks ?? [];
|
|
65
|
+
const outOfScope = detail.proposal?.outOfScope ?? [];
|
|
66
|
+
const commitBoundaries = await buildCommitBoundaries(detail.paths.tasks);
|
|
67
|
+
return {
|
|
68
|
+
changeId,
|
|
69
|
+
acceptance,
|
|
70
|
+
whatChanges,
|
|
71
|
+
dependencies,
|
|
72
|
+
risks,
|
|
73
|
+
outOfScope,
|
|
74
|
+
commitBoundaries
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type JsonSchemaIssue } from '../../shared/json-schema-mini.js';
|
|
2
|
+
export type OpenSpecRenderTaskSection = {
|
|
3
|
+
heading: string;
|
|
4
|
+
todos: string[];
|
|
5
|
+
doneItems?: string[];
|
|
6
|
+
};
|
|
7
|
+
export type OpenSpecRenderRequest = {
|
|
8
|
+
changeId: string;
|
|
9
|
+
why: string;
|
|
10
|
+
whatChanges: string[];
|
|
11
|
+
acceptanceCriteria: string[];
|
|
12
|
+
outOfScope?: string[];
|
|
13
|
+
dependencies?: string[];
|
|
14
|
+
risks?: string[];
|
|
15
|
+
tasks?: OpenSpecRenderTaskSection[];
|
|
16
|
+
design?: string;
|
|
17
|
+
};
|
|
18
|
+
export type OpenSpecRenderedFile = {
|
|
19
|
+
path: string;
|
|
20
|
+
content: string;
|
|
21
|
+
};
|
|
22
|
+
export type OpenSpecRenderResult = {
|
|
23
|
+
changeId: string;
|
|
24
|
+
changeRoot: string;
|
|
25
|
+
files: OpenSpecRenderedFile[];
|
|
26
|
+
applied: boolean;
|
|
27
|
+
};
|
|
28
|
+
export type OpenSpecRenderOptions = {
|
|
29
|
+
openspecRoot?: string;
|
|
30
|
+
apply?: boolean;
|
|
31
|
+
overwrite?: boolean;
|
|
32
|
+
schemaPath?: string;
|
|
33
|
+
};
|
|
34
|
+
export declare class OpenSpecRenderRequestInvalidError extends Error {
|
|
35
|
+
readonly issues: JsonSchemaIssue[];
|
|
36
|
+
constructor(issues: JsonSchemaIssue[]);
|
|
37
|
+
}
|
|
38
|
+
export declare function renderOpenSpecChange(request: OpenSpecRenderRequest, options?: OpenSpecRenderOptions): Promise<OpenSpecRenderResult>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { isDirectory, pathExists } from '../../shared/fs.js';
|
|
5
|
+
import { validateAgainstSchema } from '../../shared/json-schema-mini.js';
|
|
6
|
+
const CHANGE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
7
|
+
function defaultOpenSpecRoot() {
|
|
8
|
+
return join(process.cwd(), 'openspec');
|
|
9
|
+
}
|
|
10
|
+
function defaultSchemaPath() {
|
|
11
|
+
const here = fileURLToPath(import.meta.url);
|
|
12
|
+
return resolve(here, '..', '..', '..', '..', 'schemas', 'openspec-render-request.schema.json');
|
|
13
|
+
}
|
|
14
|
+
let cachedSchema = null;
|
|
15
|
+
let cachedSchemaPath = null;
|
|
16
|
+
async function loadSchema(schemaPath) {
|
|
17
|
+
if (cachedSchema !== null && cachedSchemaPath === schemaPath) {
|
|
18
|
+
return cachedSchema;
|
|
19
|
+
}
|
|
20
|
+
if (!(await pathExists(schemaPath))) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const raw = await readFile(schemaPath, 'utf8');
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
cachedSchema = parsed;
|
|
26
|
+
cachedSchemaPath = schemaPath;
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
function formatSchemaIssue(issue) {
|
|
30
|
+
return `${issue.path}: ${issue.message}`;
|
|
31
|
+
}
|
|
32
|
+
export class OpenSpecRenderRequestInvalidError extends Error {
|
|
33
|
+
issues;
|
|
34
|
+
constructor(issues) {
|
|
35
|
+
const summary = issues.map(formatSchemaIssue).join('; ');
|
|
36
|
+
super(`OpenSpec render request failed schema validation: ${summary}`);
|
|
37
|
+
this.name = 'OpenSpecRenderRequestInvalidError';
|
|
38
|
+
this.issues = issues;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function renderBullets(items) {
|
|
42
|
+
if (items === undefined || items.length === 0) {
|
|
43
|
+
return '_None_\n';
|
|
44
|
+
}
|
|
45
|
+
return `${items.map((item) => `- ${item}`).join('\n')}\n`;
|
|
46
|
+
}
|
|
47
|
+
function renderProposal(request) {
|
|
48
|
+
const lines = [];
|
|
49
|
+
lines.push(`# Change: ${request.changeId}`);
|
|
50
|
+
lines.push('');
|
|
51
|
+
lines.push('## Why');
|
|
52
|
+
lines.push('');
|
|
53
|
+
lines.push(request.why.length > 0 ? request.why : '_None_');
|
|
54
|
+
lines.push('');
|
|
55
|
+
lines.push('## What Changes');
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push(renderBullets(request.whatChanges));
|
|
58
|
+
lines.push('## Out of Scope');
|
|
59
|
+
lines.push('');
|
|
60
|
+
lines.push(renderBullets(request.outOfScope));
|
|
61
|
+
lines.push('## Dependencies');
|
|
62
|
+
lines.push('');
|
|
63
|
+
lines.push(renderBullets(request.dependencies));
|
|
64
|
+
lines.push('## Risks');
|
|
65
|
+
lines.push('');
|
|
66
|
+
lines.push(renderBullets(request.risks));
|
|
67
|
+
lines.push('## Acceptance Criteria');
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push(renderBullets(request.acceptanceCriteria));
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
function renderTasks(tasks) {
|
|
73
|
+
const sections = ['# Tasks', ''];
|
|
74
|
+
for (const task of tasks) {
|
|
75
|
+
sections.push(`## ${task.heading}`);
|
|
76
|
+
sections.push('');
|
|
77
|
+
for (const todo of task.todos) {
|
|
78
|
+
sections.push(`- [ ] ${todo}`);
|
|
79
|
+
}
|
|
80
|
+
if (task.doneItems !== undefined) {
|
|
81
|
+
for (const done of task.doneItems) {
|
|
82
|
+
sections.push(`- [x] ${done}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
sections.push('');
|
|
86
|
+
}
|
|
87
|
+
return sections.join('\n');
|
|
88
|
+
}
|
|
89
|
+
function buildFiles(request, changeRoot) {
|
|
90
|
+
const files = [
|
|
91
|
+
{ path: join(changeRoot, 'proposal.md'), content: renderProposal(request) }
|
|
92
|
+
];
|
|
93
|
+
if (request.tasks !== undefined && request.tasks.length > 0) {
|
|
94
|
+
files.push({ path: join(changeRoot, 'tasks.md'), content: renderTasks(request.tasks) });
|
|
95
|
+
}
|
|
96
|
+
if (request.design !== undefined) {
|
|
97
|
+
files.push({ path: join(changeRoot, 'design.md'), content: request.design });
|
|
98
|
+
}
|
|
99
|
+
return files;
|
|
100
|
+
}
|
|
101
|
+
async function writeRenderedFiles(files) {
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
await mkdir(dirname(file.path), { recursive: true });
|
|
104
|
+
await writeFile(file.path, file.content, 'utf8');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export async function renderOpenSpecChange(request, options = {}) {
|
|
108
|
+
const schemaPath = options.schemaPath ?? defaultSchemaPath();
|
|
109
|
+
const schema = await loadSchema(schemaPath);
|
|
110
|
+
if (schema !== null) {
|
|
111
|
+
const result = validateAgainstSchema(request, schema);
|
|
112
|
+
if (!result.valid) {
|
|
113
|
+
throw new OpenSpecRenderRequestInvalidError(result.errors);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (!CHANGE_ID_PATTERN.test(request.changeId)) {
|
|
117
|
+
throw new Error(`Invalid changeId: ${request.changeId} (expected letters, digits, dots, underscores, or dashes)`);
|
|
118
|
+
}
|
|
119
|
+
const openspecRoot = options.openspecRoot ?? defaultOpenSpecRoot();
|
|
120
|
+
const changeRoot = join(openspecRoot, 'changes', request.changeId);
|
|
121
|
+
const files = buildFiles(request, changeRoot);
|
|
122
|
+
if (options.apply !== true) {
|
|
123
|
+
return { changeId: request.changeId, changeRoot, files, applied: false };
|
|
124
|
+
}
|
|
125
|
+
if (options.overwrite !== true && (await isDirectory(changeRoot))) {
|
|
126
|
+
throw new Error(`Refusing to render: change directory already exists at ${changeRoot}. Re-run with overwrite to replace it.`);
|
|
127
|
+
}
|
|
128
|
+
await writeRenderedFiles(files);
|
|
129
|
+
return { changeId: request.changeId, changeRoot, files, applied: true };
|
|
130
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { OpenSpecChangeDetail, OpenSpecScanReport } from './openspec-types.js';
|
|
2
|
+
export type OpenSpecScanOptions = {
|
|
3
|
+
openspecRoot?: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function scanOpenSpec(options?: OpenSpecScanOptions): Promise<OpenSpecScanReport>;
|
|
6
|
+
export declare function loadOpenSpecChange(changeId: string, options?: OpenSpecScanOptions): Promise<OpenSpecChangeDetail | null>;
|