@brainervirus/workit-core 0.6.0 → 0.7.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/package.json +10 -14
- package/scripts/doctor-check.ts +20 -0
- package/scripts/install-cursor-plugin.sh +51 -28
- package/scripts/install-opencode-plugin.sh +25 -22
- package/scripts/rewrite-workspace-deps.ts +15 -9
- package/scripts/sync-runtime.sh +71 -19
- package/scripts/vendor-assets.ts +37 -0
- package/skills/wk-implement/SKILL.md +2 -2
- package/skills/wk-pr/SKILL.md +1 -1
- package/src/core/boundary.ts +27 -0
- package/src/core/branch-policy.ts +63 -0
- package/src/core/branch.ts +166 -57
- package/src/core/changelog.ts +17 -14
- package/src/core/config-guard.ts +9 -2
- package/src/core/config.ts +227 -32
- package/src/core/detector.ts +22 -11
- package/src/core/docs-layout.ts +251 -0
- package/src/core/docs-migration.ts +639 -0
- package/src/core/docs-repo.ts +58 -21
- package/src/core/docs-validate.ts +181 -43
- package/src/core/doctor.ts +801 -0
- package/src/core/flow-state.ts +1579 -137
- package/src/core/git.ts +22 -5
- package/src/core/gitignore.ts +11 -2
- package/src/core/handoff-context.ts +18 -5
- package/src/{tools/handoff.ts → core/handoff-tools.ts} +16 -63
- package/src/core/hygiene.ts +53 -17
- package/src/core/init.ts +128 -31
- package/src/core/logger.ts +321 -0
- package/src/core/package-root.ts +28 -0
- package/src/core/parse-sections.ts +2 -2
- package/src/core/plan-tasks.ts +13 -3
- package/src/core/ports/init-toolkit-status.ts +1 -1
- package/src/core/ports/vcs-verify-token.ts +1 -1
- package/src/core/ports/youtrack-api.ts +4 -2
- package/src/core/ports/youtrack-config.ts +1 -3
- package/src/core/ports/youtrack-verify-token.ts +1 -1
- package/src/core/pr-create.ts +156 -29
- package/src/core/present.ts +11 -2
- package/src/core/registration.ts +215 -0
- package/src/core/reminder.ts +1 -2
- package/src/core/repo-context.ts +447 -0
- package/src/core/repo-tool.ts +4 -1
- package/src/core/repo-tools.ts +23 -0
- package/src/core/rules.ts +10 -7
- package/src/core/safe-write.ts +22 -0
- package/src/core/scripts.ts +3 -39
- package/src/core/sdd.ts +56 -31
- package/src/core/setup-state.ts +54 -0
- package/src/core/setup.ts +1216 -0
- package/src/core/skill-manifests.ts +95 -0
- package/src/core/support-matrix.ts +12 -0
- package/src/core/sync-runtime.ts +348 -0
- package/src/core/templates.ts +16 -6
- package/src/core/vcs-config.ts +191 -62
- package/src/core/verify-parse.ts +4 -2
- package/src/core/verify-project.ts +181 -0
- package/src/core/workspaces.ts +136 -17
- package/src/core/youtrack-tools.ts +228 -0
- package/src/core/youtrack.ts +320 -87
- package/src/core.ts +18 -3
- package/templates/execution-contract.md +9 -7
- package/templates/superpowers-doc-contract.md +5 -4
- package/scripts/_shared/common.sh +0 -158
- package/scripts/changelog-context.sh +0 -42
- package/scripts/docs-refresh-context.sh +0 -40
- package/scripts/init/apply.sh +0 -5
- package/scripts/init/status.sh +0 -5
- package/scripts/init/toolkit-status.sh +0 -5
- package/scripts/pr-create.sh +0 -5
- package/scripts/pr-ready-context.sh +0 -88
- package/scripts/present/ascii-wireframe.sh +0 -5
- package/scripts/present/flow-diagram.sh +0 -5
- package/scripts/release-notes-context.sh +0 -40
- package/scripts/vcs/config.sh +0 -5
- package/scripts/vcs/merged-style.sh +0 -5
- package/scripts/vcs/token-create-urls.sh +0 -5
- package/scripts/vcs/verify-token.sh +0 -5
- package/scripts/verify-project.sh +0 -140
- package/scripts/youtrack/api.sh +0 -5
- package/scripts/youtrack/config.sh +0 -5
- package/scripts/youtrack/greeting.sh +0 -5
- package/scripts/youtrack/parse-duration.sh +0 -5
- package/scripts/youtrack/token-create-url.sh +0 -5
- package/scripts/youtrack/verify-token.sh +0 -5
- package/scripts/youtrack/work-date-ms.sh +0 -5
- package/src/tools/docs-repo.ts +0 -42
- package/src/tools/flow.ts +0 -88
- package/src/tools/index.ts +0 -22
- package/src/tools/present.ts +0 -45
- package/src/tools/repo.ts +0 -357
- package/src/tools/rules.ts +0 -30
- package/src/tools/sdd.ts +0 -189
- package/src/tools/templates.ts +0 -27
- package/src/tools/youtrack.ts +0 -360
package/src/core/workspaces.ts
CHANGED
|
@@ -1,19 +1,70 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { configDir } from "./config";
|
|
3
|
+
import { configDir, isConfigObject, type BranchPreset } from "./config";
|
|
4
4
|
|
|
5
5
|
export type VcsProvider = "gitlab" | "github";
|
|
6
6
|
|
|
7
|
+
export type IntegrationMode = "pr" | "merge";
|
|
8
|
+
|
|
9
|
+
export type WorkspaceBranchPolicy = {
|
|
10
|
+
preset: BranchPreset;
|
|
11
|
+
developBranch?: string;
|
|
12
|
+
prefixes?: { feature: string; bugfix: string; release: string; hotfix: string };
|
|
13
|
+
allowed?: string[];
|
|
14
|
+
protected?: string[];
|
|
15
|
+
integration: IntegrationMode;
|
|
16
|
+
};
|
|
17
|
+
|
|
7
18
|
export type WorkspaceConfig = {
|
|
8
19
|
name: string;
|
|
9
20
|
glob: string;
|
|
10
21
|
vcs?: { provider: VcsProvider; defaultTargetBranch?: string };
|
|
11
22
|
youtrack?: { baseUrl?: string; link_issues?: boolean };
|
|
12
23
|
issues?: { provider?: "github"; link_on_pr?: boolean };
|
|
24
|
+
branchPolicy?: WorkspaceBranchPolicy;
|
|
13
25
|
};
|
|
14
26
|
|
|
15
27
|
export const workspacesPath = (): string => path.join(configDir(), "workspaces.json");
|
|
16
28
|
|
|
29
|
+
// RL-01: typed workspaces reader. Missing is a legitimate unconfigured state
|
|
30
|
+
// (empty list); malformed JSON is reported with the exact path so risky
|
|
31
|
+
// consumers (wizard/installer/doctor) can block instead of silently resetting.
|
|
32
|
+
export type WorkspacesResult = {
|
|
33
|
+
status: "missing" | "valid" | "malformed";
|
|
34
|
+
path: string;
|
|
35
|
+
entries: WorkspaceConfig[];
|
|
36
|
+
error?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const readWorkspacesResult = (dir: string = configDir()): WorkspacesResult => {
|
|
40
|
+
const file = path.join(dir, "workspaces.json");
|
|
41
|
+
let raw: string;
|
|
42
|
+
try {
|
|
43
|
+
raw = readFileSync(file, "utf8");
|
|
44
|
+
} catch {
|
|
45
|
+
return { status: "missing", path: file, entries: [] };
|
|
46
|
+
}
|
|
47
|
+
let parsed: unknown;
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(raw);
|
|
50
|
+
} catch {
|
|
51
|
+
return { status: "malformed", path: file, entries: [], error: `${file} is not valid JSON` };
|
|
52
|
+
}
|
|
53
|
+
if (!isConfigObject(parsed)) {
|
|
54
|
+
return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
|
|
55
|
+
}
|
|
56
|
+
const list = (parsed as { workspaces?: unknown }).workspaces;
|
|
57
|
+
return {
|
|
58
|
+
status: "valid",
|
|
59
|
+
path: file,
|
|
60
|
+
entries: Array.isArray(list) ? (list as WorkspaceConfig[]) : [],
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Parse the workspaces.json list under an explicit config dir; [] when missing/malformed. */
|
|
65
|
+
export const loadWorkspacesFrom = (dir: string): WorkspaceConfig[] =>
|
|
66
|
+
readWorkspacesResult(dir).entries;
|
|
67
|
+
|
|
17
68
|
// ponytail: only globstar (`**`) is supported; if more minimatch parity is needed
|
|
18
69
|
// (char classes, braces, `?`), swap this matcher for the minimatch dependency.
|
|
19
70
|
const globToRegExp = (glob: string): RegExp => {
|
|
@@ -54,28 +105,96 @@ const globToRegExp = (glob: string): RegExp => {
|
|
|
54
105
|
return new RegExp(`^${out}$`);
|
|
55
106
|
};
|
|
56
107
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
108
|
+
/** Shared authoritative workspace matcher (WZ-12): the wizard's pattern
|
|
109
|
+
* preview and resolveWorkspaceFrom both route through it. */
|
|
110
|
+
export const matchWorkspace = (glob: string, target: string): boolean =>
|
|
111
|
+
globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/"));
|
|
112
|
+
|
|
113
|
+
// RL-08: the matcher above implements `*` and `**` only. Classic glob
|
|
114
|
+
// metacharacters it would otherwise store as literals (char classes, `?`,
|
|
115
|
+
// brace expansion) are rejected at write time so a saved pattern never
|
|
116
|
+
// silently matches nothing (Task 15 advisory: unsupported patterns were
|
|
117
|
+
// accepted and shown as "no match").
|
|
118
|
+
const UNSUPPORTED_GLOB = /[?[\]{}]/;
|
|
119
|
+
|
|
120
|
+
// B3: `!`-negation and extglob prefixes (@(...), +(...), *(...)) are the same
|
|
121
|
+
// no-match trap — the matcher stores them as literals, so `!**` or `@(a|b)`
|
|
122
|
+
// silently matches nothing. Reject them at write time too; the Task 15 match
|
|
123
|
+
// preview still routes through matchWorkspace (literals -> no match) and is
|
|
124
|
+
// unchanged.
|
|
125
|
+
const UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
|
|
126
|
+
|
|
127
|
+
export type GlobValidation = { ok: true } | { ok: false; error: string };
|
|
128
|
+
|
|
129
|
+
export const validateWorkspaceGlob = (glob: string): GlobValidation => {
|
|
130
|
+
const trimmed = glob.trim();
|
|
131
|
+
if (!trimmed) return { ok: false, error: "workspace pattern is required" };
|
|
132
|
+
const m = UNSUPPORTED_GLOB.exec(trimmed);
|
|
133
|
+
if (m) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: `unsupported glob character ${JSON.stringify(m[0])} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`,
|
|
137
|
+
};
|
|
63
138
|
}
|
|
64
|
-
|
|
139
|
+
const ext = UNSUPPORTED_EXTGLOB.exec(trimmed);
|
|
140
|
+
if (ext) {
|
|
141
|
+
const token = ext[0];
|
|
142
|
+
const kind = token === "!" ? "negation (!)" : `extglob (${token})`;
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
error: `unsupported glob ${kind} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return { ok: true };
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const realpathOf = (p: string): string => {
|
|
152
|
+
// realpathSync.native goes through libuv, which expands Windows 8.3 short
|
|
153
|
+
// names (C:\Users\RUNNER~1 -> C:\Users\runneradmin); the plain realpathSync
|
|
154
|
+
// keeps them, so a git-derived long path would miss a glob written with the
|
|
155
|
+
// short form (GitHub runner TMP uses the short name).
|
|
156
|
+
const real = realpathSync.native ?? realpathSync;
|
|
65
157
|
try {
|
|
66
|
-
|
|
158
|
+
return real(p);
|
|
67
159
|
} catch {
|
|
68
|
-
return
|
|
160
|
+
return p;
|
|
69
161
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Rebuild a glob with the realpath of its static prefix, so a config glob
|
|
165
|
+
// written with the logical path also matches a canonical target (and vice
|
|
166
|
+
// versa). The glob metacharacters themselves are left untouched.
|
|
167
|
+
const canonicalGlob = (glob: string): string => {
|
|
168
|
+
const m = /[*?[\]{]/.exec(glob);
|
|
169
|
+
const prefix = m ? glob.slice(0, m.index) : glob;
|
|
170
|
+
const rest = m ? glob.slice(m.index) : "";
|
|
171
|
+
if (!prefix) return glob;
|
|
172
|
+
const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
|
|
173
|
+
return real + (prefix.endsWith("/") ? "/" : "") + rest;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/** Match a cwd against the workspaces.json under an explicit config dir. */
|
|
177
|
+
export const resolveWorkspaceFrom = (cwd: string, dir: string): WorkspaceConfig | null => {
|
|
178
|
+
// macOS/Windows tmpdir symlinks (/var -> /private/var): git's
|
|
179
|
+
// --show-toplevel returns the realpath while config globs are usually
|
|
180
|
+
// written with the logical path, so a workspace would silently stop
|
|
181
|
+
// matching on macOS. Match both forms on each side — same class as the
|
|
182
|
+
// docs-migration escape-guard realpath comparison; on Linux both forms
|
|
183
|
+
// are identical so behavior is unchanged.
|
|
184
|
+
const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
|
|
185
|
+
for (const entry of loadWorkspacesFrom(dir)) {
|
|
75
186
|
if (!entry || typeof entry !== "object") continue;
|
|
76
187
|
const ws = entry as WorkspaceConfig;
|
|
77
188
|
if (typeof ws.glob !== "string" || !ws.glob) continue;
|
|
78
|
-
|
|
189
|
+
const glob = ws.glob.replaceAll("\\", "/");
|
|
190
|
+
const canonical = canonicalGlob(glob);
|
|
191
|
+
for (const target of targets) {
|
|
192
|
+
if (matchWorkspace(glob, target)) return ws;
|
|
193
|
+
if (canonical !== glob && matchWorkspace(canonical, target)) return ws;
|
|
194
|
+
}
|
|
79
195
|
}
|
|
80
196
|
return null;
|
|
81
197
|
};
|
|
198
|
+
|
|
199
|
+
export const resolveWorkspace = (cwd: string): WorkspaceConfig | null =>
|
|
200
|
+
resolveWorkspaceFrom(cwd, configDir());
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fail, ok, type Result } from "../core";
|
|
5
|
+
import { configDir, isConfigObject } from "./config";
|
|
6
|
+
import {
|
|
7
|
+
context as legacyContext,
|
|
8
|
+
logTime as legacyLogTime,
|
|
9
|
+
parseDuration as legacyParseDuration,
|
|
10
|
+
postUpdate as legacyPostUpdate,
|
|
11
|
+
verifyYouTrackToken,
|
|
12
|
+
} from "./youtrack";
|
|
13
|
+
|
|
14
|
+
export const ISSUE_RE = /^[A-Z]+-\d+$/;
|
|
15
|
+
export const message = (error: unknown) => (error instanceof Error ? error.message : String(error));
|
|
16
|
+
|
|
17
|
+
// Both override names point at the config dir itself, same precedence as
|
|
18
|
+
// src/core/config.ts and scripts/init/status.sh: WORKFLOW_TOOLKIT_CONFIG → WORKFLOW_TOOLKIT_CONFIG_DIR → XDG.
|
|
19
|
+
// Default env (no args) routes through configDir() so the legacy migration runs.
|
|
20
|
+
export const configPath = (env: NodeJS.ProcessEnv = process.env, home = os.homedir()) =>
|
|
21
|
+
path.join(
|
|
22
|
+
env === process.env
|
|
23
|
+
? configDir()
|
|
24
|
+
: (env.WORKFLOW_TOOLKIT_CONFIG ??
|
|
25
|
+
env.WORKFLOW_TOOLKIT_CONFIG_DIR ??
|
|
26
|
+
path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "workit")),
|
|
27
|
+
"youtrack.json",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export function readCredentials(env: NodeJS.ProcessEnv = process.env, home = os.homedir()) {
|
|
31
|
+
const resolvedConfig = configPath(env, home);
|
|
32
|
+
// AR-07/CA-37: a parseable non-object youtrack.json is malformed — exact-path
|
|
33
|
+
// error, never a raw TypeError or a silent tokenFile default.
|
|
34
|
+
let parsed: unknown;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(readFileSync(resolvedConfig, "utf8"));
|
|
37
|
+
} catch {
|
|
38
|
+
throw new Error(`${resolvedConfig} is not valid JSON`);
|
|
39
|
+
}
|
|
40
|
+
if (!isConfigObject(parsed)) {
|
|
41
|
+
throw new Error(`${resolvedConfig} is not a JSON object`);
|
|
42
|
+
}
|
|
43
|
+
const config = parsed as { tokenFile?: string };
|
|
44
|
+
const tokenFile = config.tokenFile ?? "youtrack.token";
|
|
45
|
+
const tokenPath = path.resolve(path.dirname(resolvedConfig), tokenFile.replace(/^~(?=\/)/, home));
|
|
46
|
+
if (process.platform !== "win32" && (statSync(tokenPath).mode & 0o777) !== 0o600)
|
|
47
|
+
throw new Error("youtrack.token mode must be 0600");
|
|
48
|
+
const token = readFileSync(tokenPath, "utf8").trim();
|
|
49
|
+
if (!token) throw new Error("youtrack.token is empty");
|
|
50
|
+
return { configPath: resolvedConfig, token };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const redact = (text: string, token: string) =>
|
|
54
|
+
token ? text.split(token).join("[REDACTED]") : text;
|
|
55
|
+
|
|
56
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
57
|
+
export type LegacyValue = Record<string, unknown> | void;
|
|
58
|
+
export type NotApplied = { ok: false; error: string; outcome: "not_applied" };
|
|
59
|
+
|
|
60
|
+
export type YouTrackOperations = {
|
|
61
|
+
verifyToken(): MaybePromise<LegacyValue>;
|
|
62
|
+
context(input: Record<string, unknown>): MaybePromise<LegacyValue>;
|
|
63
|
+
parseDuration(text: string, workspaceRoot: string): MaybePromise<LegacyValue>;
|
|
64
|
+
postComment(issueId: string, markdown: string, workspaceRoot?: string): MaybePromise<LegacyValue>;
|
|
65
|
+
logTime(input: Record<string, unknown>): MaybePromise<LegacyValue>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const unwrap = (value: LegacyValue) => {
|
|
69
|
+
if (!value) return {};
|
|
70
|
+
if (value.error) throw new Error(String(value.error));
|
|
71
|
+
if (value.ok === false) throw new Error(String(value.error ?? "YouTrack operation failed"));
|
|
72
|
+
return (value.data as Record<string, unknown> | undefined) ?? value;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const defaultOperations: YouTrackOperations = {
|
|
76
|
+
verifyToken: () => unwrap(verifyYouTrackToken()),
|
|
77
|
+
context: (input) => legacyContext(input as never),
|
|
78
|
+
parseDuration: (text, workspaceRoot) => legacyParseDuration(text, workspaceRoot),
|
|
79
|
+
postComment: async (issueId, markdown, workspaceRoot) =>
|
|
80
|
+
unwrap(
|
|
81
|
+
await legacyPostUpdate({
|
|
82
|
+
confirmed: true,
|
|
83
|
+
issueId,
|
|
84
|
+
markdown,
|
|
85
|
+
workspace_root: workspaceRoot,
|
|
86
|
+
} as never),
|
|
87
|
+
),
|
|
88
|
+
logTime: async (input) => unwrap(await legacyLogTime(input as never)),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type PostInput = {
|
|
92
|
+
confirmed: boolean;
|
|
93
|
+
issueId: string;
|
|
94
|
+
markdown: string;
|
|
95
|
+
minutes?: number;
|
|
96
|
+
workspace_root?: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type PostData = {
|
|
100
|
+
issueId: string;
|
|
101
|
+
postedComment: boolean;
|
|
102
|
+
loggedMinutes: number;
|
|
103
|
+
outcome?: "unknown" | "not_applied";
|
|
104
|
+
instructions?: string;
|
|
105
|
+
retry?: "workflow_youtrack_post" | "workflow_youtrack_log_time";
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const notApplied = (value: LegacyValue): value is NotApplied =>
|
|
109
|
+
value?.ok === false && value.outcome === "not_applied";
|
|
110
|
+
|
|
111
|
+
export async function postUpdate(
|
|
112
|
+
input: PostInput,
|
|
113
|
+
operations: Pick<YouTrackOperations, "postComment" | "logTime"> = defaultOperations,
|
|
114
|
+
): Promise<Result<PostData>> {
|
|
115
|
+
if (input.confirmed !== true) return fail("confirmed: true required");
|
|
116
|
+
if (!ISSUE_RE.test(input.issueId)) return fail("invalid issueId");
|
|
117
|
+
if (!input.markdown?.trim()) return fail("markdown required");
|
|
118
|
+
if (input.minutes != null && input.minutes <= 0) return fail("minutes must be positive");
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const comment = await operations.postComment(
|
|
122
|
+
input.issueId,
|
|
123
|
+
input.markdown,
|
|
124
|
+
input.workspace_root,
|
|
125
|
+
);
|
|
126
|
+
if (notApplied(comment))
|
|
127
|
+
return fail(comment.error, {
|
|
128
|
+
issueId: input.issueId,
|
|
129
|
+
postedComment: false,
|
|
130
|
+
loggedMinutes: 0,
|
|
131
|
+
outcome: "not_applied",
|
|
132
|
+
retry: "workflow_youtrack_post",
|
|
133
|
+
});
|
|
134
|
+
unwrap(comment);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return fail(message(error), {
|
|
137
|
+
issueId: input.issueId,
|
|
138
|
+
postedComment: false,
|
|
139
|
+
loggedMinutes: 0,
|
|
140
|
+
outcome: "unknown",
|
|
141
|
+
instructions: "Check YouTrack comments manually; do not retry while the outcome is unknown.",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (input.minutes != null) {
|
|
146
|
+
try {
|
|
147
|
+
const time = await operations.logTime({
|
|
148
|
+
issueId: input.issueId,
|
|
149
|
+
minutes: input.minutes,
|
|
150
|
+
text: "workit update",
|
|
151
|
+
workspace_root: input.workspace_root,
|
|
152
|
+
});
|
|
153
|
+
if (notApplied(time))
|
|
154
|
+
return fail(time.error, {
|
|
155
|
+
issueId: input.issueId,
|
|
156
|
+
postedComment: true,
|
|
157
|
+
loggedMinutes: 0,
|
|
158
|
+
outcome: "not_applied",
|
|
159
|
+
retry: "workflow_youtrack_log_time",
|
|
160
|
+
});
|
|
161
|
+
unwrap(time);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return fail(message(error), {
|
|
164
|
+
issueId: input.issueId,
|
|
165
|
+
postedComment: true,
|
|
166
|
+
loggedMinutes: 0,
|
|
167
|
+
outcome: "unknown",
|
|
168
|
+
instructions:
|
|
169
|
+
"Check YouTrack time entries manually; do not retry while the outcome is unknown.",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return ok({
|
|
175
|
+
issueId: input.issueId,
|
|
176
|
+
postedComment: true,
|
|
177
|
+
loggedMinutes: input.minutes ?? 0,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function logTimeUpdate(
|
|
182
|
+
input: Record<string, unknown>,
|
|
183
|
+
operation: Pick<YouTrackOperations, "logTime"> = defaultOperations,
|
|
184
|
+
): Promise<Result<Record<string, unknown>>> {
|
|
185
|
+
try {
|
|
186
|
+
const value = await operation.logTime(input);
|
|
187
|
+
if (notApplied(value))
|
|
188
|
+
return fail(value.error, {
|
|
189
|
+
issueId: input.issueId,
|
|
190
|
+
loggedMinutes: 0,
|
|
191
|
+
outcome: "not_applied",
|
|
192
|
+
retry: "workflow_youtrack_log_time",
|
|
193
|
+
});
|
|
194
|
+
return ok(unwrap(value));
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return fail(message(error), {
|
|
197
|
+
issueId: input.issueId,
|
|
198
|
+
loggedMinutes: 0,
|
|
199
|
+
outcome: "unknown",
|
|
200
|
+
instructions:
|
|
201
|
+
"Check YouTrack time entries manually; do not retry while the outcome is unknown.",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function normalizeContext(value: LegacyValue, mode?: string): LegacyValue {
|
|
207
|
+
if (!value || mode !== "meetings") return value;
|
|
208
|
+
const config = value.config as Record<string, unknown> | undefined;
|
|
209
|
+
const issue = String(config?.meetingIssue || "IRPT-12");
|
|
210
|
+
const { meetingIssues: _meetingIssues, ...singleMeetingConfig } = config ?? {};
|
|
211
|
+
const options = Array.isArray(value.meetingOptions)
|
|
212
|
+
? (value.meetingOptions as Array<Record<string, unknown>>)
|
|
213
|
+
: [];
|
|
214
|
+
const selected = options.find((option) => option.issue === issue) ?? {
|
|
215
|
+
key: "general",
|
|
216
|
+
issue,
|
|
217
|
+
label: issue,
|
|
218
|
+
workItemText: "Reuniones",
|
|
219
|
+
};
|
|
220
|
+
return {
|
|
221
|
+
...value,
|
|
222
|
+
config: singleMeetingConfig,
|
|
223
|
+
meetingOptions: [selected],
|
|
224
|
+
requiresMeetingChoice: false,
|
|
225
|
+
issueId: issue,
|
|
226
|
+
workItemText: selected.workItemText ?? "Reuniones",
|
|
227
|
+
};
|
|
228
|
+
}
|