@damian87/omp 0.1.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/.github/agents/architect.md +25 -0
- package/.github/agents/code-reviewer.md +25 -0
- package/.github/agents/designer.md +26 -0
- package/.github/agents/executor.md +24 -0
- package/.github/agents/planner.md +26 -0
- package/.github/agents/researcher.md +26 -0
- package/.github/agents/verifier.md +26 -0
- package/.github/copilot-instructions.md +20 -0
- package/.github/plugin/marketplace.json +30 -0
- package/.github/skills/caveman/SKILL.md +20 -0
- package/.github/skills/code-review/SKILL.md +22 -0
- package/.github/skills/codebase-research/SKILL.md +20 -0
- package/.github/skills/create-skill/SKILL.md +78 -0
- package/.github/skills/create-skill/references/best-practices.md +449 -0
- package/.github/skills/create-skill/references/examples.md +69 -0
- package/.github/skills/create-skill/references/progressive-disclosure.md +25 -0
- package/.github/skills/create-skill/references/skill-structure.md +55 -0
- package/.github/skills/debug/SKILL.md +22 -0
- package/.github/skills/grill-me/SKILL.md +16 -0
- package/.github/skills/jira-ticket/SKILL.md +21 -0
- package/.github/skills/omp-autopilot/SKILL.md +20 -0
- package/.github/skills/prototype/SKILL.md +21 -0
- package/.github/skills/ralph/SKILL.md +20 -0
- package/.github/skills/ralplan/SKILL.md +21 -0
- package/.github/skills/self-evolve/SKILL.md +157 -0
- package/.github/skills/tdd/SKILL.md +19 -0
- package/.github/skills/team/SKILL.md +20 -0
- package/.github/skills/ultraqa/SKILL.md +20 -0
- package/.github/skills/ultrawork/SKILL.md +20 -0
- package/.github/skills/verify/SKILL.md +20 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/catalog/capabilities.json +729 -0
- package/catalog/skills-general.json +427 -0
- package/dist/src/catalog.d.ts +79 -0
- package/dist/src/catalog.js +113 -0
- package/dist/src/catalog.js.map +1 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +475 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/copilot/config.d.ts +7 -0
- package/dist/src/copilot/config.js +24 -0
- package/dist/src/copilot/config.js.map +1 -0
- package/dist/src/copilot/doctor.d.ts +18 -0
- package/dist/src/copilot/doctor.js +85 -0
- package/dist/src/copilot/doctor.js.map +1 -0
- package/dist/src/copilot/launch.d.ts +14 -0
- package/dist/src/copilot/launch.js +64 -0
- package/dist/src/copilot/launch.js.map +1 -0
- package/dist/src/copilot/list.d.ts +17 -0
- package/dist/src/copilot/list.js +82 -0
- package/dist/src/copilot/list.js.map +1 -0
- package/dist/src/copilot/paths.d.ts +21 -0
- package/dist/src/copilot/paths.js +36 -0
- package/dist/src/copilot/paths.js.map +1 -0
- package/dist/src/copilot/setup.d.ts +20 -0
- package/dist/src/copilot/setup.js +90 -0
- package/dist/src/copilot/setup.js.map +1 -0
- package/dist/src/copilot/version.d.ts +13 -0
- package/dist/src/copilot/version.js +34 -0
- package/dist/src/copilot/version.js.map +1 -0
- package/dist/src/jira.d.ts +149 -0
- package/dist/src/jira.js +492 -0
- package/dist/src/jira.js.map +1 -0
- package/dist/src/lint.d.ts +11 -0
- package/dist/src/lint.js +85 -0
- package/dist/src/lint.js.map +1 -0
- package/dist/src/mcp/server.d.ts +10 -0
- package/dist/src/mcp/server.js +44 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/index.d.ts +9 -0
- package/dist/src/mcp/tools/index.js +15 -0
- package/dist/src/mcp/tools/index.js.map +1 -0
- package/dist/src/mcp/tools/notepad.d.ts +2 -0
- package/dist/src/mcp/tools/notepad.js +135 -0
- package/dist/src/mcp/tools/notepad.js.map +1 -0
- package/dist/src/mcp/tools/project-memory.d.ts +2 -0
- package/dist/src/mcp/tools/project-memory.js +91 -0
- package/dist/src/mcp/tools/project-memory.js.map +1 -0
- package/dist/src/mcp/tools/shared-memory.d.ts +2 -0
- package/dist/src/mcp/tools/shared-memory.js +148 -0
- package/dist/src/mcp/tools/shared-memory.js.map +1 -0
- package/dist/src/mcp/tools/state.d.ts +2 -0
- package/dist/src/mcp/tools/state.js +107 -0
- package/dist/src/mcp/tools/state.js.map +1 -0
- package/dist/src/mcp/tools/trace.d.ts +10 -0
- package/dist/src/mcp/tools/trace.js +102 -0
- package/dist/src/mcp/tools/trace.js.map +1 -0
- package/dist/src/mcp/types.d.ts +29 -0
- package/dist/src/mcp/types.js +7 -0
- package/dist/src/mcp/types.js.map +1 -0
- package/dist/src/mode-state/index.d.ts +4 -0
- package/dist/src/mode-state/index.js +5 -0
- package/dist/src/mode-state/index.js.map +1 -0
- package/dist/src/mode-state/paths.d.ts +5 -0
- package/dist/src/mode-state/paths.js +29 -0
- package/dist/src/mode-state/paths.js.map +1 -0
- package/dist/src/mode-state/ralph.d.ts +25 -0
- package/dist/src/mode-state/ralph.js +44 -0
- package/dist/src/mode-state/ralph.js.map +1 -0
- package/dist/src/mode-state/ultraqa.d.ts +26 -0
- package/dist/src/mode-state/ultraqa.js +51 -0
- package/dist/src/mode-state/ultraqa.js.map +1 -0
- package/dist/src/mode-state/ultrawork.d.ts +20 -0
- package/dist/src/mode-state/ultrawork.js +34 -0
- package/dist/src/mode-state/ultrawork.js.map +1 -0
- package/dist/src/project.d.ts +29 -0
- package/dist/src/project.js +101 -0
- package/dist/src/project.js.map +1 -0
- package/dist/src/skills.d.ts +17 -0
- package/dist/src/skills.js +61 -0
- package/dist/src/skills.js.map +1 -0
- package/dist/src/sync.d.ts +6 -0
- package/dist/src/sync.js +27 -0
- package/dist/src/sync.js.map +1 -0
- package/dist/src/team/api.d.ts +20 -0
- package/dist/src/team/api.js +55 -0
- package/dist/src/team/api.js.map +1 -0
- package/dist/src/team/heartbeat.d.ts +4 -0
- package/dist/src/team/heartbeat.js +27 -0
- package/dist/src/team/heartbeat.js.map +1 -0
- package/dist/src/team/idle-nudge.d.ts +27 -0
- package/dist/src/team/idle-nudge.js +60 -0
- package/dist/src/team/idle-nudge.js.map +1 -0
- package/dist/src/team/inbox.d.ts +3 -0
- package/dist/src/team/inbox.js +16 -0
- package/dist/src/team/inbox.js.map +1 -0
- package/dist/src/team/index.d.ts +11 -0
- package/dist/src/team/index.js +12 -0
- package/dist/src/team/index.js.map +1 -0
- package/dist/src/team/outbox.d.ts +14 -0
- package/dist/src/team/outbox.js +82 -0
- package/dist/src/team/outbox.js.map +1 -0
- package/dist/src/team/runtime.d.ts +84 -0
- package/dist/src/team/runtime.js +243 -0
- package/dist/src/team/runtime.js.map +1 -0
- package/dist/src/team/state-paths.d.ts +31 -0
- package/dist/src/team/state-paths.js +54 -0
- package/dist/src/team/state-paths.js.map +1 -0
- package/dist/src/team/task-store.d.ts +41 -0
- package/dist/src/team/task-store.js +153 -0
- package/dist/src/team/task-store.js.map +1 -0
- package/dist/src/team/tmux.d.ts +26 -0
- package/dist/src/team/tmux.js +87 -0
- package/dist/src/team/tmux.js.map +1 -0
- package/dist/src/team/types.d.ts +45 -0
- package/dist/src/team/types.js +2 -0
- package/dist/src/team/types.js.map +1 -0
- package/dist/src/team/worker-bootstrap.d.ts +8 -0
- package/dist/src/team/worker-bootstrap.js +52 -0
- package/dist/src/team/worker-bootstrap.js.map +1 -0
- package/docs/copilot-distribution.md +100 -0
- package/docs/general-skills.md +76 -0
- package/docs/jira.md +64 -0
- package/docs/self-evolve.md +22 -0
- package/hooks/hooks.json +74 -0
- package/package.json +58 -0
- package/plugin.json +14 -0
- package/scripts/error.mjs +31 -0
- package/scripts/lib/hook-output.mjs +30 -0
- package/scripts/lib/stdin.mjs +29 -0
- package/scripts/post-tool-use.mjs +31 -0
- package/scripts/pre-tool-use.mjs +30 -0
- package/scripts/prompt-submit.mjs +66 -0
- package/scripts/session-end.mjs +29 -0
- package/scripts/session-start.mjs +33 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export type JiraMode = 'live' | 'dry-run';
|
|
2
|
+
export type JiraOperationName = 'create' | 'comment' | 'update' | 'transition' | 'link';
|
|
3
|
+
export interface JiraOperationsConfig {
|
|
4
|
+
create?: boolean;
|
|
5
|
+
update?: boolean;
|
|
6
|
+
comment?: boolean;
|
|
7
|
+
transition?: boolean | 'discover';
|
|
8
|
+
link?: boolean | 'discover';
|
|
9
|
+
}
|
|
10
|
+
export interface JiraConfig {
|
|
11
|
+
tracker: 'jira';
|
|
12
|
+
mode: JiraMode;
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
/** Backcompat alias used by early scaffold tests. */
|
|
15
|
+
siteUrl?: string;
|
|
16
|
+
email?: string;
|
|
17
|
+
apiToken?: string;
|
|
18
|
+
projectKey?: string;
|
|
19
|
+
defaultIssueType: string;
|
|
20
|
+
operations: Required<JiraOperationsConfig>;
|
|
21
|
+
transitions: Record<string, string>;
|
|
22
|
+
linkType?: string;
|
|
23
|
+
labels?: string[];
|
|
24
|
+
components?: string[];
|
|
25
|
+
priority?: string;
|
|
26
|
+
source: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface JiraTicketInput {
|
|
29
|
+
summary: string;
|
|
30
|
+
description: string;
|
|
31
|
+
issueType?: string;
|
|
32
|
+
labels?: string[];
|
|
33
|
+
components?: string[];
|
|
34
|
+
priority?: string;
|
|
35
|
+
acceptanceCriteria?: string[];
|
|
36
|
+
implementationNotes?: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface JiraRenderedIssue {
|
|
39
|
+
operation: 'create';
|
|
40
|
+
endpoint: string;
|
|
41
|
+
payload: {
|
|
42
|
+
fields: Record<string, unknown>;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface JiraFallback {
|
|
46
|
+
operation: JiraOperationName;
|
|
47
|
+
reason: string;
|
|
48
|
+
target: string;
|
|
49
|
+
payload: unknown;
|
|
50
|
+
humanAction: string;
|
|
51
|
+
}
|
|
52
|
+
export interface JiraApplyOptions {
|
|
53
|
+
operation: JiraOperationName;
|
|
54
|
+
target?: string;
|
|
55
|
+
ticket?: JiraTicketInput;
|
|
56
|
+
comment?: string;
|
|
57
|
+
update?: Partial<JiraTicketInput>;
|
|
58
|
+
transitionState?: string;
|
|
59
|
+
linkTarget?: string;
|
|
60
|
+
dryRun?: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface JiraResult {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
live: boolean;
|
|
65
|
+
operation: JiraOperationName;
|
|
66
|
+
response?: unknown;
|
|
67
|
+
fallback?: JiraFallback;
|
|
68
|
+
}
|
|
69
|
+
export declare function loadDotEnv(cwd?: string): Record<string, string>;
|
|
70
|
+
export declare function discoverJiraConfig(options?: {
|
|
71
|
+
cwd?: string;
|
|
72
|
+
env?: NodeJS.ProcessEnv;
|
|
73
|
+
overrides?: Partial<JiraConfig>;
|
|
74
|
+
configPath?: string;
|
|
75
|
+
} | string, envArg?: NodeJS.ProcessEnv): JiraConfig;
|
|
76
|
+
export declare function ticketFromMarkdown(markdown: string, fallbackSummary?: string): JiraTicketInput;
|
|
77
|
+
export declare function readTicketInput(path: string): JiraTicketInput;
|
|
78
|
+
export declare function renderCreateIssue(ticket: JiraTicketInput, config?: JiraConfig): JiraRenderedIssue;
|
|
79
|
+
export declare function renderComment(comment: string): {
|
|
80
|
+
body: string;
|
|
81
|
+
};
|
|
82
|
+
export declare function renderSafeUpdate(update: Partial<JiraTicketInput>, config?: JiraConfig): {
|
|
83
|
+
fields: Record<string, unknown>;
|
|
84
|
+
};
|
|
85
|
+
export declare function fallbackMarkdown(fallback: JiraFallback): string;
|
|
86
|
+
export declare function makeFallback(operation: JiraOperationName, reason: string, target: string, payload: unknown): JiraFallback;
|
|
87
|
+
export declare function canRunLive(config: JiraConfig, operation: JiraOperationName): boolean;
|
|
88
|
+
export declare class JiraRestClient {
|
|
89
|
+
private readonly config;
|
|
90
|
+
constructor(config: JiraConfig);
|
|
91
|
+
createIssue(ticket: JiraTicketInput): Promise<unknown>;
|
|
92
|
+
addComment(ticketKey: string, comment: string): Promise<unknown>;
|
|
93
|
+
safeUpdate(ticketKey: string, update: Partial<JiraTicketInput>): Promise<unknown>;
|
|
94
|
+
listTransitions(ticketKey: string): Promise<Array<{
|
|
95
|
+
id: string;
|
|
96
|
+
name: string;
|
|
97
|
+
}>>;
|
|
98
|
+
transitionIssue(ticketKey: string, transitionId: string): Promise<unknown>;
|
|
99
|
+
listIssueLinkTypes(): Promise<Array<{
|
|
100
|
+
id: string;
|
|
101
|
+
name: string;
|
|
102
|
+
}>>;
|
|
103
|
+
linkIssues(inwardIssue: string, outwardIssue: string, typeName: string): Promise<unknown>;
|
|
104
|
+
private request;
|
|
105
|
+
}
|
|
106
|
+
export declare function applyJiraOperation(options: JiraApplyOptions, config?: JiraConfig): Promise<JiraResult>;
|
|
107
|
+
export declare function configSummary(config: JiraConfig): Record<string, unknown>;
|
|
108
|
+
export declare function formatJiraDryRun(config?: JiraConfig): string;
|
|
109
|
+
export declare function inferProjectRoot(fromPath: string): string;
|
|
110
|
+
/** Backcompat helper for the initial package tests and thin CLI wrappers. */
|
|
111
|
+
export declare function isJiraConfigured(config: JiraConfig): boolean;
|
|
112
|
+
/** Backcompat payload shape: REST method/endpoint/body plus configured flag. */
|
|
113
|
+
export declare function createIssuePayload(config: JiraConfig, ticket: JiraTicketInput): {
|
|
114
|
+
operation: 'create';
|
|
115
|
+
configured: boolean;
|
|
116
|
+
method: 'POST';
|
|
117
|
+
endpoint: string;
|
|
118
|
+
body: JiraRenderedIssue['payload'];
|
|
119
|
+
};
|
|
120
|
+
export declare function commentPayload(config: JiraConfig, ticketKey: string, comment: string): {
|
|
121
|
+
operation: 'comment';
|
|
122
|
+
configured: boolean;
|
|
123
|
+
method: 'POST';
|
|
124
|
+
endpoint: string;
|
|
125
|
+
body: {
|
|
126
|
+
body: string;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
export declare function safeUpdatePayload(config: JiraConfig, ticketKey: string, update: Partial<JiraTicketInput>): {
|
|
130
|
+
operation: 'update';
|
|
131
|
+
configured: boolean;
|
|
132
|
+
method: 'PUT';
|
|
133
|
+
endpoint: string;
|
|
134
|
+
body: {
|
|
135
|
+
fields: Record<string, unknown>;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
export declare function transitionFallbackPayload(config: JiraConfig, ticketKey: string, logicalState: string): {
|
|
139
|
+
operation: 'transition-fallback';
|
|
140
|
+
configured: false;
|
|
141
|
+
target: string;
|
|
142
|
+
body: JiraFallback;
|
|
143
|
+
};
|
|
144
|
+
export declare function linkFallbackPayload(config: JiraConfig, inwardIssue: string, outwardIssue: string): {
|
|
145
|
+
operation: 'link-fallback';
|
|
146
|
+
configured: false;
|
|
147
|
+
target: string;
|
|
148
|
+
body: JiraFallback;
|
|
149
|
+
};
|
package/dist/src/jira.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
const DEFAULT_TRANSITIONS = {
|
|
5
|
+
planned: 'To Do',
|
|
6
|
+
in_progress: 'In Progress',
|
|
7
|
+
review: 'In Review',
|
|
8
|
+
qa: 'QA',
|
|
9
|
+
done: 'Done',
|
|
10
|
+
blocked: 'Blocked',
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_OPERATIONS = {
|
|
13
|
+
create: true,
|
|
14
|
+
update: true,
|
|
15
|
+
comment: true,
|
|
16
|
+
transition: 'discover',
|
|
17
|
+
link: 'discover',
|
|
18
|
+
};
|
|
19
|
+
const SAFE_UPDATE_FIELDS = new Set(['summary', 'description', 'labels', 'components', 'priority']);
|
|
20
|
+
export function loadDotEnv(cwd = process.cwd()) {
|
|
21
|
+
const path = join(cwd, '.env');
|
|
22
|
+
if (!existsSync(path))
|
|
23
|
+
return {};
|
|
24
|
+
const env = {};
|
|
25
|
+
for (const rawLine of readFileSync(path, 'utf8').split(/\r?\n/)) {
|
|
26
|
+
const line = rawLine.trim();
|
|
27
|
+
if (!line || line.startsWith('#'))
|
|
28
|
+
continue;
|
|
29
|
+
const equals = line.indexOf('=');
|
|
30
|
+
if (equals === -1)
|
|
31
|
+
continue;
|
|
32
|
+
const key = line.slice(0, equals).trim();
|
|
33
|
+
const value = line.slice(equals + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
34
|
+
if (key)
|
|
35
|
+
env[key] = value;
|
|
36
|
+
}
|
|
37
|
+
return env;
|
|
38
|
+
}
|
|
39
|
+
export function discoverJiraConfig(options = {}, envArg) {
|
|
40
|
+
const normalized = typeof options === 'string' ? { cwd: options, env: envArg } : options;
|
|
41
|
+
const cwd = normalized.cwd ?? process.cwd();
|
|
42
|
+
const processEnv = normalized.env ?? process.env;
|
|
43
|
+
const dotEnv = loadDotEnv(cwd);
|
|
44
|
+
const mergedEnv = { ...dotEnv, ...processEnv };
|
|
45
|
+
const external = readExternalConfig(cwd, mergedEnv, normalized.configPath);
|
|
46
|
+
const jira = normalizeExternalJira(external.config);
|
|
47
|
+
const source = [...external.source];
|
|
48
|
+
const config = {
|
|
49
|
+
tracker: 'jira',
|
|
50
|
+
mode: normalizeMode(jira.mode ?? mergedEnv.JIRA_MODE ?? 'dry-run'),
|
|
51
|
+
baseUrl: resolveEnvRef(jira.baseUrl ?? jira.siteUrl, mergedEnv) ?? mergedEnv.JIRA_BASE_URL ?? mergedEnv.JIRA_SITE_URL,
|
|
52
|
+
email: resolveEnvRef(jira.email ?? jira.user, mergedEnv) ?? mergedEnv.JIRA_EMAIL ?? mergedEnv.JIRA_USER,
|
|
53
|
+
apiToken: resolveEnvRef(jira.apiToken ?? jira.auth ?? jira.token, mergedEnv) ?? mergedEnv.JIRA_API_TOKEN ?? mergedEnv.JIRA_TOKEN,
|
|
54
|
+
projectKey: resolveEnvRef(jira.projectKey ?? jira.project, mergedEnv) ?? mergedEnv.JIRA_PROJECT_KEY,
|
|
55
|
+
defaultIssueType: String(jira.defaultIssueType ?? mergedEnv.JIRA_DEFAULT_ISSUE_TYPE ?? 'Task'),
|
|
56
|
+
operations: { ...DEFAULT_OPERATIONS, ...(jira.operations ?? {}) },
|
|
57
|
+
transitions: { ...DEFAULT_TRANSITIONS, ...(jira.transitions ?? {}) },
|
|
58
|
+
linkType: resolveEnvRef(jira.linkType, mergedEnv) ?? mergedEnv.JIRA_LINK_TYPE,
|
|
59
|
+
labels: normalizeList(jira.labels ?? mergedEnv.JIRA_LABELS),
|
|
60
|
+
components: normalizeList(jira.components ?? mergedEnv.JIRA_COMPONENTS),
|
|
61
|
+
priority: resolveEnvRef(jira.priority, mergedEnv) ?? mergedEnv.JIRA_PRIORITY,
|
|
62
|
+
source,
|
|
63
|
+
};
|
|
64
|
+
config.siteUrl = config.baseUrl;
|
|
65
|
+
const overrides = normalized.overrides ?? {};
|
|
66
|
+
return {
|
|
67
|
+
...config,
|
|
68
|
+
...overrides,
|
|
69
|
+
operations: { ...config.operations, ...(overrides.operations ?? {}) },
|
|
70
|
+
transitions: { ...config.transitions, ...(overrides.transitions ?? {}) },
|
|
71
|
+
siteUrl: overrides.siteUrl ?? overrides.baseUrl ?? config.siteUrl,
|
|
72
|
+
source: [...config.source, ...(overrides.source ?? [])],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function readExternalConfig(cwd, env, explicitPath) {
|
|
76
|
+
const candidates = [
|
|
77
|
+
explicitPath,
|
|
78
|
+
env.OH_MY_COPILOT_JIRA_CONFIG,
|
|
79
|
+
env.JIRA_CONFIG_FILE,
|
|
80
|
+
join(homedir(), '.config', 'oh-my-copilot', 'jira.json'),
|
|
81
|
+
join(homedir(), '.config', 'oh-my-copilot', 'config.json'),
|
|
82
|
+
join(homedir(), '.oh-my-copilot', 'jira.json'),
|
|
83
|
+
join(cwd, '.oh-my-copilot.jira.json'),
|
|
84
|
+
].filter(Boolean);
|
|
85
|
+
for (const candidate of candidates) {
|
|
86
|
+
const path = isAbsolute(candidate) ? candidate : resolve(cwd, candidate);
|
|
87
|
+
if (!existsSync(path))
|
|
88
|
+
continue;
|
|
89
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
90
|
+
return { config: parsed, source: [path] };
|
|
91
|
+
}
|
|
92
|
+
return { config: {}, source: ['env/.env'] };
|
|
93
|
+
}
|
|
94
|
+
function normalizeExternalJira(raw) {
|
|
95
|
+
if (raw.jira && typeof raw.jira === 'object')
|
|
96
|
+
return raw.jira;
|
|
97
|
+
return raw;
|
|
98
|
+
}
|
|
99
|
+
function normalizeMode(value) {
|
|
100
|
+
return value === 'live' ? 'live' : 'dry-run';
|
|
101
|
+
}
|
|
102
|
+
function resolveEnvRef(value, env) {
|
|
103
|
+
if (typeof value !== 'string')
|
|
104
|
+
return undefined;
|
|
105
|
+
if (!value.startsWith('env:'))
|
|
106
|
+
return value;
|
|
107
|
+
return env[value.slice('env:'.length)];
|
|
108
|
+
}
|
|
109
|
+
function normalizeList(value) {
|
|
110
|
+
if (Array.isArray(value))
|
|
111
|
+
return value.map(String).filter(Boolean);
|
|
112
|
+
if (typeof value === 'string')
|
|
113
|
+
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
export function ticketFromMarkdown(markdown, fallbackSummary = 'Planned work') {
|
|
117
|
+
const lines = markdown.split(/\r?\n/);
|
|
118
|
+
const heading = lines.find((line) => /^#\s+/.test(line))?.replace(/^#\s+/, '').trim();
|
|
119
|
+
const firstText = lines.find((line) => line.trim() && !line.trim().startsWith('#'))?.trim();
|
|
120
|
+
const acceptanceCriteria = extractListSection(lines, /acceptance criteria/i);
|
|
121
|
+
const implementationNotes = extractListSection(lines, /implementation notes|notes/i);
|
|
122
|
+
return {
|
|
123
|
+
summary: heading || firstText || fallbackSummary,
|
|
124
|
+
description: markdown.trim() || fallbackSummary,
|
|
125
|
+
acceptanceCriteria,
|
|
126
|
+
implementationNotes,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function extractListSection(lines, headerPattern) {
|
|
130
|
+
const start = lines.findIndex((line) => headerPattern.test(line));
|
|
131
|
+
if (start === -1)
|
|
132
|
+
return undefined;
|
|
133
|
+
const items = [];
|
|
134
|
+
for (const line of lines.slice(start + 1)) {
|
|
135
|
+
if (/^#{1,6}\s+/.test(line) && items.length)
|
|
136
|
+
break;
|
|
137
|
+
const match = line.match(/^\s*[-*]\s+(.*)$/);
|
|
138
|
+
if (match)
|
|
139
|
+
items.push(match[1].trim());
|
|
140
|
+
}
|
|
141
|
+
return items.length ? items : undefined;
|
|
142
|
+
}
|
|
143
|
+
export function readTicketInput(path) {
|
|
144
|
+
const text = readFileSync(path, 'utf8');
|
|
145
|
+
if (/\.json$/i.test(path))
|
|
146
|
+
return JSON.parse(text);
|
|
147
|
+
return ticketFromMarkdown(text, `Jira ticket from ${path}`);
|
|
148
|
+
}
|
|
149
|
+
export function renderCreateIssue(ticket, config = discoverJiraConfig()) {
|
|
150
|
+
const projectKey = config.projectKey ?? '<PROJECT-KEY>';
|
|
151
|
+
const fields = {
|
|
152
|
+
project: { key: projectKey },
|
|
153
|
+
issuetype: { name: ticket.issueType ?? config.defaultIssueType },
|
|
154
|
+
summary: ticket.summary,
|
|
155
|
+
description: composeDescription(ticket),
|
|
156
|
+
};
|
|
157
|
+
const labels = [...(config.labels ?? []), ...(ticket.labels ?? [])];
|
|
158
|
+
if (labels.length)
|
|
159
|
+
fields.labels = unique(labels);
|
|
160
|
+
const components = ticket.components ?? config.components;
|
|
161
|
+
if (components?.length)
|
|
162
|
+
fields.components = components.map((name) => ({ name }));
|
|
163
|
+
const priority = ticket.priority ?? config.priority;
|
|
164
|
+
if (priority)
|
|
165
|
+
fields.priority = { name: priority };
|
|
166
|
+
return {
|
|
167
|
+
operation: 'create',
|
|
168
|
+
endpoint: '/rest/api/3/issue',
|
|
169
|
+
payload: { fields },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export function renderComment(comment) {
|
|
173
|
+
return { body: comment };
|
|
174
|
+
}
|
|
175
|
+
export function renderSafeUpdate(update, config = discoverJiraConfig()) {
|
|
176
|
+
const fields = {};
|
|
177
|
+
if (update.summary)
|
|
178
|
+
fields.summary = update.summary;
|
|
179
|
+
if (update.description || update.acceptanceCriteria || update.implementationNotes) {
|
|
180
|
+
fields.description = composeDescription({
|
|
181
|
+
summary: update.summary ?? 'Update',
|
|
182
|
+
description: update.description ?? '',
|
|
183
|
+
acceptanceCriteria: update.acceptanceCriteria,
|
|
184
|
+
implementationNotes: update.implementationNotes,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const labels = update.labels;
|
|
188
|
+
if (labels)
|
|
189
|
+
fields.labels = unique([...(config.labels ?? []), ...labels]);
|
|
190
|
+
const components = update.components;
|
|
191
|
+
if (components)
|
|
192
|
+
fields.components = components.map((name) => ({ name }));
|
|
193
|
+
const priority = update.priority;
|
|
194
|
+
if (priority)
|
|
195
|
+
fields.priority = { name: priority };
|
|
196
|
+
for (const key of Object.keys(fields)) {
|
|
197
|
+
if (!SAFE_UPDATE_FIELDS.has(key))
|
|
198
|
+
delete fields[key];
|
|
199
|
+
}
|
|
200
|
+
return { fields };
|
|
201
|
+
}
|
|
202
|
+
function composeDescription(ticket) {
|
|
203
|
+
const sections = [ticket.description.trim()];
|
|
204
|
+
if (ticket.acceptanceCriteria?.length) {
|
|
205
|
+
sections.push(['Acceptance Criteria', ...ticket.acceptanceCriteria.map((item) => `- ${item}`)].join('\n'));
|
|
206
|
+
}
|
|
207
|
+
if (ticket.implementationNotes?.length) {
|
|
208
|
+
sections.push(['Implementation Notes', ...ticket.implementationNotes.map((item) => `- ${item}`)].join('\n'));
|
|
209
|
+
}
|
|
210
|
+
return sections.filter(Boolean).join('\n\n');
|
|
211
|
+
}
|
|
212
|
+
function unique(items) {
|
|
213
|
+
return [...new Set(items.map((item) => item.trim()).filter(Boolean))];
|
|
214
|
+
}
|
|
215
|
+
export function fallbackMarkdown(fallback) {
|
|
216
|
+
return [
|
|
217
|
+
`## Jira fallback: ${fallback.operation}`,
|
|
218
|
+
`Reason: ${fallback.reason}`,
|
|
219
|
+
`Target: ${fallback.target}`,
|
|
220
|
+
'Payload:',
|
|
221
|
+
'```json',
|
|
222
|
+
JSON.stringify(fallback.payload, null, 2),
|
|
223
|
+
'```',
|
|
224
|
+
'Human action:',
|
|
225
|
+
fallback.humanAction,
|
|
226
|
+
].join('\n');
|
|
227
|
+
}
|
|
228
|
+
export function makeFallback(operation, reason, target, payload) {
|
|
229
|
+
const action = humanActionFor(operation, target);
|
|
230
|
+
return { operation, reason, target, payload, humanAction: action };
|
|
231
|
+
}
|
|
232
|
+
function humanActionFor(operation, target) {
|
|
233
|
+
switch (operation) {
|
|
234
|
+
case 'create':
|
|
235
|
+
return 'Create a Jira issue with the payload above.';
|
|
236
|
+
case 'comment':
|
|
237
|
+
return `Add the comment payload above to ${target}.`;
|
|
238
|
+
case 'update':
|
|
239
|
+
return `Update only the safe fields shown above on ${target}.`;
|
|
240
|
+
case 'transition':
|
|
241
|
+
return `Choose the matching Jira workflow transition for ${target}; do not guess an id.`;
|
|
242
|
+
case 'link':
|
|
243
|
+
return `Create the Jira issue link for ${target} only after confirming the link type exists.`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
export function canRunLive(config, operation) {
|
|
247
|
+
if (config.mode !== 'live')
|
|
248
|
+
return false;
|
|
249
|
+
if (!config.baseUrl || !config.email || !config.apiToken)
|
|
250
|
+
return false;
|
|
251
|
+
if (operation === 'create' && !config.projectKey)
|
|
252
|
+
return false;
|
|
253
|
+
const configured = config.operations[operation];
|
|
254
|
+
return configured === true || configured === 'discover';
|
|
255
|
+
}
|
|
256
|
+
export class JiraRestClient {
|
|
257
|
+
config;
|
|
258
|
+
constructor(config) {
|
|
259
|
+
this.config = config;
|
|
260
|
+
}
|
|
261
|
+
async createIssue(ticket) {
|
|
262
|
+
return this.request('/rest/api/3/issue', { method: 'POST', body: renderCreateIssue(ticket, this.config).payload });
|
|
263
|
+
}
|
|
264
|
+
async addComment(ticketKey, comment) {
|
|
265
|
+
return this.request(`/rest/api/3/issue/${encodeURIComponent(ticketKey)}/comment`, { method: 'POST', body: renderComment(comment) });
|
|
266
|
+
}
|
|
267
|
+
async safeUpdate(ticketKey, update) {
|
|
268
|
+
return this.request(`/rest/api/3/issue/${encodeURIComponent(ticketKey)}`, { method: 'PUT', body: renderSafeUpdate(update, this.config) });
|
|
269
|
+
}
|
|
270
|
+
async listTransitions(ticketKey) {
|
|
271
|
+
const response = await this.request(`/rest/api/3/issue/${encodeURIComponent(ticketKey)}/transitions`, { method: 'GET' });
|
|
272
|
+
return response.transitions ?? [];
|
|
273
|
+
}
|
|
274
|
+
async transitionIssue(ticketKey, transitionId) {
|
|
275
|
+
return this.request(`/rest/api/3/issue/${encodeURIComponent(ticketKey)}/transitions`, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
body: { transition: { id: transitionId } },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async listIssueLinkTypes() {
|
|
281
|
+
const response = await this.request('/rest/api/3/issueLinkType', { method: 'GET' });
|
|
282
|
+
return response.issueLinkTypes ?? [];
|
|
283
|
+
}
|
|
284
|
+
async linkIssues(inwardIssue, outwardIssue, typeName) {
|
|
285
|
+
return this.request('/rest/api/3/issueLink', {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
body: { type: { name: typeName }, inwardIssue: { key: inwardIssue }, outwardIssue: { key: outwardIssue } },
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
async request(path, init) {
|
|
291
|
+
if (!this.config.baseUrl || !this.config.email || !this.config.apiToken) {
|
|
292
|
+
throw new Error('Jira config is incomplete');
|
|
293
|
+
}
|
|
294
|
+
const response = await fetch(new URL(path, this.config.baseUrl), {
|
|
295
|
+
method: init.method,
|
|
296
|
+
headers: {
|
|
297
|
+
authorization: `Basic ${Buffer.from(`${this.config.email}:${this.config.apiToken}`).toString('base64')}`,
|
|
298
|
+
accept: 'application/json',
|
|
299
|
+
'content-type': 'application/json',
|
|
300
|
+
},
|
|
301
|
+
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
|
302
|
+
});
|
|
303
|
+
const text = await response.text();
|
|
304
|
+
const data = text ? JSON.parse(text) : undefined;
|
|
305
|
+
if (!response.ok)
|
|
306
|
+
throw new Error(`Jira ${init.method} ${path} failed: ${response.status} ${text}`);
|
|
307
|
+
return data;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
export async function applyJiraOperation(options, config = discoverJiraConfig()) {
|
|
311
|
+
const dryRun = options.dryRun || !canRunLive(config, options.operation);
|
|
312
|
+
const target = options.target ?? 'new issue';
|
|
313
|
+
const payload = payloadForOperation(options, config);
|
|
314
|
+
if (dryRun) {
|
|
315
|
+
return {
|
|
316
|
+
ok: true,
|
|
317
|
+
live: false,
|
|
318
|
+
operation: options.operation,
|
|
319
|
+
fallback: makeFallback(options.operation, fallbackReason(config, options.operation, options.dryRun), target, payload),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const client = new JiraRestClient(config);
|
|
323
|
+
try {
|
|
324
|
+
if (options.operation === 'create') {
|
|
325
|
+
return { ok: true, live: true, operation: 'create', response: await client.createIssue(requireTicket(options.ticket)) };
|
|
326
|
+
}
|
|
327
|
+
if (options.operation === 'comment') {
|
|
328
|
+
return { ok: true, live: true, operation: 'comment', response: await client.addComment(requireTarget(options.target), options.comment ?? '') };
|
|
329
|
+
}
|
|
330
|
+
if (options.operation === 'update') {
|
|
331
|
+
return { ok: true, live: true, operation: 'update', response: await client.safeUpdate(requireTarget(options.target), options.update ?? options.ticket ?? {}) };
|
|
332
|
+
}
|
|
333
|
+
if (options.operation === 'transition') {
|
|
334
|
+
const transition = await resolveDiscoveredTransition(client, requireTarget(options.target), options.transitionState ?? 'done', config);
|
|
335
|
+
if (!transition)
|
|
336
|
+
throw new DiscoveryRequiredError('No exact configured/discovered Jira transition matched');
|
|
337
|
+
return { ok: true, live: true, operation: 'transition', response: await client.transitionIssue(requireTarget(options.target), transition.id) };
|
|
338
|
+
}
|
|
339
|
+
if (options.operation === 'link') {
|
|
340
|
+
const linkType = await resolveDiscoveredLinkType(client, config);
|
|
341
|
+
if (!linkType)
|
|
342
|
+
throw new DiscoveryRequiredError('No configured Jira link type was discovered');
|
|
343
|
+
return { ok: true, live: true, operation: 'link', response: await client.linkIssues(requireTarget(options.target), requireTarget(options.linkTarget), linkType.name) };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
live: false,
|
|
350
|
+
operation: options.operation,
|
|
351
|
+
fallback: makeFallback(options.operation, error instanceof Error ? error.message : String(error), target, payload),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
ok: false,
|
|
356
|
+
live: false,
|
|
357
|
+
operation: options.operation,
|
|
358
|
+
fallback: makeFallback(options.operation, `Unsupported Jira operation: ${options.operation}`, target, payload),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function payloadForOperation(options, config) {
|
|
362
|
+
switch (options.operation) {
|
|
363
|
+
case 'create': return renderCreateIssue(requireTicket(options.ticket), config).payload;
|
|
364
|
+
case 'comment': return renderComment(options.comment ?? '');
|
|
365
|
+
case 'update': return renderSafeUpdate(options.update ?? options.ticket ?? {}, config);
|
|
366
|
+
case 'transition': return { transition: { logicalState: options.transitionState ?? 'done', configuredName: config.transitions[options.transitionState ?? 'done'] } };
|
|
367
|
+
case 'link': return { type: { name: config.linkType ?? '<discover>' }, inwardIssue: { key: options.target }, outwardIssue: { key: options.linkTarget } };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function fallbackReason(config, operation, explicitDryRun) {
|
|
371
|
+
if (explicitDryRun)
|
|
372
|
+
return 'dry-run requested; no live Jira write was attempted';
|
|
373
|
+
if (config.mode !== 'live')
|
|
374
|
+
return 'Jira mode is dry-run; no live Jira write was attempted';
|
|
375
|
+
if (!config.baseUrl || !config.email || !config.apiToken)
|
|
376
|
+
return 'Jira credentials/config are incomplete';
|
|
377
|
+
if (operation === 'create' && !config.projectKey)
|
|
378
|
+
return 'Jira project key is missing; live create was not attempted';
|
|
379
|
+
if (!config.operations[operation])
|
|
380
|
+
return `Jira ${operation} operation is disabled in config`;
|
|
381
|
+
return `Jira ${operation} requires discovery or live execution was not available`;
|
|
382
|
+
}
|
|
383
|
+
async function resolveDiscoveredTransition(client, ticketKey, logicalState, config) {
|
|
384
|
+
const wanted = config.transitions[logicalState] ?? logicalState;
|
|
385
|
+
const transitions = await client.listTransitions(ticketKey);
|
|
386
|
+
return transitions.find((transition) => transition.name === wanted || transition.id === wanted);
|
|
387
|
+
}
|
|
388
|
+
async function resolveDiscoveredLinkType(client, config) {
|
|
389
|
+
if (!config.linkType)
|
|
390
|
+
return undefined;
|
|
391
|
+
const linkTypes = await client.listIssueLinkTypes();
|
|
392
|
+
return linkTypes.find((type) => type.name === config.linkType || type.id === config.linkType);
|
|
393
|
+
}
|
|
394
|
+
class DiscoveryRequiredError extends Error {
|
|
395
|
+
}
|
|
396
|
+
function requireTicket(ticket) {
|
|
397
|
+
if (!ticket)
|
|
398
|
+
throw new Error('Jira ticket input is required');
|
|
399
|
+
return ticket;
|
|
400
|
+
}
|
|
401
|
+
function requireTarget(target) {
|
|
402
|
+
if (!target)
|
|
403
|
+
throw new Error('Jira ticket key/target is required');
|
|
404
|
+
return target;
|
|
405
|
+
}
|
|
406
|
+
export function configSummary(config) {
|
|
407
|
+
return {
|
|
408
|
+
tracker: config.tracker,
|
|
409
|
+
mode: config.mode,
|
|
410
|
+
baseUrlConfigured: Boolean(config.baseUrl),
|
|
411
|
+
emailConfigured: Boolean(config.email),
|
|
412
|
+
tokenConfigured: Boolean(config.apiToken),
|
|
413
|
+
projectKey: config.projectKey,
|
|
414
|
+
defaultIssueType: config.defaultIssueType,
|
|
415
|
+
operations: config.operations,
|
|
416
|
+
source: config.source,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
export function formatJiraDryRun(config = discoverJiraConfig()) {
|
|
420
|
+
const sampleTicket = {
|
|
421
|
+
issueType: config.defaultIssueType,
|
|
422
|
+
summary: 'Dry-run Jira issue',
|
|
423
|
+
description: 'Generated by oh-my-copilot jira:dry-run.',
|
|
424
|
+
labels: ['oh-my-copilot', 'dry-run'],
|
|
425
|
+
};
|
|
426
|
+
const create = createIssuePayload(config, sampleTicket);
|
|
427
|
+
const comment = commentPayload(config, 'OMC-123', 'Verification: dry-run evidence collected.');
|
|
428
|
+
const update = safeUpdatePayload(config, 'OMC-123', { summary: 'Dry-run Jira issue update', labels: ['verified'] });
|
|
429
|
+
const transition = transitionFallbackPayload(config, 'OMC-123', 'Done');
|
|
430
|
+
const link = linkFallbackPayload(config, 'OMC-123', 'OMC-124');
|
|
431
|
+
return JSON.stringify({
|
|
432
|
+
ok: true,
|
|
433
|
+
dryRun: true,
|
|
434
|
+
jira: configSummary(config),
|
|
435
|
+
operations: { create, comment, update, transition, link },
|
|
436
|
+
}, null, 2);
|
|
437
|
+
}
|
|
438
|
+
export function inferProjectRoot(fromPath) {
|
|
439
|
+
return dirname(resolve(fromPath));
|
|
440
|
+
}
|
|
441
|
+
/** Backcompat helper for the initial package tests and thin CLI wrappers. */
|
|
442
|
+
export function isJiraConfigured(config) {
|
|
443
|
+
return Boolean((config.baseUrl ?? config.siteUrl) && config.email && config.apiToken && config.projectKey);
|
|
444
|
+
}
|
|
445
|
+
/** Backcompat payload shape: REST method/endpoint/body plus configured flag. */
|
|
446
|
+
export function createIssuePayload(config, ticket) {
|
|
447
|
+
const rendered = renderCreateIssue(ticket, config);
|
|
448
|
+
return {
|
|
449
|
+
operation: 'create',
|
|
450
|
+
configured: isJiraConfigured(config),
|
|
451
|
+
method: 'POST',
|
|
452
|
+
endpoint: rendered.endpoint,
|
|
453
|
+
body: rendered.payload,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
export function commentPayload(config, ticketKey, comment) {
|
|
457
|
+
return {
|
|
458
|
+
operation: 'comment',
|
|
459
|
+
configured: isJiraConfigured(config),
|
|
460
|
+
method: 'POST',
|
|
461
|
+
endpoint: `/rest/api/3/issue/${encodeURIComponent(ticketKey)}/comment`,
|
|
462
|
+
body: renderComment(comment),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
export function safeUpdatePayload(config, ticketKey, update) {
|
|
466
|
+
return {
|
|
467
|
+
operation: 'update',
|
|
468
|
+
configured: isJiraConfigured(config),
|
|
469
|
+
method: 'PUT',
|
|
470
|
+
endpoint: `/rest/api/3/issue/${encodeURIComponent(ticketKey)}`,
|
|
471
|
+
body: renderSafeUpdate(update, config),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
export function transitionFallbackPayload(config, ticketKey, logicalState) {
|
|
475
|
+
const payload = { transition: { logicalState, configuredName: config.transitions[logicalState] ?? logicalState } };
|
|
476
|
+
return {
|
|
477
|
+
operation: 'transition-fallback',
|
|
478
|
+
configured: false,
|
|
479
|
+
target: ticketKey,
|
|
480
|
+
body: makeFallback('transition', 'transition requires Jira discovery; no transition id was guessed', ticketKey, payload),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
export function linkFallbackPayload(config, inwardIssue, outwardIssue) {
|
|
484
|
+
const payload = { type: { name: config.linkType ?? '<discover>' }, inwardIssue: { key: inwardIssue }, outwardIssue: { key: outwardIssue } };
|
|
485
|
+
return {
|
|
486
|
+
operation: 'link-fallback',
|
|
487
|
+
configured: false,
|
|
488
|
+
target: `${inwardIssue} -> ${outwardIssue}`,
|
|
489
|
+
body: makeFallback('link', 'link requires Jira link type discovery; no link type was guessed', `${inwardIssue} -> ${outwardIssue}`, payload),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
//# sourceMappingURL=jira.js.map
|