@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/setup.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  readConfigFromDir,
23
23
  resolveConfigDir,
24
24
  type BranchPreset,
25
+ type ToolkitConfig,
25
26
  } from "./config";
26
27
  import { detectBranchPolicy } from "./branch-policy";
27
28
  import { readSetupState, type SetupState } from "./setup-state";
@@ -38,13 +39,26 @@ import { planHygieneFiles } from "./hygiene";
38
39
  import { packageRoot } from "./package-root";
39
40
  import {
40
41
  cursorMcpServerEntry,
42
+ CURSOR_RUNTIME_PACKAGE,
43
+ isWorkitPlugin,
41
44
  mergeCursorMcp,
45
+ mergeOpenCodePlugins,
42
46
  mergeCursorSettings,
43
47
  mergeOpenCodeConfig,
44
48
  OPENCODE_NPM_PIN,
45
49
  } from "./registration";
46
50
  import { runDoctor, type DoctorReport } from "./doctor";
47
51
  import { writeFileExclusive } from "./safe-write";
52
+ import {
53
+ isCodexWorkitInstalled,
54
+ isPiWorkitInstalled,
55
+ planHostInstall,
56
+ runHostInstall,
57
+ type HostCommandRunner,
58
+ type HostId,
59
+ type HostInstallCommand,
60
+ runHostCommand,
61
+ } from "./host-install";
48
62
 
49
63
  // Setup preview + apply (WZ-04-WZ-09, WZ-13-WZ-15; CA-12, CA-14, CA-22, CA-23).
50
64
  // buildSetupPreview is a pure reader: it classifies the current setup state and
@@ -82,6 +96,8 @@ export type SetupPreviewInput = {
82
96
  * configured tokenFile, the replacement is planned as its own distinct
83
97
  * reviewed mutation (AR-10). */
84
98
  tokenPaths?: { youtrack?: string; gitlab?: string; github?: string };
99
+ /** Optional global commit policy used as the base for repository overrides. */
100
+ commitPolicy?: ToolkitConfig["commitPolicy"];
85
101
  };
86
102
 
87
103
  export type SetupMutation =
@@ -98,6 +114,7 @@ export type SetupMutation =
98
114
  // during preview so the reviewed mutation set IS the Apply write set.
99
115
  | { type: "register-platform"; platform: Platform; path: string }
100
116
  | { type: "install-adapter"; platform: "cursor"; path: string }
117
+ | { type: "install-host"; platform: HostId; path: string; commands: HostInstallCommand[] }
101
118
  // AR-10: changing a configured tokenFile path is never hidden inside a
102
119
  // generic merge — it is its own reviewed mutation.
103
120
  | { type: "set-token-path"; path: string; key: string; value: string };
