@pi-unipi/unipi 2.4.0 → 2.4.2

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 (99) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +26 -22
  3. package/packages/ask-user/package.json +2 -2
  4. package/packages/autocomplete/package.json +4 -2
  5. package/packages/btw/package.json +2 -2
  6. package/packages/cocoindex/package.json +2 -2
  7. package/packages/cocoindex/tools.ts +0 -8
  8. package/packages/compactor/README.md +18 -22
  9. package/packages/compactor/package.json +3 -3
  10. package/packages/compactor/src/compaction/auto-trigger.ts +0 -0
  11. package/packages/compactor/src/compaction/build-sections.ts +2 -2
  12. package/packages/compactor/src/compaction/format.ts +1 -1
  13. package/packages/compactor/src/config/presets.ts +19 -30
  14. package/packages/compactor/src/config/schema.ts +0 -0
  15. package/packages/compactor/src/index.ts +39 -56
  16. package/packages/compactor/src/session/resume-inject.ts +27 -0
  17. package/packages/compactor/src/tools/ctx-batch-execute.ts +4 -2
  18. package/packages/compactor/src/tools/ctx-execute-file.ts +4 -2
  19. package/packages/compactor/src/tools/ctx-execute.ts +4 -2
  20. package/packages/compactor/src/tools/register.ts +22 -4
  21. package/packages/compactor/src/tui/settings-overlay.ts +9 -22
  22. package/packages/compactor/src/types.ts +24 -2
  23. package/packages/core/events.ts +13 -13
  24. package/packages/core/package.json +3 -2
  25. package/packages/footer/README.md +2 -2
  26. package/packages/footer/package.json +3 -3
  27. package/packages/footer/src/events.ts +0 -10
  28. package/packages/footer/src/segments/core.ts +0 -9
  29. package/packages/image/package.json +2 -2
  30. package/packages/info-screen/core-groups.ts +0 -14
  31. package/packages/info-screen/package.json +2 -2
  32. package/packages/input-shortcuts/package.json +2 -2
  33. package/packages/kanboard/commands.ts +0 -1
  34. package/packages/kanboard/package.json +2 -2
  35. package/packages/mcp/package.json +2 -2
  36. package/packages/memory/package.json +3 -3
  37. package/packages/memory/storage.ts +0 -18
  38. package/packages/memory/tools.ts +0 -1
  39. package/packages/milestone/hooks.ts +9 -15
  40. package/packages/milestone/package.json +2 -2
  41. package/packages/notify/README.md +2 -0
  42. package/packages/notify/events.ts +23 -13
  43. package/packages/notify/package.json +2 -2
  44. package/packages/notify/tools.ts +17 -8
  45. package/packages/notify/types.ts +5 -1
  46. package/packages/ralph/index.ts +10 -5
  47. package/packages/ralph/package.json +3 -3
  48. package/packages/ralph/ralph-loop.ts +10 -0
  49. package/packages/subagents/README.md +3 -1
  50. package/packages/subagents/package.json +10 -6
  51. package/packages/subagents/src/agent-manager.ts +46 -11
  52. package/packages/subagents/src/core-compat.ts +38 -0
  53. package/packages/subagents/src/global.d.ts +7 -0
  54. package/packages/subagents/src/index.ts +6 -3
  55. package/packages/subagents/src/widget.ts +0 -7
  56. package/packages/unipi/bundled.js +371 -438
  57. package/packages/updater/package.json +4 -3
  58. package/packages/utility/README.md +1 -1
  59. package/packages/utility/package.json +7 -4
  60. package/packages/utility/src/commands.ts +2 -10
  61. package/packages/utility/src/diff/theme.ts +0 -3
  62. package/packages/utility/src/tui/name-badge.ts +0 -9
  63. package/packages/web-api/package.json +2 -2
  64. package/packages/web-api/src/engine/extract.ts +0 -26
  65. package/packages/web-api/src/tools.ts +0 -13
  66. package/packages/workflow/commands.ts +13 -1
  67. package/packages/workflow/index.ts +12 -1
  68. package/packages/workflow/lifecycle.ts +43 -0
  69. package/packages/workflow/package.json +3 -2
  70. package/packages/autocomplete/index.ts +0 -7
  71. package/packages/autocomplete/src/__tests__/command-registry.audit.test.ts +0 -120
  72. package/packages/autocomplete/src/__tests__/provider.sorting.test.ts +0 -423
  73. package/packages/compactor/src/compaction/recall-scope.ts +0 -32
  74. package/packages/compactor/src/compaction/sections.ts +0 -15
  75. package/packages/compactor/src/display/bash-display.ts +0 -28
  76. package/packages/compactor/src/display/diff-presentation.ts +0 -25
  77. package/packages/compactor/src/display/pending-diff-preview.ts +0 -51
  78. package/packages/compactor/src/display/user-message-box.ts +0 -16
  79. package/packages/compactor/src/session/analytics.ts +0 -205
  80. package/packages/image/index.ts +0 -1
  81. package/packages/input-shortcuts/index.ts +0 -5
  82. package/packages/input-shortcuts/src/status.ts +0 -35
  83. package/packages/kanboard/tui/kanboard-overlay.ts +0 -300
  84. package/packages/mcp/index.ts +0 -1
  85. package/packages/memory/search.ts +0 -134
  86. package/packages/milestone/coexist.ts +0 -114
  87. package/packages/notify/src/__tests__/ask-user-prompt-message.test.ts +0 -159
  88. package/packages/notify/src/__tests__/event-bus.test.ts +0 -119
  89. package/packages/notify/src/__tests__/permission-prompt-message.test.ts +0 -172
  90. package/packages/subagents/src/__tests__/badge-generation.test.ts +0 -315
  91. package/packages/subagents/src/__tests__/config.test.ts +0 -240
  92. package/packages/subagents/src/__tests__/esc-propagation.test.ts +0 -162
  93. package/packages/subagents/src/__tests__/file-lock.test.ts +0 -244
  94. package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +0 -185
  95. package/packages/subagents/src/__tests__/workflow-integration.test.ts +0 -334
  96. package/packages/subagents/src/prompts.ts +0 -39
  97. package/packages/utility/index.ts +0 -1
  98. package/packages/web-api/src/tui/progress.ts +0 -168
  99. package/packages/web-api/src/tui/result.ts +0 -173
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@pi-unipi/updater",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Auto-updater, changelog browser, and readme browser for Unipi — checks npm registry, renders CHANGELOG.md and README.md files in TUI overlays",
5
5
  "type": "module",
