@brainervirus/workit-core 2.0.2 → 2.1.1

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.
Files changed (44) hide show
  1. package/README.md +2 -2
  2. package/package.json +2 -3
  3. package/scripts/sync-runtime.sh +9 -5
  4. package/src/core/detect-hosts.ts +24 -22
  5. package/src/core/doctor.ts +11 -4
  6. package/src/core/host-install.ts +458 -0
  7. package/src/core/init.ts +1 -256
  8. package/src/core/policy-resolver.ts +0 -45
  9. package/src/core/registration.ts +3 -4
  10. package/src/core/repo-context.ts +1 -1
  11. package/src/core/rules.ts +1 -61
  12. package/src/core/setup.ts +203 -11
  13. package/src/core/templates.ts +1 -26
  14. package/src/core/vcs-config.ts +2 -8
  15. package/src/core/workspaces.ts +18 -17
  16. package/src/core/config-guard.ts +0 -27
  17. package/src/core/doc-render.ts +0 -14
  18. package/src/core/docs-layout.ts +0 -279
  19. package/src/core/docs-migration.ts +0 -639
  20. package/src/core/docs-repo.ts +0 -233
  21. package/src/core/docs-validate.ts +0 -393
  22. package/src/core/parse-sections.ts +0 -24
  23. package/src/core/ports/init-apply.ts +0 -15
  24. package/src/core/ports/init-status.ts +0 -4
  25. package/src/core/ports/init-toolkit-status.ts +0 -4
  26. package/src/core/ports/pr-create.ts +0 -30
  27. package/src/core/ports/present-ascii.ts +0 -10
  28. package/src/core/ports/present-flow.ts +0 -10
  29. package/src/core/ports/vcs-config.ts +0 -14
  30. package/src/core/ports/vcs-merged-style.ts +0 -5
  31. package/src/core/ports/vcs-verify-token.ts +0 -4
  32. package/src/core/ports/youtrack-api.ts +0 -19
  33. package/src/core/ports/youtrack-config.ts +0 -21
  34. package/src/core/ports/youtrack-greeting.ts +0 -10
  35. package/src/core/ports/youtrack-parse-duration.ts +0 -14
  36. package/src/core/ports/youtrack-token-create-url.ts +0 -4
  37. package/src/core/ports/youtrack-verify-token.ts +0 -12
  38. package/src/core/ports/youtrack-work-date-ms.ts +0 -10
  39. package/src/core/present.ts +0 -109
  40. package/src/core/repo-tool.ts +0 -12
  41. package/src/core/repo-tools.ts +0 -22
  42. package/src/core/sync-runtime.ts +0 -375
  43. package/src/core/verify-parse.ts +0 -29
  44. package/src/core/verify-project.ts +0 -181
package/src/core/init.ts CHANGED
@@ -2,12 +2,9 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { configDir, isConfigObject } from "./config";
5
- import { PLUGIN_ROOT } from "./scripts";
6
5
  import { writeFileExclusive } from "./safe-write";
7
- import { resolveWorkspace, workspacesPath } from "./workspaces";
8
6
  import { applyWorkspaceBranchPolicy } from "./setup";
9
- import { vcsConfig, vcsVerifyToken } from "./vcs-config";
10
- import { youTrackTokenCreateUrl, youTrackVerifyToken } from "./youtrack";
7
+ import { youTrackTokenCreateUrl } from "./youtrack";
11
8
 
12
9
  const TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
13
10
 
@@ -22,12 +19,6 @@ const readJson = (p: string): Record<string, any> | null => {
22
19
  }
23
20
  };
24
21
 
25
- const isPlaceholder = (text: string): boolean =>
26
- text === TOKEN_PLACEHOLDER || text.startsWith(TOKEN_PLACEHOLDER);
27
-
28
- const modeOk = (p: string): boolean =>
29
- process.platform === "win32" || (fs.statSync(p).mode & 0o777) === 0o600;
30
-
31
22
  /** Provider for a fresh vcs.json: explicit env, else origin remote, else null
32
23
  * (no silent default — a tool for everyone must not assume a provider). */