@@ -249,6 +266,7 @@ export function buildSetupPreview(
249
266
  locale: values.locale,
250
267
  localeOptions: current.localeOptions,
251
268
  timezone: values.timezone,
269
+ ...(values.commitPolicy === undefined ? {} : { commitPolicy: values.commitPolicy }),
252
270
  branchPolicy: mergePreset(
253
271
  values.branchPreset,
254
272
  {
@@ -344,9 +362,56 @@ export function buildSetupPreview(
344
362
  if (platform === "opencode") {
345
363
  mutations.push({ type: "register-platform", platform, path: paths.opencodeConfig });
346
364
  } else if (platform === "cursor") {
365
+ const cursorRoot = adapterRoot("cursor", {
366
+ ...paths,
367
+ dev: opts.dev ?? env.WORKFLOW_TOOLKIT_DEV ?? null,
368
+ });
369
+ if (cursorRoot === null) {
370
+ const commands = planHostInstall("cursor", {
371
+ home: paths.home,
372
+ cwd: paths.cwd,
373
+ env,
374
+ packageVersion: "latest",
375
+ targetDir: paths.cursorPluginDir,
376
+ });
377
+ mutations.push({
378
+ type: "install-host",
379
+ platform: "cursor",
380
+ path: path.join(
381
+ paths.home,
382
+ ".cursor",
383
+ "plugins",
384
+ "local",
385
+ ".workit-package-bootstrap",
386
+ ),
387
+ commands,
388
+ });
389
+ }
347
390
  mutations.push({ type: "install-adapter", platform, path: paths.cursorPluginDir });
348
391
  mutations.push({ type: "register-platform", platform, path: paths.cursorSettings });
349
392
  mutations.push({ type: "register-platform", platform, path: paths.cursorMcp });
393
+ } else if (platform === "codex" || platform === "pi") {
394
+ const commands = planHostInstall(platform, { home: paths.home, cwd: paths.cwd, env });
395
+ if (commands.length > 0) {
396
+ mutations.push({
397
+ type: "install-host",
398
+ platform,
399
+ path:
400
+ platform === "codex"
401
+ ? path.join(
402
+ env.CODEX_HOME ?? path.join(paths.home, ".codex"),
403
+ "plugins",
404
+ "cache",
405
+ "workflow-toolkit",
406
+ "workit",
407
+ )
408
+ : path.join(
409
+ env.PI_CODING_AGENT_DIR ?? path.join(paths.home, ".pi", "agent"),
410
+ "settings.json",
411
+ ),
412
+ commands,
413
+ });
414
+ }
350
415
  }
351
416
  }
352
417
  }
@@ -391,7 +456,7 @@ export type Platform = "opencode" | "cursor";
391
456
  export type SetupResultStatus = "Installed" | "Configured" | "Skipped" | "Failed";
392
457
 
393
458
  export type SetupResultEntry = {
394
- platform: Platform | "core";
459
+ platform: HostId | "core";
395
460
  file: string;
396
461
  status: SetupResultStatus;
397
462
  detail?: string;
@@ -422,6 +487,10 @@ export type ApplySetupOptions = {
422
487
  cursorMcp?: string;
423
488
  cursorPluginDir?: string;
424
489
  stateDir?: string;
490
+ /** Injectable synchronous host runner; defaults to a bounded no-shell spawn. */
491
+ runHostCommand?: HostCommandRunner;
492
+ /** Prefer staged npm adapter assets, used by the explicit upgrade flow. */
493
+ preferInstalledAdapter?: boolean;
425
494
  };
426
495
 
427
496
  type ResolvedApply = {
@@ -528,7 +597,7 @@ const readFileSafe = (p: string): string | null => {
528
597
  // adapter resolution (AR-09).
529
598
  type CoreMutation = Exclude<
530
599
  SetupMutation,
531
- { type: "register-platform" } | { type: "install-adapter" }
600
+ { type: "register-platform" } | { type: "install-adapter" } | { type: "install-host" }
532
601
  >;
533
602
 
534
603
  const workspaceEntriesAlreadyMatch = (
@@ -735,8 +804,23 @@ const isAdapter = (root: string, platform: Platform): boolean => {
735
804
  }
736
805
  };
737
806
 
738
- function adapterRoot(platform: Platform, res: ResolvedApply): string | null {
807
+ function adapterRoot(
808
+ platform: Platform,
809
+ res: ResolvedApply,
810
+ preferInstalledAdapter = false,
811
+ ): string | null {
739
812
  const candidates: string[] = [];
813
+ const stagedCursor = path.join(
814
+ res.home,
815
+ ".cursor",
816
+ "plugins",
817
+ "local",
818
+ ".workit-package-bootstrap",
819
+ "node_modules",
820
+ "@brainervirus",
821
+ "workit-cursor",
822
+ );
823
+ if (platform === "cursor" && preferInstalledAdapter) candidates.push(stagedCursor);
740
824
  if (res.dev) {
741
825
  // A dev checkout is the complete source of truth for adapter packages: when
742
826
  // WORKFLOW_TOOLKIT_DEV is set, a package missing from it is a real failure,
@@ -757,6 +841,7 @@ function adapterRoot(platform: Platform, res: ResolvedApply): string | null {
757
841
  candidates.push(
758
842
  path.join(res.home, ".local", "share", "workit", "packages", `workit-${platform}`),
759
843
  );
844
+ if (platform === "cursor") candidates.push(stagedCursor);
760
845
  for (const candidate of candidates) {
761
846
  if (isAdapter(candidate, platform)) return candidate;
762
847
  }
@@ -787,8 +872,8 @@ const opencodePin = (root: string, res: ResolvedApply): string | null =>
787
872
  resolveOpenCodePin(root, { dev: res.dev });
788
873
 
789
874
  function applyOpenCode(root: string, res: ResolvedApply): SetupResultEntry {
790
- const pin = opencodePin(root, res);
791
- if (!pin) {
875
+ const resolvedPin = opencodePin(root, res);
876
+ if (!resolvedPin) {
792
877
  return {
793
878
  platform: "opencode",
794
879
  file: res.opencodeConfig,
@@ -807,7 +892,33 @@ function applyOpenCode(root: string, res: ResolvedApply): SetupResultEntry {
807
892
  detail: `cannot merge into malformed config: ${existing.error} — repair or remove the file`,
808
893
  };
809
894
  }
810
- const merged = mergeOpenCodeConfig(existing.kind === "record" ? existing.value : {}, pin);
895
+ const configuredPlugins =
896
+ existing.kind === "record"
897
+ ? Array.isArray(existing.value.plugins)
898
+ ? existing.value.plugins
899
+ : typeof existing.value.plugins === "string"
900
+ ? [existing.value.plugins]
901
+ : typeof existing.value.plugin === "string"
902
+ ? [existing.value.plugin]
903
+ : Array.isArray(existing.value.plugin)
904
+ ? existing.value.plugin
905
+ : []
906
+ : [];
907
+ const configuredPin = configuredPlugins.find(
908
+ (entry): entry is string => typeof entry === "string" && isWorkitPlugin(entry),
909
+ );
910
+ const pin = configuredPin ?? resolvedPin;
911
+ const base = existing.kind === "record" ? existing.value : {};
912
+ const merged =
913
+ existing.kind === "record" && Object.hasOwn(existing.value, "plugins")
914
+ ? (() => {
915
+ const plugins = mergeOpenCodePlugins(existing.value.plugins, pin);
916
+ return {
917
+ config: { ...existing.value, plugins: plugins.config },
918
+ changed: plugins.changed.length > 0 ? ["plugins"] : [],
919
+ };
920
+ })()
921
+ : mergeOpenCodeConfig(base, pin);
811
922
  if (merged.changed.length === 0) {
812
923
  return {
813
924
  platform: "opencode",
@@ -996,11 +1107,21 @@ function applyCursorMcp(root: string, res: ResolvedApply): SetupResultEntry {
996
1107
  detail: `cannot merge into malformed config: ${mcpExisting.error} — repair or remove the file`,
997
1108
  };
998
1109
  }
999
- const mcp = mergeCursorMcp(
1000
- mcpExisting.kind === "record" ? mcpExisting.value : {},
1001
- "workit",
1002
- cursorMcpServerEntry(res.cursorPluginDir),
1110
+ const mcpBase = mcpExisting.kind === "record" ? mcpExisting.value : {};
1111
+ const server = cursorMcpServerEntry(res.cursorPluginDir);
1112
+ const servers = isRecord(mcpBase.mcpServers) ? mcpBase.mcpServers : {};
1113
+ const current = isRecord(servers.workit) ? servers.workit : {};
1114
+ const previousArgs = Array.isArray(current.args) ? current.args.map(String) : [];
1115
+ const previousPackage = previousArgs.find(
1116
+ (arg) =>
1117
+ arg.startsWith("--package=@brainervirus/workit-cursor@") &&
1118
+ arg !== `--package=${CURSOR_RUNTIME_PACKAGE}`,
1003
1119
  );
1120
+ if (previousPackage) {
1121
+ const index = server.args.findIndex((arg) => arg.startsWith("--package="));
1122
+ if (index >= 0) server.args[index] = previousPackage;
1123
+ }
1124
+ const mcp = mergeCursorMcp(mcpBase, "workit", server);
1004
1125
  if (mcp.changed.length === 0) {
1005
1126
  return {
1006
1127
  platform: "cursor",
@@ -1117,6 +1238,77 @@ export function applySetupPreview(
1117
1238
  // legacy identity or its registration.
1118
1239
  let cursorCopyOk = false;
1119
1240
  for (const mutation of preview.mutations) {
1241
+ if (mutation.type === "install-host") {
1242
+ const expectedPath =
1243
+ mutation.platform === "cursor"
1244
+ ? path.join(res.home, ".cursor", "plugins", "local", ".workit-package-bootstrap")
1245
+ : mutation.platform === "codex"
1246
+ ? path.join(
1247
+ res.env.CODEX_HOME ?? path.join(res.home, ".codex"),
1248
+ "plugins",
1249
+ "cache",
1250
+ "workflow-toolkit",
1251
+ "workit",
1252
+ )
1253
+ : mutation.platform === "pi"
1254
+ ? path.join(
1255
+ res.env.PI_CODING_AGENT_DIR ?? path.join(res.home, ".pi", "agent"),
1256
+ "settings.json",
1257
+ )
1258
+ : "";
1259
+ if (mutation.path !== expectedPath) {
1260
+ entries.push({
1261
+ platform: mutation.platform,
1262
+ file: mutation.path,
1263
+ status: "Failed",
1264
+ detail: `preview/apply path mismatch: apply resolved ${expectedPath}, the previewed host install was ${mutation.path} — rebuild the preview with the same options`,
1265
+ });
1266
+ continue;
1267
+ }
1268
+ const run =
1269
+ options.runHostCommand ??
1270
+ ((step: HostInstallCommand) => runHostCommand(step, { home: res.home, env: res.env }));
1271
+ const installed = runHostInstall(mutation.commands, run);
1272
+ for (const { command, result } of installed.results) {
1273
+ entries.push({
1274
+ platform: mutation.platform,
1275
+ file: command.command,
1276
+ status: result.exitCode === 0 ? "Installed" : "Failed",
1277
+ detail:
1278
+ result.exitCode === 0
1279
+ ? `${command.purpose} (${command.args.join(" ")})`
1280
+ : `${command.purpose} failed (${command.args.join(" ")}): ${result.stderr || `exit ${result.exitCode}`}`,
1281
+ });
1282
+ }
1283
+ if (!installed.ok && installed.results.length === 0) {
1284
+ entries.push({
1285
+ platform: mutation.platform,
1286
+ file: mutation.platform,
1287
+ status: "Failed",
1288
+ detail: "host installation did not run",
1289
+ });
1290
+ }
1291
+ if (!installed.ok) continue;
1292
+ if (mutation.platform === "cursor" && installed.packageRoot) {
1293
+ rootFor.set("cursor", installed.packageRoot);
1294
+ }
1295
+ const verified =
1296
+ mutation.platform === "codex"
1297
+ ? isCodexWorkitInstalled(res.home, res.env)
1298
+ : mutation.platform === "pi"
1299
+ ? isPiWorkitInstalled(res.home, res.env)
1300
+ : true;
1301
+ if (!verified) {
1302
+ entries.push({
1303
+ platform: mutation.platform,
1304
+ file: mutation.platform,
1305
+ status: "Failed",
1306
+ detail:
1307
+ "the host command succeeded, but Workit registration was not found; inspect the host's native package list and retry",
1308
+ });
1309
+ }
1310
+ continue;
1311
+ }
1120
1312
  if (mutation.type !== "register-platform" && mutation.type !== "install-adapter") {
1121
1313
  entries.push(applyMutation(mutation));
1122
1314
  continue;
@@ -1141,7 +1333,7 @@ export function applySetupPreview(
1141
1333
  }
1142
1334
  let root = rootFor.get(platform);
1143
1335
  if (root === undefined) {
1144
- root = adapterRoot(platform, res);
1336
+ root = adapterRoot(platform, res, options.preferInstalledAdapter);
1145
1337
  rootFor.set(platform, root);
1146
1338
  }
1147
1339
  if (root === null) {
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { configDir } from "./config";
4
4
  import { assetRoot } from "./package-root";
@@ -20,28 +20,3 @@ export const readTemplate = (
20
20
  content: readFileSync(path.join(repoRoot, "templates", `${name}.md`), "utf8"),
21
21
  };
22
22
  };
23
-
24
- export const writeTemplate = (
25
- name: TemplateName,
26
- content: string,
27
- confirmed: boolean,
28
- ): { ok: true; path: string } | { ok: false; error: string } => {
29
- if (!confirmed) return { ok: false, error: "confirmed: true required" };
30
- const file = templatePath(name);
31
- mkdirSync(path.dirname(file), { recursive: true });
32
- writeFileSync(file, content, "utf8");
33
- return { ok: true, path: file };
34
- };
35
-
36
- export const listTemplates = (): {
37
- name: TemplateName;
38
- source: "config" | "repo" | "missing";
39
- path: string;
40
- }[] =>
41
- (["issue-update", "greeting", "headers"] as TemplateName[]).map((name) => {
42
- const cfg = templatePath(name);
43
- const repoFile = path.join(repoRoot, "templates", `${name}.md`);
44
- if (existsSync(cfg)) return { name, source: "config", path: cfg };
45
- if (existsSync(repoFile)) return { name, source: "repo", path: repoFile };
46
- return { name, source: "missing", path: cfg };
47
- });
@@ -191,10 +191,8 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
191
191
  (hasWorkspacePolicy ? policyDefault : (cfg.defaultTargetBranch ?? policyDefault)) ??
192
192
  "develop",
193
193
  );
194
- const linkIssues =
195
- provider === "gitlab" && typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
196
- const youtrackBaseUrl =
197
- provider === "gitlab" && typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
194
+ const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
195
+ const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
198
196
  // github issues path only when BOTH providers are github (mirrors WorkspaceConfig.issues).
199
197
  let issuesProvider: string | null = null;
200
198
  let linkOnPr: boolean | null = null;
@@ -305,10 +303,6 @@ export function vcsCliIdentity(cwd?: string): Record<string, any> {
305
303
  }
306
304
 
307
305
  /** Legacy command name, now verifies native CLI auth rather than a separate token. */
308
- export async function vcsVerifyToken(): Promise<Record<string, any>> {
309
- return vcsCliIdentity();
310
- }
311
-
312
306
  /** Port of scripts/vcs/merged-style.sh — recent merged MR/PR bodies for style reference. */
313
307
  export function mergedPrStyle(limit = 6, cwd?: string): Record<string, any> {
314
308
  const cfg = vcsConfig("load", cwd);
@@ -134,13 +134,6 @@ const workspaceConfigSchema = z
134
134
  })
135
135
  .passthrough()
136
136
  .superRefine((workspace, context) => {
137
- if (workspace.youtrack && workspace.vcs?.provider !== "gitlab") {
138
- context.addIssue({
139
- code: "custom",
140
- path: ["youtrack"],
141
- message: `YouTrack issue linking requires the gitlab provider, got ${workspace.vcs?.provider ?? "unset"}`,
142
- });
143
- }
144
137
  if (workspace.issues && workspace.vcs?.provider !== "github") {
145
138
  context.addIssue({
146
139
  code: "custom",
@@ -446,23 +439,20 @@ const selectWorkspaceMatch = <T extends { name: string; glob: string }>(
446
439
  return mostSpecific[0];
447
440
  };
448
441
 
449
- /** Match a cwd against the workspaces.json under an explicit config dir. */
450
- export const resolveWorkspaceFrom = (
442
+ /** Shared pure workspace matcher for draft previews and disk-backed resolution. */
443
+ export const resolveWorkspaceFromEntries = <T extends { name: string; glob: string }>(
451
444
  cwd: string,
452
- dir: string,
445
+ entries: readonly T[],
453
446
  workspaceName?: string,
454
- ): WorkspaceConfig | null => {
447
+ ): T | null => {
455
448
  // macOS/Windows tmpdir symlinks (/var -> /private/var): git's
456
449
  // --show-toplevel returns the realpath while config globs are usually
457
450
  // written with the logical path, so a workspace would silently stop
458
- // matching on macOS. Match both forms on each side — same class as the
459
- // docs-migration escape-guard realpath comparison; on Linux both forms
451
+ // matching on macOS. Match both forms on each side; on Linux both forms
460
452
  // are identical so behavior is unchanged.
461
- const result = readWorkspacesResult(dir);
462
- if (result.status === "malformed" || result.status === "invalid") throw new Error(result.error);
463
453
  const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
464
- const matches: WorkspaceConfig[] = [];
465
- for (const ws of result.entries) {
454
+ const matches: T[] = [];
455
+ for (const ws of entries) {
466
456
  const glob = ws.glob.replaceAll("\\", "/");
467
457
  const canonical = canonicalGlob(glob);
468
458
  let matched = false;
@@ -480,6 +470,17 @@ export const resolveWorkspaceFrom = (
480
470
  return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
481
471
  };
482
472
 
473
+ /** Match a cwd against the workspaces.json under an explicit config dir. */
474
+ export const resolveWorkspaceFrom = (
475
+ cwd: string,
476
+ dir: string,
477
+ workspaceName?: string,
478
+ ): WorkspaceConfig | null => {
479
+ const result = readWorkspacesResult(dir);
480
+ if (result.status === "malformed" || result.status === "invalid") throw new Error(result.error);
481
+ return resolveWorkspaceFromEntries(cwd, result.entries, workspaceName);
482
+ };
483
+
483
484
  export const resolveWorkspace = (
484
485
  cwd: string,
485
486
  workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined,
@@ -1,27 +0,0 @@
1
- import { initStatus } from "./init";
2
-
3
- export const ALL_ITEM_IDS = ["youtrack_json", "youtrack_token", "vcs_json"];
4
- export const CONFIG_GAP_MARKER = "workflow config missing";
5
-
6
- export function describeConfigGaps(scope?: string[]): { missing: string[]; ok: boolean } {
7
- const all = scope ?? ALL_ITEM_IDS;
8
- try {
9
- const status: unknown = initStatus();
10
- if (!status || typeof status !== "object" || (status as Record<string, unknown>).error)
11
- return { missing: all, ok: false };
12
- const items = (status as Record<string, unknown>).items;
13
- if (!Array.isArray(items) || items.length === 0) return { missing: all, ok: false };
14
- const known = all;
15
- const missing = items
16
- .filter((item) => item && (item as Record<string, unknown>).ok === false)
17
- .map((item) => String((item as Record<string, unknown>).id))
18
- .filter((id) => known.includes(id));
19
- return { missing, ok: missing.length === 0 };
20
- } catch {
21
- return { missing: all, ok: false };
22
- }
23
- }
24
-
25
- export function configGuardError(missing: string[]): string {
26
- return `${CONFIG_GAP_MARKER}: ${missing.join(", ")}. Run \`npx workit init\` or \`/wk-init\` to configure.`;
27
- }
@@ -1,14 +0,0 @@
1
- export const MAX_LINES = 150;
2
- export const MAX_BYTES = 8192;
3
- export const MAX_MERMAID = 3;
4
-
5
- const MERMAID_FENCE = /^```mermaid\s*$/gm;
6
-
7
- export const shouldRenderDoc = (text: string): boolean => {
8
- const lines = text.split(/\r?\n/);
9
- if (lines[lines.length - 1] === "") lines.pop();
10
- if (lines.length > MAX_LINES) return false;
11
- if (Buffer.byteLength(text, "utf8") > MAX_BYTES) return false;
12
- const mermaidCount = text.match(MERMAID_FENCE)?.length ?? 0;
13
- return mermaidCount <= MAX_MERMAID;
14
- };