6
- "main": "src/index.ts",
6
+ "main": "index.ts",
7
7
  "license": "MIT",
8
8
  "author": "Neuron Mr White",
9
9
  "repository": {
@@ -22,6 +22,7 @@
22
22
  ],
23
23
  "files": [
24
24
  "index.ts",
25
+ "types.ts",
25
26
  "src/**/*.ts",
26
27
  "skills/**/*",
27
28
  "README.md"
@@ -33,7 +34,7 @@
33
34
  "access": "public"
34
35
  },
35
36
  "dependencies": {
36
- "@pi-unipi/core": "2.4.0"
37
+ "@pi-unipi/core": "2.4.1"
37
38
  },
38
39
  "peerDependencies": {
39
40
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -10,7 +10,7 @@ The diff rendering is the standout feature — Shiki-powered syntax-highlighted
10
10
  |---------|-------------|
11
11
  | `/unipi:env` | Show environment info (Node, Pi, OS, paths) |
12
12
  | `/unipi:doctor` | Run diagnostics across all modules |
13
- | `/unipi:status` | Show status of all unipi modules |
13
+ | `/unipi:status` | Show immediate guidance to the live `/unipi:info` dashboard and `/unipi:doctor` diagnostics |
14
14
  | `/unipi:cleanup` | Clean stale DBs, temp files, old sessions |
15
15
  | `/unipi:reload` | Explain how to reload extensions |
16
16
  | `/unipi:name-badge` | Toggle name badge overlay |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/utility",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Utility commands and tools for Pi coding agent — lifecycle, diagnostics, cache, analytics, display, batch execution",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -28,21 +28,24 @@
28
28
  "src/types.ts",
29
29
  "src/**/*.ts",
30
30
  "README.md",
31
- "skills/**/*.md"
31
+ "skills/**/*.md",
32
+ "!src/__tests__/**",
33
+ "!src/**/*.test.ts"
32
34
  ],
33
35
  "publishConfig": {
34
36
  "access": "public"
35
37
  },
36
38
  "dependencies": {
37
- "@pi-unipi/core": "2.4.0",
39
+ "@pi-unipi/core": "2.4.1",
38
40
  "diff": "^7.0.0",
39
- "@shikijs/cli": "^4.0.2"
41
+ "shiki": "^4.0.2"
40
42
  },