33
24
  export const resolveInitProvider = (cwd?: string): string | null => {
@@ -50,234 +41,6 @@ export const resolveInitProvider = (cwd?: string): string | null => {
50
41
  return null;
51
42
  };
52
43
 
53
- const resolvePath = (p: string): string => {
54
- try {
55
- return fs.realpathSync(p);
56
- } catch {
57
- return path.resolve(p);
58
- }
59
- };
60
-
61
- /** Port of scripts/init/status.sh — filesystem init state. */
62
- export function initStatusData(configDirPath = configDir()): Record<string, any> {
63
- const ytJson = path.join(configDirPath, "youtrack.json");
64
- const vcsJson = path.join(configDirPath, "vcs.json");
65
- const items: Array<Record<string, any>> = [];
66
-
67
- let youtrackConfig: Record<string, any> | null = null;
68
- let youtrackTokenCreate: Record<string, any> | null = null;
69
- const ytParsed = readJson(ytJson);
70
- if (ytParsed) {
71
- const base = String(ytParsed.baseUrl ?? "").replace(/\/+$/, "");
72
- const meeting = String(ytParsed.meetingIssue ?? "");
73
- const tokenFile = String(ytParsed.tokenFile ?? "");
74
- const meetingIssues = [];
75
- const rawMeetings = (ytParsed.meetingIssues ?? {}) as Record<string, any>;
76
- if (rawMeetings && typeof rawMeetings === "object") {
77
- for (const [key, item] of Object.entries(rawMeetings)) {
78
- const iss = String(item?.issue ?? "");
79
- meetingIssues.push({
80
- key,
81
- issue: iss,
82
- label: item?.label ?? iss,
83
- workItemText: item?.workItemText ?? "Reuniones",
84
- url: item?.url ?? (base && iss ? `${base}/issue/${iss}` : null),
85
- });
86
- }
87
- }
88
- const expanded = tokenFile ? path.resolve(tokenFile) : null;
89
- const resolvedTokenFile =
90
- expanded && fs.existsSync(expanded)
91
- ? resolvePath(expanded)
92
- : expanded
93
- ? path.isAbsolute(expanded)
94
- ? expanded
95
- : path.resolve(configDirPath, expanded)
96
- : null;
97
- youtrackConfig = {
98
- config_edit_path: resolvePath(ytJson),
99
- baseUrl: base,
100
- meetingIssue: meeting,
101
- meetingIssues,
102
- meetingIssueUrl: base && meeting ? `${base}/issue/${meeting}` : null,
103
- defaultMention: ytParsed.defaultMention,
104
- timezone: ytParsed.timezone,
105
- locale: ytParsed.locale,
106
- tokenFile: resolvedTokenFile,
107
- tokenDefaults: ytParsed.tokenDefaults,
108
- timeLogging: {
109
- meetings: {
110
- options: meetingIssues,
111
- skill: "/wk-meetings",
112
- logsTime: true,
113
- postsComment: false,
114
- },
115
- taskWork: {
116
- issueSource: "active spec/plan **YouTrack:** field or --issue",
117
- skill: "/wk-issue-update",
118
- logsTime: true,
119
- postsComment: true,
120
- },
121
- },
122
- };
123
- youtrackTokenCreate = youTrackTokenCreateUrl().data;
124
- if (youtrackTokenCreate && !(youtrackConfig as Record<string, any>).error) {
125
- youtrackConfig.tokenCreate = youtrackTokenCreate;
126
- }
127
- } else if (fs.existsSync(ytJson)) {
128
- // Distinguish parse failure (legacy message, path in config_edit_path) from a
129
- // parseable non-object, which gets the shared shape diagnostic with the path.
130
- let parseFailed = false;
131
- try {
132
- JSON.parse(fs.readFileSync(ytJson, "utf8"));
133
- } catch {
134
- parseFailed = true;
135
- }
136
- youtrackConfig = {
137
- config_edit_path: resolvePath(ytJson),
138
- error: parseFailed ? "invalid youtrack.json" : `${resolvePath(ytJson)} is not a JSON object`,
139
- };
140
- }
141
-
142
- items.push({
143
- id: "youtrack_json",
144
- label: "YouTrack config",
145
- ok: fs.existsSync(ytJson) && youtrackConfig !== null && !("error" in youtrackConfig),
146
- path: fs.existsSync(ytJson) ? resolvePath(ytJson) : path.resolve(ytJson),
147
- config_edit_path: resolvePath(ytJson),
148
- fix: "workit_init_apply action=youtrack_scaffold",
149
- });
150
-
151
- const ytTokenPath = youtrackConfig?.tokenFile ?? path.join(configDirPath, "youtrack.token");
152
- const tokenText = fs.existsSync(ytTokenPath) ? fs.readFileSync(ytTokenPath, "utf8").trim() : "";
153
- const placeholder = fs.existsSync(ytTokenPath) && isPlaceholder(tokenText);
154
- const tokenOk =
155
- fs.existsSync(ytTokenPath) &&
156
- modeOk(ytTokenPath) &&
157
- Boolean(tokenText) &&
158
- !isPlaceholder(tokenText);
159
-
160
- const youtrackTokenItem: Record<string, any> = {
161
- id: "youtrack_token",
162
- label: "YouTrack API token (mode 600, not placeholder)",
163
- ok: tokenOk,
164
- path: fs.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path.resolve(ytTokenPath),
165
- token_edit_path: fs.existsSync(ytTokenPath)
166
- ? resolvePath(ytTokenPath)
167
- : path.resolve(ytTokenPath),
168
- placeholder,
169
- fix: `Open ${resolvePath(ytTokenPath)} — replace ${TOKEN_PLACEHOLDER} with your permanent token, save, then /wk-status`,
170
- };
171
- if (youtrackTokenCreate) {
172
- if (youtrackTokenCreate.createUrl)
173
- youtrackTokenItem.token_create_url = youtrackTokenCreate.createUrl;
174
- if (youtrackTokenCreate.docsUrl)
175
- youtrackTokenItem.token_create_docs_url = youtrackTokenCreate.docsUrl;
176
- if (youtrackTokenCreate.scopes) youtrackTokenItem.token_scopes = youtrackTokenCreate.scopes;
177
- if (youtrackTokenCreate.tokenName) youtrackTokenItem.token_name = youtrackTokenCreate.tokenName;
178
- if (youtrackTokenCreate.steps) youtrackTokenItem.token_create_steps = youtrackTokenCreate.steps;
179
- youtrackTokenItem.token_prefill_supported = youtrackTokenCreate.prefillSupported ?? false;
180
- }
181
- items.push(youtrackTokenItem);
182
-
183
- let vcsCfg: Record<string, any> | null = null;
184
- const vcsParsed = readJson(vcsJson);
185
- if (vcsParsed) {
186
- const rawProvider = vcsParsed.provider;
187
- const effective = vcsConfig("resolve", process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd());
188
- const provider =
189
- effective?.ok && typeof effective.provider === "string"
190
- ? effective.provider
191
- : typeof rawProvider === "string" && rawProvider.trim()
192
- ? rawProvider.toLowerCase()
193
- : null;
194
- vcsCfg = {
195
- config_edit_path: resolvePath(vcsJson),
196
- provider,
197
- defaultTargetBranch: effective.ok
198
- ? effective.defaultTargetBranch
199
- : (vcsParsed.defaultTargetBranch ?? "develop"),
200
- pr: vcsParsed.pr ?? {},
201
- gitlab: vcsParsed.gitlab,
202
- github: vcsParsed.github,
203
- skill: "/wk-pr",
204
- switchHint: 'Set "provider" to "gitlab" or "github" in vcs.json',
205
- };
206
- } else if (fs.existsSync(vcsJson)) {
207
- vcsCfg = { config_edit_path: resolvePath(vcsJson), error: "invalid vcs.json" };
208
- }
209
-
210
- items.push({
211
- id: "vcs_json",
212
- label: "VCS config (GitLab / GitHub)",
213
- ok: fs.existsSync(vcsJson) && vcsCfg !== null && !("error" in vcsCfg),
214
- path: fs.existsSync(vcsJson) ? resolvePath(vcsJson) : path.resolve(vcsJson),
215
- config_edit_path: resolvePath(vcsJson),
216
- fix: "workit_init_apply action=vcs_scaffold",
217
- });
218
-
219
- return {
220
- config_dir: configDirPath,
221
- plugin_root: PLUGIN_ROOT,
222
- token_placeholder: TOKEN_PLACEHOLDER,
223
- youtrack_config: youtrackConfig,
224
- vcs_config: vcsCfg,
225
- items,
226
- ready: false,
227
- };
228
- }
229
-
230
- /** Port of scripts/init/toolkit-status.sh — filesystem + API health check. */
231
- export async function toolkitStatusData(configDirPath = configDir()): Promise<Record<string, any>> {
232
- const status = initStatusData(configDirPath);
233
- const tokenItem = status.items.find((i: Record<string, any>) => i.id === "youtrack_token") ?? {};
234
- const placeholder = Boolean(tokenItem.placeholder);
235
-
236
- const vcsCfg = (status.vcs_config ?? {}) as Record<string, any>;
237
- const provider =
238
- typeof vcsCfg.provider === "string" && vcsCfg.provider.trim()
239
- ? vcsCfg.provider.toLowerCase()
240
- : null;
241
- const vcsJsonOk = Boolean(status.items.find((i: Record<string, any>) => i.id === "vcs_json")?.ok);
242
-
243
- const verify = placeholder
244
- ? { ok: false, error: "token still placeholder YOUR_TOKEN_HERE" }
245
- : await youTrackVerifyToken();
246
- const vcsVerify =
247
- provider === null
248
- ? {
249
- ok: false,
250
- error: "vcs provider unconfigured — set provider to gitlab or github in vcs.json",
251
- }
252
- : await vcsVerifyToken();
253
- const youTrackHealth = ("data" in verify ? verify.data : verify) as Record<string, any>;
254
-
255
- status.youtrack_verify = verify;
256
- status.youtrack_ok = placeholder ? false : Boolean(youTrackHealth.ok);
257
- status.vcs_verify = vcsVerify;
258
- status.vcs_ok = vcsJsonOk && Boolean((vcsVerify as Record<string, any>).ok);
259
-
260
- const fsReady = status.items.every((i: Record<string, any>) => i.required === false || i.ok);
261
- status.ready = fsReady && Boolean(status.youtrack_ok) && (!vcsJsonOk || Boolean(status.vcs_ok));
262
-
263
- if (placeholder) {
264
- const tokenPath = tokenItem.token_edit_path ?? tokenItem.path ?? "";
265
- const createUrl = tokenItem.token_create_url ?? status.youtrack_config?.tokenCreate?.createUrl;
266
- status.token_edit_path = tokenPath;
267
- if (createUrl) status.token_create_url = createUrl;
268
- status.next_step = createUrl
269
- ? "Open the YouTrack create-token URL, New token (name workit, scope YouTrack), paste into the token file, save, then run /wk-status"
270
- : "Open the YouTrack token file, replace YOUR_TOKEN_HERE, save, then run /wk-status";
271
- } else if (!status.youtrack_ok) {
272
- status.next_step = "Fix YouTrack token or re-run /wk-init";
273
- } else if (vcsJsonOk && !status.vcs_ok) {
274
- status.next_step = `Run ${provider === "gitlab" ? "glab" : "gh"} auth login/switch, then /wk-status`;
275
- } else if (status.ready) {
276
- status.next_step = "All checks passed";
277
- }
278
- return status;
279
- }
280
-
281
44
  // Port of scripts/init/apply.sh write_youtrack_json — env overrides honored.
282
45
  const youtrackJsonContent = (dir: string): Record<string, any> => ({
283
46
  baseUrl: process.env.WORKFLOW_YT_BASE_URL ?? "https://enghouseamg.youtrack.cloud",
@@ -467,24 +230,6 @@ export function initApplyData(
467
230
  }
468
231
  }
469
232
 
470
- export function initStatus(): Record<string, any> {
471
- const data = initStatusData();
472
- const workspaces: Record<string, any> = { path: workspacesPath(), resolved: null };
473
- try {
474
- workspaces.resolved = resolveWorkspace(process.cwd());
475
- } catch (error) {
476
- workspaces.error = error instanceof Error ? error.message : String(error);
477
- }
478
- return {
479
- ...data,
480
- workspaces,
481
- };
482
- }
483
-
484
- export async function toolkitStatus(): Promise<Record<string, any>> {
485
- return toolkitStatusData();
486
- }
487
-
488
233
  export function initApply({
489
234
  action,
490
235
  confirmed,
@@ -38,51 +38,6 @@ export type ResolverPreferences = {
38
38
  thorough?: boolean;
39
39
  };
40
40
 
41
- /** Size facts for deterministic spec triage (op1 with override). */
42
- export type TriageFacts = {
43
- /** New or changed observable behavior. */
44
- newOrChangedBehavior: boolean;
45
- /** Open ambiguity needing user resolution. */
46
- ambiguityOpen: boolean;
47
- /** Cross-package/host contract, auth/data/security surface, or irreversible migration. */
48
- crossContract: boolean;
49
- /** Subsystems touched. */
50
- subsystems: number;
51
- /** Packages touched (a 2-package change is Large even in one subsystem). */
52
- packagesTouched: number;
53
- /** Estimated implementation steps. */
54
- steps: number;
55
- };
56
-
57
- export type TriageTier = "spec-plan" | "plan-only" | "neither";
58
-
59
- /** Large → spec + full plan; Medium → compact plan-only; Small → neither.
60
- * Step count alone never escalates: a long but known, single-subsystem run
61
- * stays plan-only. The lead may re-tier with a reason recorded in progress
62
- * (override). */
63
- export const triageTier = (facts: TriageFacts): TriageTier => {
64
- if (
65
- facts.newOrChangedBehavior ||
66
- facts.ambiguityOpen ||
67
- facts.crossContract ||
68
- facts.subsystems >= 3 ||
69
- facts.packagesTouched >= 2
70
- )
71
- return "spec-plan";
72
- if (facts.steps >= 2) return "plan-only";
73
- return "neither";
74
- };
75
-
76
- /** Tier → assessor signal values (durableAgreementNeeded, coordinationPlanNeeded). */
77
- export const triageSignals = (
78
- tier: TriageTier,
79
- ): { durableAgreementNeeded: boolean; coordinationPlanNeeded: boolean } =>
80
- tier === "spec-plan"
81
- ? { durableAgreementNeeded: true, coordinationPlanNeeded: true }
82
- : tier === "plan-only"
83
- ? { durableAgreementNeeded: false, coordinationPlanNeeded: true }
84
- : { durableAgreementNeeded: false, coordinationPlanNeeded: false };
85
-
86
41
  export type ResolverInput = {
87
42
  intent: Intent;
88
43
  assessment: Assessment;
@@ -31,10 +31,9 @@ export function isWorkitPlugin(value: unknown): boolean {
31
31
  const s = String(value).replaceAll("\\", "/");
32
32
  const url = s.startsWith("file://") || s.startsWith("git+file://");
33
33
  const pkgPath =
34
- s.includes("/packages/workit-opencode/") ||
35
- s.includes("/packages/workit-cursor/") ||
36
- s.includes("/node_modules/@brainervirus/workit-opencode/") ||
37
- s.includes("/node_modules/@brainervirus/workit-cursor/");
34
+ /\/(?:packages\/workit-(?:opencode|cursor)|node_modules\/@brainervirus\/workit-(?:opencode|cursor))(?:\/|$)/.test(
35
+ s,
36
+ );
38
37
  return (
39
38
  named(s, "workflow-toolkit") ||
40
39
  named(s, "workflow-toolkit-opencode") ||
@@ -7,7 +7,7 @@ import { gitRevisionParts, resolveGitRevision } from "../core";
7
7
  // Ports of scripts/_shared/common.sh + the four maintained context generators
8
8
  // (pr-ready-context.sh, changelog-context.sh, docs-refresh-context.sh,
9
9
  // release-notes-context.sh). Each generator returns the same stdout text shape
10
- // the shell produced (## sections parsed by parse-sections.ts).
10
+ // the shell produced (## sections).
11
11
 
12
12
  export type ContextResult = { stdout: string; stderr: string; exitCode: number; cwd: string };
13
13
 
package/src/core/rules.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { configDir } from "./config";
4
4
 
@@ -41,71 +41,11 @@ export const parseRule = (markdown: string): CanonicalRule | { error: string } =
41
41
  return { name, description, platforms, body: fm[2].trim() + "\n" };
42
42
  };
43
43
 
44
- export const listRules = (): { name: string; platforms: string[]; source: "config" | "repo" }[] => {
45
- const result: { name: string; platforms: string[]; source: "config" | "repo" }[] = [];
46
- const dir = rulesDir();
47
- if (existsSync(dir)) {
48
- for (const entry of readdirSync(dir)) {
49
- const file = path.join(dir, entry, "rule.md");
50
- if (!existsSync(file)) continue;
51
- const parsed = parseRule(readFileSync(file, "utf8"));
52
- if ("error" in parsed) continue;
53
- result.push({ name: parsed.name, platforms: parsed.platforms, source: "config" });
54
- }
55
- }
56
- return result;
57
- };
58
-
59
- export const readRule = (
60
- name: string,
61
- ): { source: "config" | "repo" | "missing"; rule: CanonicalRule } | { error: string } => {
62
- const file = path.join(rulesDir(), name, "rule.md");
63
- if (existsSync(file)) {
64
- const parsed = parseRule(readFileSync(file, "utf8"));
65
- if ("error" in parsed) return { error: parsed.error };
66
- return { source: "config", rule: parsed };
67
- }
68
- return { source: "missing", rule: { name, description: "", platforms: [], body: "" } };
69
- };
70
-
71
44
  const RULE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
72
45
 
73
- export const writeRule = (
74
- rule: CanonicalRule,
75
- confirmed: boolean,
76
- ): { ok: true; path: string } | { ok: false; error: string } => {
77
- if (!confirmed) return { ok: false, error: "confirmed: true required" };
78
- if (!RULE_NAME_RE.test(rule.name))
79
- return { ok: false, error: `invalid rule name: ${JSON.stringify(rule.name)}` };
80
- const dir = path.join(rulesDir(), rule.name);
81
- mkdirSync(dir, { recursive: true });
82
- const file = path.join(dir, "rule.md");
83
- const md = `---\nname: ${rule.name}\ndescription: ${rule.description}\nplatforms: [${rule.platforms.join(", ")}]\n---\n${rule.body}`;
84
- writeFileSync(file, md, "utf8");
85
- return { ok: true, path: file };
86
- };
87
-
88
46
  export const compileRuleCursor = (rule: CanonicalRule): string =>
89
47
  `---\ndescription: ${rule.description}\nalwaysApply: true\n---\n\n${rule.body}`;
90
48
 
91
- export const compileRuleOpenCode = (rule: CanonicalRule): string =>
92
- `## ${rule.name}\n\n${rule.body}`;
93
-
94
- export const compiledOpenCodeSections = (): string => {
95
- const sections: string[] = [];
96
- const dir = rulesDir();
97
- if (existsSync(dir)) {
98
- for (const entry of readdirSync(dir)) {
99
- const file = path.join(dir, entry, "rule.md");
100
- if (!existsSync(file)) continue;
101
- const parsed = parseRule(readFileSync(file, "utf8"));
102
- if ("error" in parsed || !parsed.platforms.includes("opencode")) continue;
103
- sections.push(compileRuleOpenCode(parsed));
104
- }
105
- }
106
- return sections.join("\n\n");
107
- };
108
-
109
49
  export const writeCompiledCursorRules = (targetDir: string): string[] => {
110
50
  const written: string[] = [];
111
51
  const dir = rulesDir();