refactor(extensions): simplify lib helpers, tool registration and kit-doctor
Behavior-preserving cleanups: - extract shared resolveBaseUrl helper for jira/confluence base-URL logic - extract shared registerTool helper; port jira/gitlab/confluence tools to it - make scanSecrets declarative (filter/map) - collapse ok/warn pair into status() and drop double existsSync in kit-doctor Typecheck clean; npm test 71 PASS, 0 FAIL.
This commit is contained in:
@@ -6,31 +6,16 @@
|
|||||||
* DISABLE: remove/rename this file from extensions/.
|
* DISABLE: remove/rename this file from extensions/.
|
||||||
*/
|
*/
|
||||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||||
|
import { registerTool, type ToolSpec } from "./lib/tools.ts";
|
||||||
import { search, pageGet, pageCreate } from "./lib/confluence.ts";
|
import { search, pageGet, pageCreate } from "./lib/confluence.ts";
|
||||||
|
|
||||||
export default function confluenceToolsExtension(pi: ExtensionAPI) {
|
export default function confluenceToolsExtension(pi: ExtensionAPI) {
|
||||||
const register = (name: string, description: string, parameters: Record<string, unknown>, run: (p: any) => Promise<unknown>) => {
|
const tools: ToolSpec[] = [
|
||||||
pi.registerTool({
|
|
||||||
name,
|
|
||||||
label: name,
|
|
||||||
description,
|
|
||||||
parameters: parameters as any,
|
|
||||||
async execute(_id: string, params: unknown) {
|
|
||||||
try {
|
|
||||||
const out = await run(params ?? {});
|
|
||||||
return { content: [{ type: "text", text: JSON.stringify(out ?? {}, null, 2) }], details: {} };
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
return { content: [{ type: "text", text: `confluence error: ${msg}` }], isError: true, details: {} };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
register(
|
|
||||||
"confluence_search",
|
|
||||||
"Найти страницы в Confluence по CQL-запросу (текст/заголовок/пространство).",
|
|
||||||
{
|
{
|
||||||
|
name: "confluence_search",
|
||||||
|
description: "Найти страницы в Confluence по CQL-запросу (текст/заголовок/пространство).",
|
||||||
|
errorPrefix: "confluence",
|
||||||
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
cql: { type: "string", description: 'CQL, например text="деплой" AND space = TECHRUN' },
|
cql: { type: "string", description: 'CQL, например text="деплой" AND space = TECHRUN' },
|
||||||
@@ -39,13 +24,13 @@ export default function confluenceToolsExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
required: ["cql"],
|
required: ["cql"],
|
||||||
},
|
},
|
||||||
async (p) => search(p.cql, { limit: p.limit, expandBody: p.expandBody }),
|
run: async (p) => search(p.cql, { limit: p.limit, expandBody: p.expandBody }),
|
||||||
);
|
},
|
||||||
|
|
||||||
register(
|
|
||||||
"confluence_page_get",
|
|
||||||
"Получить страницу Confluence по ID (с содержимым, если expandBody).",
|
|
||||||
{
|
{
|
||||||
|
name: "confluence_page_get",
|
||||||
|
description: "Получить страницу Confluence по ID (с содержимым, если expandBody).",
|
||||||
|
errorPrefix: "confluence",
|
||||||
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: "string", description: "ID страницы" },
|
id: { type: "string", description: "ID страницы" },
|
||||||
@@ -53,13 +38,13 @@ export default function confluenceToolsExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
required: ["id"],
|
required: ["id"],
|
||||||
},
|
},
|
||||||
async (p) => pageGet(p.id, { expandBody: p.expandBody }),
|
run: async (p) => pageGet(p.id, { expandBody: p.expandBody }),
|
||||||
);
|
},
|
||||||
|
|
||||||
register(
|
|
||||||
"confluence_page_create",
|
|
||||||
"Создать страницу в Confluence. body — HTML в Storage format.",
|
|
||||||
{
|
{
|
||||||
|
name: "confluence_page_create",
|
||||||
|
description: "Создать страницу в Confluence. body — HTML в Storage format.",
|
||||||
|
errorPrefix: "confluence",
|
||||||
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
spaceKey: { type: "string", description: "Ключ пространства" },
|
spaceKey: { type: "string", description: "Ключ пространства" },
|
||||||
@@ -69,6 +54,9 @@ export default function confluenceToolsExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
required: ["spaceKey", "title", "body"],
|
required: ["spaceKey", "title", "body"],
|
||||||
},
|
},
|
||||||
async (p) => pageCreate(p),
|
run: async (p) => pageCreate(p),
|
||||||
);
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const t of tools) registerTool(pi, t);
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-36
@@ -6,31 +6,16 @@
|
|||||||
* DISABLE: remove/rename this file from extensions/.
|
* DISABLE: remove/rename this file from extensions/.
|
||||||
*/
|
*/
|
||||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||||
import { mrCreate, pipelineStatus, codeSearch, deriveProjectFromOrigin } from "./lib/gitlab.ts";
|
import { registerTool, type ToolSpec } from "./lib/tools.ts";
|
||||||
|
import { mrCreate, pipelineStatus, codeSearch } from "./lib/gitlab.ts";
|
||||||
|
|
||||||
export default function gitlabToolsExtension(pi: ExtensionAPI) {
|
export default function gitlabToolsExtension(pi: ExtensionAPI) {
|
||||||
const register = (name: string, description: string, parameters: Record<string, unknown>, run: (p: any) => Promise<unknown>) => {
|
const tools: ToolSpec[] = [
|
||||||
pi.registerTool({
|
|
||||||
name,
|
|
||||||
label: name,
|
|
||||||
description,
|
|
||||||
parameters: parameters as any,
|
|
||||||
async execute(_id: string, params: unknown) {
|
|
||||||
try {
|
|
||||||
const out = await run(params ?? {});
|
|
||||||
return { content: [{ type: "text", text: JSON.stringify(out ?? {}, null, 2) }], details: {} };
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
return { content: [{ type: "text", text: `gitlab error: ${msg}` }], isError: true, details: {} };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
register(
|
|
||||||
"gitlab_mr_create",
|
|
||||||
"Создать merge request в GitLab. Проект определяется из git remote origin (или укажи project).",
|
|
||||||
{
|
{
|
||||||
|
name: "gitlab_mr_create",
|
||||||
|
description: "Создать merge request в GitLab. Проект определяется из git remote origin (или укажи project).",
|
||||||
|
errorPrefix: "gitlab",
|
||||||
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
title: { type: "string", description: "Заголовок MR" },
|
title: { type: "string", description: "Заголовок MR" },
|
||||||
@@ -42,13 +27,21 @@ export default function gitlabToolsExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
required: ["title"],
|
required: ["title"],
|
||||||
},
|
},
|
||||||
async (p) => mrCreate({ title: p.title, sourceBranch: p.sourceBranch || undefined, targetBranch: p.targetBranch, project: p.project, removeSourceBranch: p.removeSourceBranch, mergeWhenPipelineSucceeds: p.mergeWhenPipelineSucceeds }),
|
run: async (p) =>
|
||||||
);
|
mrCreate({
|
||||||
|
title: p.title,
|
||||||
register(
|
sourceBranch: p.sourceBranch || undefined,
|
||||||
"gitlab_pipeline_status",
|
targetBranch: p.targetBranch,
|
||||||
"Показать статусы CI/CD пайплайнов проекта GitLab (по ветке).",
|
project: p.project,
|
||||||
|
removeSourceBranch: p.removeSourceBranch,
|
||||||
|
mergeWhenPipelineSucceeds: p.mergeWhenPipelineSucceeds,
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
name: "gitlab_pipeline_status",
|
||||||
|
description: "Показать статусы CI/CD пайплайнов проекта GitLab (по ветке).",
|
||||||
|
errorPrefix: "gitlab",
|
||||||
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
project: { type: "string", description: "Группа/репозиторий (обязательно; например group/name)" },
|
project: { type: "string", description: "Группа/репозиторий (обязательно; например group/name)" },
|
||||||
@@ -57,13 +50,13 @@ export default function gitlabToolsExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
required: ["project"],
|
required: ["project"],
|
||||||
},
|
},
|
||||||
async (p) => pipelineStatus(p.project, { ref: p.ref, perPage: p.perPage }),
|
run: async (p) => pipelineStatus(p.project, { ref: p.ref, perPage: p.perPage }),
|
||||||
);
|
},
|
||||||
|
|
||||||
register(
|
|
||||||
"gitlab_code_search",
|
|
||||||
"Поиск по коду в GitLab (blobs). Требует scope api/read_api у токена.",
|
|
||||||
{
|
{
|
||||||
|
name: "gitlab_code_search",
|
||||||
|
description: "Поиск по коду в GitLab (blobs). Требует scope api/read_api у токена.",
|
||||||
|
errorPrefix: "gitlab",
|
||||||
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
query: { type: "string", description: "Строка поиска (до 512 символов)" },
|
query: { type: "string", description: "Строка поиска (до 512 символов)" },
|
||||||
@@ -72,6 +65,9 @@ export default function gitlabToolsExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
required: ["query"],
|
required: ["query"],
|
||||||
},
|
},
|
||||||
async (p) => codeSearch(p.query, { project: p.project, perPage: p.perPage }),
|
run: async (p) => codeSearch(p.query, { project: p.project, perPage: p.perPage }),
|
||||||
);
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const t of tools) registerTool(pi, t);
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-62
@@ -9,6 +9,7 @@
|
|||||||
* DISABLE: remove/rename this file from extensions/.
|
* DISABLE: remove/rename this file from extensions/.
|
||||||
*/
|
*/
|
||||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||||
|
import { registerTool, type ToolSpec } from "./lib/tools.ts";
|
||||||
import {
|
import {
|
||||||
issueGet,
|
issueGet,
|
||||||
issueCreate,
|
issueCreate,
|
||||||
@@ -19,22 +20,20 @@ import {
|
|||||||
linkMr,
|
linkMr,
|
||||||
} from "./lib/jira.ts";
|
} from "./lib/jira.ts";
|
||||||
|
|
||||||
function tool(name: string, description: string, parameters: Record<string, unknown>, run: (params: any) => Promise<unknown>) {
|
|
||||||
return { name, description, parameters, run };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function jiraToolsExtension(pi: ExtensionAPI) {
|
export default function jiraToolsExtension(pi: ExtensionAPI) {
|
||||||
const tools = [
|
const tools: ToolSpec[] = [
|
||||||
tool(
|
|
||||||
"jira_issue_get",
|
|
||||||
"Получить тикет Jira по ключу (summary, status, description).",
|
|
||||||
{ type: "object", properties: { key: { type: "string", description: "Ключ задачи, например PROJ-123" } }, required: ["key"] },
|
|
||||||
async (p) => issueGet(p.key),
|
|
||||||
),
|
|
||||||
tool(
|
|
||||||
"jira_issue_create",
|
|
||||||
"Создать задачу в Jira.",
|
|
||||||
{
|
{
|
||||||
|
name: "jira_issue_get",
|
||||||
|
description: "Получить тикет Jira по ключу (summary, status, description).",
|
||||||
|
errorPrefix: "jira",
|
||||||
|
parameters: { type: "object", properties: { key: { type: "string", description: "Ключ задачи, например PROJ-123" } }, required: ["key"] },
|
||||||
|
run: async (p) => issueGet(p.key),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jira_issue_create",
|
||||||
|
description: "Создать задачу в Jira.",
|
||||||
|
errorPrefix: "jira",
|
||||||
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
projectKey: { type: "string", description: "Ключ проекта, например PROJ" },
|
projectKey: { type: "string", description: "Ключ проекта, например PROJ" },
|
||||||
@@ -47,12 +46,13 @@ export default function jiraToolsExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
required: ["projectKey", "summary", "issuetype"],
|
required: ["projectKey", "summary", "issuetype"],
|
||||||
},
|
},
|
||||||
async (p) => issueCreate(p),
|
run: async (p) => issueCreate(p),
|
||||||
),
|
},
|
||||||
tool(
|
|
||||||
"jira_issue_update",
|
|
||||||
"Обновить поля задачи Jira (summary/description/status и т.п.). Status переводит по доступным переходам.",
|
|
||||||
{
|
{
|
||||||
|
name: "jira_issue_update",
|
||||||
|
description: "Обновить поля задачи Jira (summary/description/status и т.п.). Status переводит по доступным переходам.",
|
||||||
|
errorPrefix: "jira",
|
||||||
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
key: { type: "string", description: "Ключ задачи" },
|
key: { type: "string", description: "Ключ задачи" },
|
||||||
@@ -65,12 +65,13 @@ export default function jiraToolsExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
required: ["key"],
|
required: ["key"],
|
||||||
},
|
},
|
||||||
async (p) => issueUpdate(p.key, p),
|
run: async (p) => issueUpdate(p.key, p),
|
||||||
),
|
},
|
||||||
tool(
|
|
||||||
"jira_issue_comment",
|
|
||||||
"Добавить комментарий к тикету Jira.",
|
|
||||||
{
|
{
|
||||||
|
name: "jira_issue_comment",
|
||||||
|
description: "Добавить комментарий к тикету Jira.",
|
||||||
|
errorPrefix: "jira",
|
||||||
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
key: { type: "string", description: "Ключ задачи" },
|
key: { type: "string", description: "Ключ задачи" },
|
||||||
@@ -78,12 +79,13 @@ export default function jiraToolsExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
required: ["key", "text"],
|
required: ["key", "text"],
|
||||||
},
|
},
|
||||||
async (p) => issueComment(p.key, p.text),
|
run: async (p) => issueComment(p.key, p.text),
|
||||||
),
|
},
|
||||||
tool(
|
|
||||||
"jira_issue_transition",
|
|
||||||
"Перевести тикет Jira в статус (To Do / In Progress / Review / Done).",
|
|
||||||
{
|
{
|
||||||
|
name: "jira_issue_transition",
|
||||||
|
description: "Перевести тикет Jira в статус (To Do / In Progress / Review / Done).",
|
||||||
|
errorPrefix: "jira",
|
||||||
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
key: { type: "string", description: "Ключ задачи" },
|
key: { type: "string", description: "Ключ задачи" },
|
||||||
@@ -91,12 +93,13 @@ export default function jiraToolsExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
required: ["key", "target"],
|
required: ["key", "target"],
|
||||||
},
|
},
|
||||||
async (p) => setStatus(p.key, p.target),
|
run: async (p) => setStatus(p.key, p.target),
|
||||||
),
|
},
|
||||||
tool(
|
|
||||||
"jira_search",
|
|
||||||
"Найти тикеты Jira по JQL-запросу.",
|
|
||||||
{
|
{
|
||||||
|
name: "jira_search",
|
||||||
|
description: "Найти тикеты Jira по JQL-запросу.",
|
||||||
|
errorPrefix: "jira",
|
||||||
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
jql: { type: "string", description: "JQL, например 'project = PROJ AND status != Done'" },
|
jql: { type: "string", description: "JQL, например 'project = PROJ AND status != Done'" },
|
||||||
@@ -104,12 +107,13 @@ export default function jiraToolsExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
required: ["jql"],
|
required: ["jql"],
|
||||||
},
|
},
|
||||||
async (p) => search(p.jql, p.maxResults),
|
run: async (p) => search(p.jql, p.maxResults),
|
||||||
),
|
},
|
||||||
tool(
|
|
||||||
"jira_link_mr",
|
|
||||||
"Привязать ссылку на merge request к тикету Jira (комментарий со ссылкой).",
|
|
||||||
{
|
{
|
||||||
|
name: "jira_link_mr",
|
||||||
|
description: "Привязать ссылку на merge request к тикету Jira (комментарий со ссылкой).",
|
||||||
|
errorPrefix: "jira",
|
||||||
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
key: { type: "string", description: "Ключ задачи" },
|
key: { type: "string", description: "Ключ задачи" },
|
||||||
@@ -118,29 +122,9 @@ export default function jiraToolsExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
required: ["key", "mrUrl"],
|
required: ["key", "mrUrl"],
|
||||||
},
|
},
|
||||||
async (p) => linkMr(p.key, p.mrUrl, p.title),
|
run: async (p) => linkMr(p.key, p.mrUrl, p.title),
|
||||||
),
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const t of tools) {
|
for (const t of tools) registerTool(pi, t);
|
||||||
pi.registerTool({
|
|
||||||
name: t.name,
|
|
||||||
label: t.name,
|
|
||||||
description: t.description,
|
|
||||||
parameters: t.parameters as any,
|
|
||||||
async execute(_id: string, params: unknown) {
|
|
||||||
try {
|
|
||||||
const out = await t.run(params ?? {});
|
|
||||||
return { content: [{ type: "text", text: JSON.stringify(out ?? {}, null, 2) }], details: {} };
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: `jira error: ${msg}` }],
|
|
||||||
isError: true,
|
|
||||||
details: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -63,40 +63,40 @@ export default function kitCliExtension(pi: ExtensionAPI) {
|
|||||||
pi.registerCommand("kit-doctor", {
|
pi.registerCommand("kit-doctor", {
|
||||||
description: "Проверка окружения pi-kit (node, версия, конфиг, env, токены)",
|
description: "Проверка окружения pi-kit (node, версия, конфиг, env, токены)",
|
||||||
handler: async (_args, ctx) => {
|
handler: async (_args, ctx) => {
|
||||||
const ok = (s: string) => `✓ ${s}`;
|
const status = (ok: boolean, text: string) => `${ok ? "✓" : "⚠"} ${text}`;
|
||||||
const warn = (s: string) => `⚠ ${s}`;
|
|
||||||
const cfg = readConfig();
|
const cfg = readConfig();
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|
||||||
lines.push(ok(`Node.js ${process.version}`));
|
lines.push(status(true, `Node.js ${process.version}`));
|
||||||
lines.push(ok(`pi-kit v${readVersion()}${process.env.PI_KIT_CHANNEL ? ` (канал: ${process.env.PI_KIT_CHANNEL})` : ""}`));
|
lines.push(status(true, `pi-kit v${readVersion()}${process.env.PI_KIT_CHANNEL ? ` (канал: ${process.env.PI_KIT_CHANNEL})` : ""}`));
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
const cfgOk = typeof cfg.gitHost === "string" && cfg.gitHost !== "TODO";
|
const cfgOk = typeof cfg.gitHost === "string" && cfg.gitHost !== "TODO";
|
||||||
lines.push((cfgOk ? ok : warn)(`config: gitHost=${cfg.gitHost ?? "?"}, tracker=${cfg.trackerUrl ?? "?"}, docs=${cfg.docsUrl ?? "?"}`));
|
lines.push(status(cfgOk, `config: gitHost=${cfg.gitHost ?? "?"}, tracker=${cfg.trackerUrl ?? "?"}, docs=${cfg.docsUrl ?? "?"}`));
|
||||||
if (cfg.remoteConfigUrl) lines.push(ok(`remoteConfigUrl задан`));
|
if (cfg.remoteConfigUrl) lines.push(status(true, `remoteConfigUrl задан`));
|
||||||
// TODO-placeholder values are a readiness signal, not an error: warn so
|
// TODO-placeholder values are a readiness signal, not an error: warn so
|
||||||
// maintainers notice they haven't filled the single source of truth yet.
|
// maintainers notice they haven't filled the single source of truth yet.
|
||||||
for (const [field, value] of Object.entries({ repoMap: cfg.repoMap, trackerUrl: cfg.trackerUrl, docsUrl: cfg.docsUrl })) {
|
for (const [field, value] of Object.entries({ repoMap: cfg.repoMap, trackerUrl: cfg.trackerUrl, docsUrl: cfg.docsUrl })) {
|
||||||
if (typeof value !== "string" || value === "TODO" || value.trim() === "" || value.startsWith("TODO")) {
|
if (typeof value !== "string" || value === "TODO" || value.trim() === "" || value.startsWith("TODO")) {
|
||||||
lines.push(warn(`config: ${field} не заполнен (TODO) — скиллы repo-map/docs-map/jira будут просить уточнить`));
|
lines.push(status(false, `config: ${field} не заполнен (TODO) — скиллы repo-map/docs-map/jira будут просить уточнить`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Env vars (presence only, never values)
|
// Env vars (presence only, never values)
|
||||||
for (const v of ["TRACKER_URL", "JIRA_TOKEN", "CONFLUENCE_TOKEN", "GITLAB_TOKEN"]) {
|
for (const v of ["TRACKER_URL", "JIRA_TOKEN", "CONFLUENCE_TOKEN", "GITLAB_TOKEN"]) {
|
||||||
lines.push((process.env[v] ? ok : warn)(`${v} ${process.env[v] ? "задан" : "не задан"}`));
|
lines.push(status(Boolean(process.env[v]), `${v} ${process.env[v] ? "задан" : "не задан"}`));
|
||||||
}
|
}
|
||||||
const provider = PROVIDER_KEYS.find((k) => process.env[k]);
|
const provider = PROVIDER_KEYS.find((k) => process.env[k]);
|
||||||
lines.push((provider ? ok : warn)(provider ? `LLM-ключ: ${provider}` : "LLM-ключ провайдера не найден"));
|
lines.push(status(Boolean(provider), provider ? `LLM-ключ: ${provider}` : "LLM-ключ провайдера не найден"));
|
||||||
|
|
||||||
// env.sh
|
// env.sh
|
||||||
lines.push((fs.existsSync(ENV_FILE) ? ok : warn)(`env.sh ${fs.existsSync(ENV_FILE) ? "есть" : "нет"} (${ENV_FILE})`));
|
const envShExists = fs.existsSync(ENV_FILE);
|
||||||
|
lines.push(status(envShExists, `env.sh ${envShExists ? "есть" : "нет"} (${ENV_FILE})`));
|
||||||
|
|
||||||
// Loaded commands/skills
|
// Loaded commands/skills
|
||||||
const cmds = pi.getCommands();
|
const cmds = pi.getCommands();
|
||||||
const skills = cmds.filter((c) => c.source === "skill").length;
|
const skills = cmds.filter((c) => c.source === "skill").length;
|
||||||
lines.push(ok(`команд загружено: ${cmds.length} (скилов: ${skills})`));
|
lines.push(status(true, `команд загружено: ${cmds.length} (скилов: ${skills})`));
|
||||||
|
|
||||||
const warns = lines.filter((l) => l.startsWith("⚠")).length;
|
const warns = lines.filter((l) => l.startsWith("⚠")).length;
|
||||||
lines.push("", warns === 0 ? "Итог: всё ок." : `Итог: предупреждений — ${warns}.`);
|
lines.push("", warns === 0 ? "Итог: всё ок." : `Итог: предупреждений — ${warns}.`);
|
||||||
|
|||||||
@@ -36,6 +36,18 @@ function base(url: string): string {
|
|||||||
return url.replace(/\/+$/, "");
|
return url.replace(/\/+$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve an effective base URL from company.json + an env override.
|
||||||
|
* The env var wins when set (and not "TODO"); otherwise the config value
|
||||||
|
* (if set and not "TODO") is used. Trailing slashes are trimmed. Returns ""
|
||||||
|
* when neither is configured; callers decide whether to throw.
|
||||||
|
*/
|
||||||
|
export function resolveBaseUrl(envName: string, cfgValue: string | null | undefined): string {
|
||||||
|
const fromCfg = cfgValue && cfgValue !== "TODO" ? cfgValue : "";
|
||||||
|
const env = process.env[envName];
|
||||||
|
return (env && env !== "TODO" ? env : fromCfg).replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
/** Build a URL with query params; undefined/empty values are dropped. */
|
/** Build a URL with query params; undefined/empty values are dropped. */
|
||||||
export function buildUrl(
|
export function buildUrl(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* `docsUrl` comes from config/company.json; `CONFLUENCE_TOKEN` from the env.
|
* `docsUrl` comes from config/company.json; `CONFLUENCE_TOKEN` from the env.
|
||||||
* No pi API — unit-testable.
|
* No pi API — unit-testable.
|
||||||
*/
|
*/
|
||||||
import { apiRequest, requireToken, ApiError } from "./atlassian.ts";
|
import { apiRequest, requireToken, ApiError, resolveBaseUrl } from "./atlassian.ts";
|
||||||
import { readLocalConfig } from "./company-config.ts";
|
import { readLocalConfig } from "./company-config.ts";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@@ -14,8 +14,7 @@ const CONFIG_PATH = path.join(HERE, "..", "..", "config", "company.json");
|
|||||||
/** Resolve the Confluence base URL from company.json (env override for labs). */
|
/** Resolve the Confluence base URL from company.json (env override for labs). */
|
||||||
export function confluenceBaseUrl(): string {
|
export function confluenceBaseUrl(): string {
|
||||||
const cfg = readLocalConfig(CONFIG_PATH);
|
const cfg = readLocalConfig(CONFIG_PATH);
|
||||||
const fromCfg = cfg.docsUrl && cfg.docsUrl !== "TODO" ? cfg.docsUrl : "";
|
const url = resolveBaseUrl("CONFLUENCE_URL", cfg.docsUrl);
|
||||||
const url = (process.env.CONFLUENCE_URL && process.env.CONFLUENCE_URL !== "TODO" ? process.env.CONFLUENCE_URL : fromCfg).replace(/\/+$/, "");
|
|
||||||
if (!url) throw new ApiError("Не задан docsUrl в config/company.json и нет CONFLUENCE_URL.");
|
if (!url) throw new ApiError("Не задан docsUrl в config/company.json и нет CONFLUENCE_URL.");
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* `trackerUrl` comes from config/company.json; `JIRA_TOKEN` from the env.
|
* `trackerUrl` comes from config/company.json; `JIRA_TOKEN` from the env.
|
||||||
* No pi API — unit-testable (see test/api.test.ts).
|
* No pi API — unit-testable (see test/api.test.ts).
|
||||||
*/
|
*/
|
||||||
import { apiRequest, requireToken, ApiError } from "./atlassian.ts";
|
import { apiRequest, requireToken, ApiError, resolveBaseUrl } from "./atlassian.ts";
|
||||||
import { readLocalConfig } from "./company-config.ts";
|
import { readLocalConfig } from "./company-config.ts";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@@ -11,12 +11,11 @@ import { fileURLToPath } from "node:url";
|
|||||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const CONFIG_PATH = path.join(HERE, "..", "..", "config", "company.json");
|
const CONFIG_PATH = path.join(HERE, "..", "..", "config", "company.json");
|
||||||
|
|
||||||
/** Resolve the tracker base URL — prefer company.json; env override wins. */
|
/** Resolve the tracker base URL — company.json default, env override wins. */
|
||||||
export function jiraBaseUrl(): string {
|
export function jiraBaseUrl(): string {
|
||||||
const cfg = readLocalConfig(CONFIG_PATH);
|
const cfg = readLocalConfig(CONFIG_PATH);
|
||||||
const fromCfg = cfg.trackerUrl && cfg.trackerUrl !== "TODO" ? cfg.trackerUrl : "";
|
|
||||||
// env TRACKER_URL may override the packaged default for local labs.
|
// env TRACKER_URL may override the packaged default for local labs.
|
||||||
return (process.env.TRACKER_URL && process.env.TRACKER_URL !== "TODO" ? process.env.TRACKER_URL : fromCfg).replace(/\/+$/, "");
|
return resolveBaseUrl("TRACKER_URL", cfg.trackerUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
function token(): string {
|
function token(): string {
|
||||||
|
|||||||
@@ -29,12 +29,8 @@ export const SECRET_PATTERNS: SecretPattern[] = [
|
|||||||
/** Return the names of secret patterns found in `text` (empty if none). */
|
/** Return the names of secret patterns found in `text` (empty if none). */
|
||||||
export function scanSecrets(text: string): string[] {
|
export function scanSecrets(text: string): string[] {
|
||||||
if (!text) return [];
|
if (!text) return [];
|
||||||
const hits: string[] = [];
|
// No pattern carries the /g flag, so `test` stays stateless and order is preserved.
|
||||||
for (const p of SECRET_PATTERNS) {
|
return SECRET_PATTERNS.filter((p) => p.re.test(text)).map((p) => p.name);
|
||||||
// Use a non-stateful copy (no global flag) to avoid lastIndex surprises.
|
|
||||||
if (p.re.test(text)) hits.push(p.name);
|
|
||||||
}
|
|
||||||
return hits;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Replace every secret occurrence with a labelled placeholder. */
|
/** Replace every secret occurrence with a labelled placeholder. */
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Shared native pi tool registration.
|
||||||
|
* The three *-tools.ts extensions each register tools whose `execute` bodies
|
||||||
|
* are byte-identical apart from the error prefix in the failure payload.
|
||||||
|
* This helper DRYs that boilerplate; extensions only supply the spec.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||||
|
|
||||||
|
export interface ToolSpec {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
parameters: Record<string, unknown>;
|
||||||
|
/** Prefix in the error payload, e.g. "jira" → "jira error: …". */
|
||||||
|
errorPrefix: string;
|
||||||
|
run: (params: any) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Register a native tool whose result/error payload is formatted uniformly. */
|
||||||
|
export function registerTool(pi: ExtensionAPI, spec: ToolSpec): void {
|
||||||
|
pi.registerTool({
|
||||||
|
name: spec.name,
|
||||||
|
label: spec.name,
|
||||||
|
description: spec.description,
|
||||||
|
parameters: spec.parameters as any,
|
||||||
|
async execute(_id: string, params: unknown) {
|
||||||
|
try {
|
||||||
|
const out = await spec.run(params ?? {});
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(out ?? {}, null, 2) }], details: {} };
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
return { content: [{ type: "text", text: `${spec.errorPrefix} error: ${msg}` }], isError: true, details: {} };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user