41
43
  "devDependencies": {
42
44
  "@types/diff": "^7.0.2"
43
45
  },
44
46
  "peerDependencies": {
45
47
  "@earendil-works/pi-coding-agent": "^0.80.0",
48
+ "@earendil-works/pi-tui": "^0.80.0",
46
49
  "typebox": "^1.1.38"
47
50
  },
48
51
  "scripts": {
@@ -234,19 +234,11 @@ export function registerUtilityCommands(pi: ExtensionAPI): void {
234
234
  return;
235
235
  }
236
236
 
237
- // Request status from all modules — responses will be logged
238
- const requestId = `status-${Date.now()}`;
239
- emitEvent(pi, UNIPI_EVENTS.MODULE_STATUS_REQUEST, { requestId });
240
-
241
- // Give modules a moment to respond, then show what we know
242
- await new Promise((resolve) => setTimeout(resolve, 500));
243
-
244
237
  sendResponse(
245
238
  pi,
246
239
  "## 📊 Module Status\n\n" +
247
- "Status request broadcast to all modules.\n" +
248
- "Modules that support status reporting will respond via events.\n\n" +
249
- `*Request ID: \`${requestId}\`*`,
240
+ "Use `/unipi:info` for the live module and tool dashboard.\n" +
241
+ "Use `/unipi:doctor` to run configuration and runtime diagnostics.",
250
242
  );
251
243
  },
252
244
  });
@@ -101,9 +101,6 @@ const PRESETS: Record<string, DiffPreset> = {
101
101
  },
102
102
  };
103
103
 
104
- /** Hardcoded fallback colors (last resort) */
105
- const HARDCODED_FALLBACK: DiffColors = PRESETS.default.colors;
106
-
107
104
  // ─── Preset Access ──────────────────────────────────────────────────────────────
108
105
 
109
106
  /**
@@ -13,15 +13,6 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
13
13
  /** Placeholder text when no session name is set */
14
14
  const PLACEHOLDER = "Set a name";
15
15
 
16
- /**
17
- * Pad content to exact visible width.
18
- */
19
- function padVisible(content: string, targetWidth: number): string {
20
- const vw = visibleWidth(content);
21
- const pad = Math.max(0, targetWidth - vw);
22
- return content + " ".repeat(pad);
23
- }
24
-
25
16
  /**
26
17
  * NameBadgeComponent — bordered box HUD overlay showing session name.
27
18
  *
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/web-api",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Web search, read, and summarize tools with provider-based backend selection for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -31,7 +31,7 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
- "@pi-unipi/core": "2.4.0",
34
+ "@pi-unipi/core": "2.4.1",
35
35
  "defuddle": "^0.18.1",
36
36
  "linkedom": "^0.18.12",
37
37
  "lodash": "^4.17.21",
@@ -35,7 +35,6 @@ import { truncateContent, formatContent } from "./format.js";
35
35
  const MAX_REDIRECTS = 5;
36
36
 
37
37
  /** Maximum alternate link fallbacks to try */
38
- const MAX_ALTERNATE_LINKS = 3;
39
38
 
40
39
  /**
41
40
  * Validate a URL for fetching.
@@ -193,31 +192,6 @@ function findMetaRefresh(document: Document): string | null {
193
192
  return match[1];
194
193
  }
195
194
 
196
- /**
197
- * Check for alternate JSON content links.
198
- *
199
- * @param document - DOM document
200
- * @returns Array of alternate URLs
201
- */
202
- function findAlternateLinks(document: Document): string[] {
203
- const alternates: string[] = [];
204
-
205
- // Look for JSON feeds, oEmbed, etc.
206
- const links = document.querySelectorAll(
207
- 'link[rel="alternate"][type="application/json"], ' +
208
- 'link[rel="alternate"][type="application/ld+json"]'
209
- );
210
-
211
- for (const link of Array.from(links)) {
212
- const href = link.getAttribute("href");
213
- if (href) {
214
- alternates.push(href);
215
- }
216
- }
217
-
218
- return alternates.slice(0, MAX_ALTERNATE_LINKS);
219
- }
220
-
221
195
  /**
222
196
  * Detect content type from response.
223
197
  */
@@ -62,19 +62,6 @@ function getAvailableProviders(capability: WebCapability): WebProvider[] {
62
62
  });
63
63
  }
64
64
 
65
- /**
66
- * Select provider for a capability.
67
- * If sourceRank is specified, use that rank.
68
- * Otherwise, use the lowest-ranked available provider.
69
- */
70
- function selectProvider(
71
- capability: WebCapability,
72
- sourceRank?: number
73
- ): WebProvider {
74
- const candidates = selectProviderChain(capability, sourceRank);
75
- return candidates[0];
76
- }
77
-
78
65
  /**
79
66
  * Build the ordered list of providers to try for a capability.
80
67
  *
@@ -8,7 +8,7 @@
8
8
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
9
  import { readFileSync, readdirSync, existsSync, statSync } from "fs";
10
10
  import { join, basename } from "path";
11
- import { UNIPI_PREFIX, WORKFLOW_COMMANDS, getToolsForCommand, getSandboxLevel, type SandboxLevel } from "@pi-unipi/core";
11
+ import { UNIPI_PREFIX, WORKFLOW_COMMANDS, getToolsForCommand, getSandboxLevel, type SandboxLevel, type UnipiWorkflowEvent } from "@pi-unipi/core";
12
12
 
13
13
  type CompletionItem = { value: string; label: string; description: string };
14
14
 
@@ -22,6 +22,8 @@ export interface WorkflowCommandOptions {
22
22
  setActiveTools: (tools: string[], level: SandboxLevel) => void;
23
23
  /** Save tools for later restore */
24
24
  saveTools: (tools: string[]) => void;
25
+ /** Begin one workflow lifecycle; false means another workflow is active. */
26
+ startWorkflow: (event: UnipiWorkflowEvent) => boolean;
25
27
  }
26
28
 
27
29
  /** Command definition */
@@ -402,6 +404,16 @@ export function registerWorkflowCommands(
402
404
  return null;
403
405
  },
404
406
  handler: async (args, ctx) => {
407
+ const workflowEvent: UnipiWorkflowEvent = {
408
+ command: cmd.name,
409
+ fullCommand: `/${fullCommand}`,
410
+ args: args?.trim() ?? "",
411
+ };
412
+ if (!options.startWorkflow(workflowEvent)) {
413
+ if (ctx.hasUI) ctx.ui.notify("Another UniPi workflow is still active", "warning");
414
+ return;
415
+ }
416
+
405
417
  // Apply sandbox — save current tools, set command's tools
406
418
  const currentTools = options.getActiveTools();
407
419
  options.saveTools(currentTools);
@@ -21,6 +21,7 @@ import {
21
21
  getBlockedToolsForLevel,
22
22
  } from "@pi-unipi/core";
23
23
  import { registerWorkflowCommands } from "./commands.js";
24
+ import { WorkflowLifecycle } from "./lifecycle.js";
24
25
 
25
26
  /** Package version (read from package.json at load time) */
26
27
  const VERSION = getPackageVersion(dirname(fileURLToPath(import.meta.url)));
@@ -41,6 +42,7 @@ let currentSandboxLevel: SandboxLevel | null = null;
41
42
  let currentSandboxTools: string[] | null = null;
42
43
 
43
44
  export default function (pi: ExtensionAPI) {
45
+ const workflowLifecycle = new WorkflowLifecycle();
44
46
 
45
47
  // Register all workflow commands
46
48
  registerWorkflowCommands(pi, {
@@ -55,6 +57,11 @@ export default function (pi: ExtensionAPI) {
55
57
  saveTools: (tools: string[]) => {
56
58
  savedTools = tools;
57
59
  },
60
+ startWorkflow: (event) => {
61
+ if (!workflowLifecycle.start(event)) return false;
62
+ emitEvent(pi, UNIPI_EVENTS.WORKFLOW_START, event);
63
+ return true;
64
+ },
58
65
  });
59
66
 
60
67
  // Block tool calls that violate sandbox
@@ -109,7 +116,7 @@ export default function (pi: ExtensionAPI) {
109
116
  });
110
117
 
111
118
  // Restore tools when agent finishes
112
- pi.on("agent_end", async (_event, _ctx) => {
119
+ pi.on("agent_end", async (event, _ctx) => {
113
120
  if (sandboxActive && savedTools) {
114
121
  pi.setActiveTools(savedTools);
115
122
  savedTools = null;
@@ -117,6 +124,9 @@ export default function (pi: ExtensionAPI) {
117
124
  currentSandboxLevel = null;
118
125
  currentSandboxTools = null;
119
126
  }
127
+
128
+ const completedWorkflow = workflowLifecycle.complete(event.messages);
129
+ if (completedWorkflow) emitEvent(pi, UNIPI_EVENTS.WORKFLOW_END, completedWorkflow);
120
130
  });
121
131
 
122
132
  // Announce module presence on session start
@@ -163,5 +173,6 @@ export default function (pi: ExtensionAPI) {
163
173
  ralphDetected = false;
164
174
  savedTools = null;
165
175
  sandboxActive = false;
176
+ workflowLifecycle.reset();
166
177
  });
167
178
  }
@@ -0,0 +1,43 @@
1
+ import type { UnipiWorkflowEvent } from "@pi-unipi/core";
2
+
3
+ interface WorkflowMessage {
4
+ role: string;
5
+ stopReason?: string;
6
+ }
7
+
8
+ export interface CompletedWorkflowEvent extends UnipiWorkflowEvent {
9
+ success: boolean;
10
+ durationMs: number;
11
+ }
12
+
13
+ /** Single-active workflow lifecycle state shared by slash handlers and agent_end. */
14
+ export class WorkflowLifecycle {
15
+ private active: (UnipiWorkflowEvent & { startedAt: number }) | null = null;
16
+
17
+ constructor(private readonly now: () => number = Date.now) {}
18
+
19
+ start(event: UnipiWorkflowEvent): boolean {
20
+ if (this.active) return false;
21
+ this.active = { ...event, startedAt: this.now() };
22
+ return true;
23
+ }
24
+
25
+ complete(messages: WorkflowMessage[]): CompletedWorkflowEvent | undefined {
26
+ if (!this.active) return undefined;
27
+ const workflow = this.active;
28
+ this.active = null;
29
+ const finalAssistant = [...messages].reverse().find((message) => message.role === "assistant");
30
+ const success = finalAssistant?.stopReason !== "error" && finalAssistant?.stopReason !== "aborted";
31
+ return {
32
+ command: workflow.command,
33
+ fullCommand: workflow.fullCommand,
34
+ args: workflow.args,
35
+ success,
36
+ durationMs: this.now() - workflow.startedAt,
37
+ };
38
+ }
39
+
40
+ reset(): void {
41
+ this.active = null;
42
+ }
43
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/workflow",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Structured development workflow commands for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -19,6 +19,7 @@
19
19
  ],
20
20
  "files": [
21
21
  "*.ts",
22
+ "!*.test.ts",
22
23
  "skills/**/*",
23
24
  "README.md"
24
25
  ],
@@ -26,7 +27,7 @@
26
27
  "access": "public"
27
28
  },
28
29
  "dependencies": {
29
- "@pi-unipi/core": "2.4.0"
30
+ "@pi-unipi/core": "2.4.1"
30
31
  },
31
32
  "peerDependencies": {
32
33
  "@earendil-works/pi-ai": "^0.80.0",
@@ -1,7 +0,0 @@
1
- /**
2
- * @pi-unipi/command-enchantment — Re-export
3
- *
4
- * Re-exports from src/ for module resolution.
5
- */
6
-
7
- export { default } from "./src/index.js";
@@ -1,120 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { existsSync, globSync, readFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
4
-
5
- function findRepoRoot(start: string): string {
6
- let dir = start;
7
- while (dir !== dirname(dir)) {
8
- if (existsSync(join(dir, "packages", "autocomplete", "src", "constants.ts"))) return dir;
9
- dir = dirname(dir);
10
- }
11
- return start;
12
- }
13
-
14
- const root = findRepoRoot(process.cwd());
15
-
16
- function read(path: string): string {
17
- return readFileSync(join(root, path), "utf-8");
18
- }
19
-
20
- function collectConstants(): Map<string, string> {
21
- const constants = new Map<string, string>([["UNIPI_PREFIX", "unipi:"]]);
22
-
23
- for (const path of globSync("packages/**/*.ts", { cwd: root })) {
24
- const text = read(path);
25
- for (const obj of text.matchAll(/(?:export\s+)?const\s+(\w+)\s*=\s*\{([\s\S]*?)\}\s*as\s+const/g)) {
26
- const [, name, body] = obj;
27
- for (const item of body.matchAll(/(\w+):\s*"([^"]+)"/g)) {
28
- constants.set(`${name}.${item[1]}`, item[2]);
29
- }
30
- }
31
- }
32
-
33
- return constants;
34
- }
35
-
36
- function evaluateCommandExpression(expr: string, constants: Map<string, string>): string | null {
37
- const trimmed = expr.trim();
38
-
39
- if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
40
- const quote = trimmed[0];
41
- const end = trimmed.indexOf(quote, 1);
42
- return end > 0 ? trimmed.slice(1, end) : null;
43
- }
44
-
45
- if (trimmed.startsWith("`")) {
46
- const end = trimmed.lastIndexOf("`");
47
- if (end <= 0) return null;
48
- return trimmed.slice(1, end).replace(/\$\{([^}]+)\}/g, (_match, key: string) => {
49
- const resolved = constants.get(key.trim());
50
- return resolved ?? `\${${key}}`;
51
- });
52
- }
53
-
54
- return constants.get(trimmed) ?? null;
55
- }
56
-
57
- function registeredCommands(): { commands: Set<string>; nonUnipi: string[]; unresolved: string[] } {
58
- const constants = collectConstants();
59
- const commands = new Set<string>();
60
- const nonUnipi: string[] = [];
61
- const unresolved: string[] = [];
62
-
63
- for (const path of globSync("packages/**/*.ts", { cwd: root }).sort()) {
64
- const text = read(path);
65
- if (!text.includes("registerCommand")) continue;
66
-
67
- for (const match of text.matchAll(/\.registerCommand\(\s*([^,\n]+)/g)) {
68
- const expr = match[1].trim();
69
-
70
- // Workflow registers a loop over WORKFLOW_COMMANDS via local `fullCommand`.
71
- if (path === "packages/workflow/commands.ts" && expr === "fullCommand") {
72
- for (const [key, value] of constants) {
73
- if (key.startsWith("WORKFLOW_COMMANDS.")) commands.add(`unipi:${value}`);
74
- }
75
- continue;
76
- }
77
-
78
- const command = evaluateCommandExpression(expr, constants);
79
- if (!command || command.includes("${")) {
80
- unresolved.push(`${path}: ${expr}`);
81
- continue;
82
- }
83
-
84
- if (command.startsWith("unipi:")) {
85
- commands.add(command);
86
- } else {
87
- nonUnipi.push(`${command} (${path})`);
88
- }
89
- }
90
- }
91
-
92
- return { commands, nonUnipi, unresolved };
93
- }
94
-
95
- function autocompleteRegistry(): { registry: Set<string>; descriptions: Set<string>; registryPackages: Set<string>; labels: Set<string> } {
96
- const text = read("packages/autocomplete/src/constants.ts");
97
- const registryBody = text.match(/export const COMMAND_REGISTRY[^=]*= \{([\s\S]*?)\n\};/)?.[1] ?? "";
98
- const descriptionsBody = text.match(/export const COMMAND_DESCRIPTIONS[^=]*= \{([\s\S]*?)\n\};/)?.[1] ?? "";
99
- const labelsBody = text.match(/export const PACKAGE_LABELS[^=]*= \{([\s\S]*?)\n\};/)?.[1] ?? "";
100
-
101
- const registry = new Set([...registryBody.matchAll(/"(unipi:[^"]+)"\s*:/g)].map((m) => m[1]));
102
- const descriptions = new Set([...descriptionsBody.matchAll(/"(unipi:[^"]+)"\s*:/g)].map((m) => m[1]));
103
- const registryPackages = new Set([...registryBody.matchAll(/"unipi:[^"]+"\s*:\s*"([^"]+)"/g)].map((m) => m[1]));
104
- const labels = new Set([...labelsBody.matchAll(/^\s*"?([a-z][a-z0-9-]*)"?:\s*"/gm)].map((m) => m[1]));
105
-
106
- return { registry, descriptions, registryPackages, labels };
107
- }
108
-
109
- describe("autocomplete command registry audit", () => {
110
- it("mirrors every registered /unipi:* command and has no non-unipi package commands", () => {
111
- const registered = registeredCommands();
112
- const autocomplete = autocompleteRegistry();
113
-
114
- expect(registered.unresolved, "unresolved registerCommand expressions").toEqual([]);
115
- expect(registered.nonUnipi, "package commands must use the unipi: prefix").toEqual([]);
116
- expect([...registered.commands].sort()).toEqual([...autocomplete.registry].sort());
117
- expect([...autocomplete.registry].sort()).toEqual([...autocomplete.descriptions].sort());
118
- expect([...autocomplete.registryPackages].filter((pkg) => !autocomplete.labels.has(pkg)).sort()).toEqual([]);
119
- });
120
- });