@uluops/setup 0.2.0 → 0.6.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.
Files changed (253) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -89
  3. package/assets/auto-tracker-save.mjs +142 -0
  4. package/assets/claude-code/agents/anxiety-reader-agent.md +464 -0
  5. package/assets/{agents → claude-code/agents}/api-contract-validator-agent.md +9 -228
  6. package/assets/{agents → claude-code/agents}/aristotle-analyst-agent.md +51 -4
  7. package/assets/{agents → claude-code/agents}/aristotle-explorer-agent.md +6 -2
  8. package/assets/{agents → claude-code/agents}/aristotle-forecaster-agent.md +15 -230
  9. package/assets/{agents → claude-code/agents}/aristotle-validator-agent.md +12 -252
  10. package/assets/{agents → claude-code/agents}/assumption-excavator-agent.md +21 -247
  11. package/assets/{agents → claude-code/agents}/code-auditor-agent.md +12 -255
  12. package/assets/{agents → claude-code/agents}/code-optimizer-agent.md +15 -236
  13. package/assets/{agents → claude-code/agents}/code-validator-agent.md +31 -300
  14. package/assets/claude-code/agents/docs-validator-agent.md +472 -0
  15. package/assets/{agents → claude-code/agents}/frontend-validator-agent.md +15 -258
  16. package/assets/{agents → claude-code/agents}/mcp-validator-agent.md +8 -252
  17. package/assets/{agents → claude-code/agents}/pre-implementation-architect-agent.md +8 -224
  18. package/assets/{agents → claude-code/agents}/prompt-engineer-agent.md +57 -290
  19. package/assets/{agents → claude-code/agents}/prompt-pattern-analyzer-agent.md +10 -225
  20. package/assets/{agents → claude-code/agents}/prompt-quality-validator-agent.md +11 -249
  21. package/assets/{agents → claude-code/agents}/public-interface-validator-agent.md +15 -268
  22. package/assets/claude-code/agents/release-readiness-agent.md +495 -0
  23. package/assets/{agents → claude-code/agents}/security-analyst-agent.md +236 -480
  24. package/assets/{agents → claude-code/agents}/test-architect-agent.md +16 -259
  25. package/assets/{agents → claude-code/agents}/type-safety-validator-agent.md +23 -266
  26. package/assets/{agents → claude-code/agents}/workflow-synthesis-agent.md +23 -226
  27. package/assets/claude-code/commands/agents/anxiety-reader.md +157 -0
  28. package/assets/{commands → claude-code/commands}/agents/api-contract.md +156 -135
  29. package/assets/{commands → claude-code/commands}/agents/architect.md +156 -135
  30. package/assets/claude-code/commands/agents/aristotle-analyst.md +157 -0
  31. package/assets/claude-code/commands/agents/aristotle-explorer.md +157 -0
  32. package/assets/claude-code/commands/agents/aristotle-forecaster.md +157 -0
  33. package/assets/claude-code/commands/agents/aristotle-validator.md +157 -0
  34. package/assets/{commands → claude-code/commands}/agents/assumption-excavator.md +49 -6
  35. package/assets/{commands → claude-code/commands}/agents/audit.md +156 -136
  36. package/assets/{commands → claude-code/commands}/agents/docs-validate.md +156 -133
  37. package/assets/{commands → claude-code/commands}/agents/frontend.md +156 -135
  38. package/assets/{commands → claude-code/commands}/agents/mcp-validate.md +156 -136
  39. package/assets/{commands → claude-code/commands}/agents/optimize.md +156 -133
  40. package/assets/{commands → claude-code/commands}/agents/pattern-analyzer.md +150 -126
  41. package/assets/{commands → claude-code/commands}/agents/prompt-quality.md +155 -134
  42. package/assets/claude-code/commands/agents/prompt-validate.md +155 -0
  43. package/assets/{commands → claude-code/commands}/agents/public-interface.md +156 -134
  44. package/assets/{commands → claude-code/commands}/agents/release.md +156 -135
  45. package/assets/{commands → claude-code/commands}/agents/security.md +156 -137
  46. package/assets/{commands → claude-code/commands}/agents/test-review.md +156 -136
  47. package/assets/{commands → claude-code/commands}/agents/type-safety.md +156 -135
  48. package/assets/{commands → claude-code/commands}/agents/validate.md +156 -134
  49. package/assets/claude-code/commands/agents/workflow-synthesis.md +157 -0
  50. package/assets/claude-code/commands/pipelines/aristotle.md +143 -0
  51. package/assets/claude-code/commands/pipelines/ship.md +188 -0
  52. package/assets/claude-code/commands/workflows/post-implementation.md +60 -0
  53. package/assets/claude-code/commands/workflows/pre-implementation.md +46 -0
  54. package/assets/claude-code/commands/workflows/prompt-audit.md +44 -0
  55. package/assets/codex/agents/anxiety-reader-agent.toml +462 -0
  56. package/assets/codex/agents/api-contract-validator-agent.toml +738 -0
  57. package/assets/codex/agents/aristotle-analyst-agent.toml +750 -0
  58. package/assets/codex/agents/aristotle-explorer-agent.toml +155 -0
  59. package/assets/codex/agents/aristotle-forecaster-agent.toml +449 -0
  60. package/assets/codex/agents/aristotle-validator-agent.toml +424 -0
  61. package/assets/codex/agents/assumption-excavator-agent.toml +1126 -0
  62. package/assets/codex/agents/code-auditor-agent.toml +815 -0
  63. package/assets/codex/agents/code-optimizer-agent.toml +652 -0
  64. package/assets/codex/agents/code-validator-agent.toml +573 -0
  65. package/assets/codex/agents/docs-validator-agent.toml +468 -0
  66. package/assets/codex/agents/frontend-validator-agent.toml +598 -0
  67. package/assets/codex/agents/mcp-validator-agent.toml +580 -0
  68. package/assets/codex/agents/pre-implementation-architect-agent.toml +817 -0
  69. package/assets/codex/agents/prompt-engineer-agent.toml +922 -0
  70. package/assets/codex/agents/prompt-pattern-analyzer-agent.toml +689 -0
  71. package/assets/codex/agents/prompt-quality-validator-agent.toml +777 -0
  72. package/assets/codex/agents/public-interface-validator-agent.toml +695 -0
  73. package/assets/codex/agents/release-readiness-agent.toml +491 -0
  74. package/assets/codex/agents/security-analyst-agent.toml +847 -0
  75. package/assets/codex/agents/test-architect-agent.toml +615 -0
  76. package/assets/codex/agents/type-safety-validator-agent.toml +686 -0
  77. package/assets/codex/agents/workflow-synthesis-agent.toml +631 -0
  78. package/assets/gemini-cli/agents/anxiety-reader-agent.md +470 -0
  79. package/assets/gemini-cli/agents/api-contract-validator-agent.md +747 -0
  80. package/assets/gemini-cli/agents/aristotle-analyst-agent.md +758 -0
  81. package/assets/gemini-cli/agents/aristotle-explorer-agent.md +163 -0
  82. package/assets/gemini-cli/agents/aristotle-forecaster-agent.md +457 -0
  83. package/assets/gemini-cli/agents/aristotle-validator-agent.md +432 -0
  84. package/assets/gemini-cli/agents/assumption-excavator-agent.md +1134 -0
  85. package/assets/gemini-cli/agents/code-auditor-agent.md +827 -0
  86. package/assets/gemini-cli/agents/code-optimizer-agent.md +661 -0
  87. package/assets/gemini-cli/agents/code-validator-agent.md +582 -0
  88. package/assets/gemini-cli/agents/docs-validator-agent.md +477 -0
  89. package/assets/gemini-cli/agents/frontend-validator-agent.md +610 -0
  90. package/assets/gemini-cli/agents/mcp-validator-agent.md +589 -0
  91. package/assets/gemini-cli/agents/pre-implementation-architect-agent.md +826 -0
  92. package/assets/gemini-cli/agents/prompt-engineer-agent.md +931 -0
  93. package/assets/gemini-cli/agents/prompt-pattern-analyzer-agent.md +698 -0
  94. package/assets/gemini-cli/agents/prompt-quality-validator-agent.md +786 -0
  95. package/assets/gemini-cli/agents/public-interface-validator-agent.md +707 -0
  96. package/assets/gemini-cli/agents/release-readiness-agent.md +500 -0
  97. package/assets/gemini-cli/agents/security-analyst-agent.md +859 -0
  98. package/assets/gemini-cli/agents/test-architect-agent.md +624 -0
  99. package/assets/gemini-cli/agents/type-safety-validator-agent.md +695 -0
  100. package/assets/gemini-cli/agents/workflow-synthesis-agent.md +639 -0
  101. package/assets/gemini-cli/commands/agents/anxiety-reader.toml +155 -0
  102. package/assets/gemini-cli/commands/agents/api-contract.toml +154 -0
  103. package/assets/gemini-cli/commands/agents/architect.toml +154 -0
  104. package/assets/gemini-cli/commands/agents/aristotle-analyst.toml +155 -0
  105. package/assets/gemini-cli/commands/agents/aristotle-explorer.toml +155 -0
  106. package/assets/gemini-cli/commands/agents/aristotle-forecaster.toml +155 -0
  107. package/assets/gemini-cli/commands/agents/aristotle-validator.toml +155 -0
  108. package/assets/gemini-cli/commands/agents/assumption-excavator.toml +155 -0
  109. package/assets/gemini-cli/commands/agents/audit.toml +154 -0
  110. package/assets/gemini-cli/commands/agents/docs-validate.toml +154 -0
  111. package/assets/gemini-cli/commands/agents/frontend.toml +154 -0
  112. package/assets/gemini-cli/commands/agents/mcp-validate.toml +154 -0
  113. package/assets/gemini-cli/commands/agents/optimize.toml +154 -0
  114. package/assets/gemini-cli/commands/agents/pattern-analyzer.toml +148 -0
  115. package/assets/gemini-cli/commands/agents/prompt-quality.toml +153 -0
  116. package/assets/gemini-cli/commands/agents/prompt-validate.toml +153 -0
  117. package/assets/gemini-cli/commands/agents/public-interface.toml +154 -0
  118. package/assets/gemini-cli/commands/agents/release.toml +154 -0
  119. package/assets/gemini-cli/commands/agents/security.toml +154 -0
  120. package/assets/gemini-cli/commands/agents/test-review.toml +154 -0
  121. package/assets/gemini-cli/commands/agents/type-safety.toml +154 -0
  122. package/assets/gemini-cli/commands/agents/validate.toml +154 -0
  123. package/assets/gemini-cli/commands/agents/workflow-synthesis.toml +155 -0
  124. package/assets/gemini-cli/commands/pipelines/aristotle.toml +139 -0
  125. package/assets/gemini-cli/commands/pipelines/ship.toml +184 -0
  126. package/assets/gemini-cli/commands/workflows/post-implementation.toml +56 -0
  127. package/assets/gemini-cli/commands/workflows/pre-implementation.toml +42 -0
  128. package/assets/gemini-cli/commands/workflows/prompt-audit.toml +40 -0
  129. package/assets/opencode/agents/anxiety-reader-agent.md +472 -0
  130. package/assets/opencode/agents/api-contract-validator-agent.md +749 -0
  131. package/assets/opencode/agents/aristotle-analyst-agent.md +760 -0
  132. package/assets/opencode/agents/aristotle-explorer-agent.md +164 -0
  133. package/assets/opencode/agents/aristotle-forecaster-agent.md +459 -0
  134. package/assets/opencode/agents/aristotle-validator-agent.md +434 -0
  135. package/assets/opencode/agents/assumption-excavator-agent.md +1136 -0
  136. package/assets/opencode/agents/code-auditor-agent.md +826 -0
  137. package/assets/opencode/agents/code-optimizer-agent.md +663 -0
  138. package/assets/opencode/agents/code-validator-agent.md +584 -0
  139. package/assets/opencode/agents/docs-validator-agent.md +479 -0
  140. package/assets/opencode/agents/frontend-validator-agent.md +609 -0
  141. package/assets/opencode/agents/mcp-validator-agent.md +591 -0
  142. package/assets/opencode/agents/pre-implementation-architect-agent.md +828 -0
  143. package/assets/opencode/agents/prompt-engineer-agent.md +933 -0
  144. package/assets/opencode/agents/prompt-pattern-analyzer-agent.md +700 -0
  145. package/assets/opencode/agents/prompt-quality-validator-agent.md +788 -0
  146. package/assets/opencode/agents/public-interface-validator-agent.md +706 -0
  147. package/assets/opencode/agents/release-readiness-agent.md +502 -0
  148. package/assets/opencode/agents/security-analyst-agent.md +858 -0
  149. package/assets/opencode/agents/test-architect-agent.md +626 -0
  150. package/assets/opencode/agents/type-safety-validator-agent.md +697 -0
  151. package/assets/opencode/agents/workflow-synthesis-agent.md +641 -0
  152. package/dist/cli.js +22 -380
  153. package/dist/commands/helpers.d.ts +73 -0
  154. package/dist/commands/helpers.js +274 -0
  155. package/dist/commands/setup.d.ts +13 -0
  156. package/dist/commands/setup.js +93 -0
  157. package/dist/commands/uninstall.d.ts +3 -0
  158. package/dist/commands/uninstall.js +126 -0
  159. package/dist/commands/verify.d.ts +1 -0
  160. package/dist/commands/verify.js +28 -0
  161. package/dist/harnesses/claude-code.d.ts +8 -0
  162. package/dist/harnesses/claude-code.js +74 -0
  163. package/dist/harnesses/codex.d.ts +15 -0
  164. package/dist/harnesses/codex.js +54 -0
  165. package/dist/harnesses/gemini-cli.d.ts +12 -0
  166. package/dist/harnesses/gemini-cli.js +80 -0
  167. package/dist/harnesses/index.d.ts +27 -0
  168. package/dist/harnesses/index.js +54 -0
  169. package/dist/harnesses/opencode.d.ts +14 -0
  170. package/dist/harnesses/opencode.js +139 -0
  171. package/dist/harnesses/types.d.ts +106 -0
  172. package/dist/harnesses/types.js +26 -0
  173. package/dist/lib/agent-transform.d.ts +12 -0
  174. package/dist/lib/agent-transform.js +129 -0
  175. package/dist/lib/asset-catalog.d.ts +9 -0
  176. package/dist/lib/asset-catalog.js +56 -0
  177. package/dist/lib/atomic-write.d.ts +11 -0
  178. package/dist/lib/atomic-write.js +28 -0
  179. package/dist/lib/config-merger.d.ts +9 -2
  180. package/dist/lib/config-merger.js +44 -7
  181. package/dist/lib/display.d.ts +14 -0
  182. package/dist/lib/display.js +66 -0
  183. package/dist/lib/file-ops.d.ts +11 -0
  184. package/dist/lib/file-ops.js +40 -4
  185. package/dist/lib/hash.d.ts +1 -0
  186. package/dist/lib/hash.js +2 -1
  187. package/dist/lib/health.d.ts +2 -0
  188. package/dist/lib/health.js +10 -0
  189. package/dist/lib/manifest.d.ts +51 -5
  190. package/dist/lib/manifest.js +146 -13
  191. package/dist/lib/paths.d.ts +30 -3
  192. package/dist/lib/paths.js +98 -12
  193. package/dist/lib/settings-merger.d.ts +31 -8
  194. package/dist/lib/settings-merger.js +87 -24
  195. package/dist/lib/version.d.ts +2 -0
  196. package/dist/lib/version.js +10 -0
  197. package/dist/steps/agents.d.ts +4 -1
  198. package/dist/steps/agents.js +48 -9
  199. package/dist/steps/auth.js +26 -10
  200. package/dist/steps/cli.d.ts +53 -0
  201. package/dist/steps/cli.js +90 -0
  202. package/dist/steps/commands.d.ts +6 -1
  203. package/dist/steps/commands.js +36 -9
  204. package/dist/steps/detect.d.ts +3 -0
  205. package/dist/steps/detect.js +11 -0
  206. package/dist/steps/mcp.d.ts +6 -2
  207. package/dist/steps/mcp.js +39 -22
  208. package/dist/steps/metrics.d.ts +26 -10
  209. package/dist/steps/metrics.js +108 -108
  210. package/dist/steps/shell.d.ts +2 -0
  211. package/dist/steps/shell.js +26 -9
  212. package/dist/steps/signup.d.ts +7 -4
  213. package/dist/steps/signup.js +29 -20
  214. package/dist/steps/verify.d.ts +2 -2
  215. package/dist/steps/verify.js +118 -112
  216. package/package.json +40 -14
  217. package/assets/agents/docs-validator-agent.md +0 -490
  218. package/assets/agents/release-readiness-agent.md +0 -482
  219. package/assets/commands/agents/aristotle-analyst.md +0 -115
  220. package/assets/commands/agents/aristotle-explorer.md +0 -92
  221. package/assets/commands/agents/aristotle-forecaster.md +0 -114
  222. package/assets/commands/agents/aristotle-validator.md +0 -114
  223. package/assets/commands/agents/prompt-validate.md +0 -135
  224. package/assets/commands/agents/workflow-synthesis.md +0 -101
  225. package/assets/commands/workflows/aristotle.md +0 -543
  226. package/assets/commands/workflows/post-implementation.md +0 -577
  227. package/assets/commands/workflows/pre-implementation.md +0 -670
  228. package/assets/commands/workflows/prompt-audit.md +0 -754
  229. package/assets/commands/workflows/ship.md +0 -721
  230. package/dist/test/auth.test.d.ts +0 -1
  231. package/dist/test/auth.test.js +0 -43
  232. package/dist/test/config-io.test.d.ts +0 -1
  233. package/dist/test/config-io.test.js +0 -56
  234. package/dist/test/config-merger.test.d.ts +0 -1
  235. package/dist/test/config-merger.test.js +0 -94
  236. package/dist/test/detect.test.d.ts +0 -1
  237. package/dist/test/detect.test.js +0 -25
  238. package/dist/test/file-ops.test.d.ts +0 -1
  239. package/dist/test/file-ops.test.js +0 -100
  240. package/dist/test/hash.test.d.ts +0 -1
  241. package/dist/test/hash.test.js +0 -14
  242. package/dist/test/manifest.test.d.ts +0 -1
  243. package/dist/test/manifest.test.js +0 -78
  244. package/dist/test/paths.test.d.ts +0 -1
  245. package/dist/test/paths.test.js +0 -30
  246. package/dist/test/settings-merger.test.d.ts +0 -1
  247. package/dist/test/settings-merger.test.js +0 -167
  248. package/dist/test/shell-profile.test.d.ts +0 -1
  249. package/dist/test/shell-profile.test.js +0 -40
  250. package/dist/test/shell.test.d.ts +0 -1
  251. package/dist/test/shell.test.js +0 -71
  252. package/dist/test/signup.test.d.ts +0 -1
  253. package/dist/test/signup.test.js +0 -83
@@ -1,106 +1,108 @@
1
1
  /**
2
2
  * Metrics Step
3
3
  *
4
- * Installs agent-metrics tool files to ~/.claude/tools/agent-metrics/
5
- * and configures the SubagentStop hook in settings.json for auto-capture.
4
+ * Installs agent-metrics tool files and configures post-agent hooks.
5
+ * Only active for harnesses that support hooks (currently Claude Code only).
6
6
  */
7
- import { mkdir, readdir, copyFile, rm, access } from "node:fs/promises";
7
+ import { mkdir, readdir, copyFile, rm, access, readFile } from "node:fs/promises";
8
8
  import { join } from "node:path";
9
- import { getClaudeHome } from "../lib/paths.js";
10
- import { readSettings, writeSettings, mergeUluopsHook, removeUluopsHook, } from "../lib/settings-merger.js";
11
- /** Where agent-metrics dist files are installed */
12
- export function getMetricsToolDir() {
13
- return join(getClaudeHome(), "tools", "agent-metrics");
9
+ /** Where agent-metrics dist files are installed (derived from profile) */
10
+ function getMetricsToolDir(profile) {
11
+ return profile.paths.toolsDir;
14
12
  }
15
- /** Path to Claude Code's settings.json */
16
- export function getSettingsPath() {
17
- return join(getClaudeHome(), "settings.json");
18
- }
19
- /** The hook command that runs on SubagentStop */
20
- function getHookCommand() {
21
- const toolDir = getMetricsToolDir();
22
- return `node ${join(toolDir, "dist", "hook.js")}`;
13
+ /**
14
+ * The hook command that runs on SubagentStop.
15
+ * @internal Exported for testing only — not part of the public API.
16
+ */
17
+ export function getHookCommand(profile) {
18
+ const toolDir = getMetricsToolDir(profile);
19
+ if (!toolDir)
20
+ throw new Error("No tool dir for this harness");
21
+ const nodePath = process.execPath;
22
+ const hookPath = join(toolDir, "dist", "hook.js");
23
+ if (nodePath.includes('"') || hookPath.includes('"')) {
24
+ throw new Error("Hook command paths must not contain double-quote characters");
25
+ }
26
+ return `"${nodePath}" "${hookPath}"`;
23
27
  }
24
28
  /**
25
29
  * Find the agent-metrics package source directory.
26
30
  * Looks for it as a sibling package in the monorepo or as an npm dependency.
31
+ *
32
+ * Also reads the source package.json's version so the manifest can record
33
+ * which agent-metrics version was actually copied — the shared version
34
+ * ledger across the setup↔agent-metrics seam.
27
35
  */
28
36
  async function findMetricsSource() {
29
- // Try to find the package via Node.js module resolution
30
37
  try {
31
38
  const resolved = import.meta.resolve("@uluops/agent-metrics");
32
39
  // resolved is like file:///path/to/dist/index.js — get the package root
33
40
  const distDir = new URL(".", resolved).pathname;
34
41
  const pkgRoot = join(distDir, "..");
35
- return pkgRoot;
42
+ const version = await readSourceVersion(pkgRoot);
43
+ return { pkgRoot, version };
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ async function readSourceVersion(pkgRoot) {
50
+ try {
51
+ const raw = await readFile(join(pkgRoot, "package.json"), "utf-8");
52
+ const parsed = JSON.parse(raw);
53
+ return typeof parsed.version === "string" ? parsed.version : null;
36
54
  }
37
55
  catch {
38
- // Not installed as dependency
56
+ return null;
39
57
  }
40
- return null;
41
58
  }
42
59
  /**
43
- * Copy agent-metrics dist files to the tool directory.
44
- * Copies all .js files needed for the hook and CLI.
60
+ * Read the installed agent-metrics version from the harness tree.
61
+ * Returns null when the file is missing or unparseable.
62
+ * Exported so verify.ts can detect drift between installed and source.
45
63
  */
46
- async function copyToolFiles(srcRoot, destRoot, dryRun) {
47
- const srcDist = join(srcRoot, "dist");
48
- const destDist = join(destRoot, "dist");
49
- if (!dryRun) {
50
- await mkdir(destDist, { recursive: true });
51
- await mkdir(join(destDist, "commands"), { recursive: true });
52
- await mkdir(join(destDist, "display"), { recursive: true });
53
- }
54
- let filesCopied = 0;
55
- // Copy top-level dist files
56
- const topFiles = await readdir(srcDist);
57
- for (const file of topFiles) {
58
- if (!file.endsWith(".js"))
59
- continue;
60
- if (file.includes(".test."))
61
- continue;
62
- if (file === "test-utils.js")
63
- continue;
64
- if (!dryRun) {
65
- await copyFile(join(srcDist, file), join(destDist, file));
66
- }
67
- filesCopied++;
68
- }
69
- // Copy commands/ subdirectory
64
+ export async function readInstalledMetricsVersion(toolDir) {
65
+ return readSourceVersion(toolDir);
66
+ }
67
+ /** Copy .js files from a source dir to a dest dir, skipping test files. */
68
+ async function copyJsDir(srcDir, destDir, dryRun) {
69
+ let count = 0;
70
70
  try {
71
- const cmdFiles = await readdir(join(srcDist, "commands"));
72
- for (const file of cmdFiles) {
73
- if (!file.endsWith(".js"))
71
+ const files = await readdir(srcDir);
72
+ for (const file of files) {
73
+ if (!file.endsWith(".js") || file.includes(".test.") || file === "test-utils.js")
74
74
  continue;
75
- if (file.includes(".test."))
76
- continue;
77
- if (!dryRun) {
78
- await copyFile(join(srcDist, "commands", file), join(destDist, "commands", file));
79
- }
80
- filesCopied++;
75
+ if (!dryRun)
76
+ await copyFile(join(srcDir, file), join(destDir, file));
77
+ count++;
81
78
  }
82
79
  }
83
80
  catch {
84
- // commands/ doesn't exist — not critical
81
+ // Directory doesn't exist — not critical
85
82
  }
86
- // Copy display/ subdirectory
87
- try {
88
- const dispFiles = await readdir(join(srcDist, "display"));
89
- for (const file of dispFiles) {
90
- if (!file.endsWith(".js"))
91
- continue;
92
- if (file.includes(".test."))
93
- continue;
94
- if (!dryRun) {
95
- await copyFile(join(srcDist, "display", file), join(destDist, "display", file));
96
- }
97
- filesCopied++;
83
+ return count;
84
+ }
85
+ async function copyToolFiles(srcRoot, destRoot, dryRun) {
86
+ const srcDist = join(srcRoot, "dist");
87
+ const destDist = join(destRoot, "dist");
88
+ const subDirs = ["commands", "display"];
89
+ // Replace, don't merge. The previous behavior copied new files over old
90
+ // ones without removing stale entries — if agent-metrics renames or
91
+ // removes a file in a future version, the stale file would persist on
92
+ // disk and shadow the new one. Wipe dist/ before repopulating so the
93
+ // installed tree matches the source tree exactly.
94
+ if (!dryRun) {
95
+ await rm(destDist, { recursive: true, force: true });
96
+ await mkdir(destDist, { recursive: true });
97
+ for (const sub of subDirs) {
98
+ await mkdir(join(destDist, sub), { recursive: true });
98
99
  }
99
100
  }
100
- catch {
101
- // display/ doesn't exist not critical
101
+ let filesCopied = await copyJsDir(srcDist, destDist, dryRun);
102
+ for (const sub of subDirs) {
103
+ filesCopied += await copyJsDir(join(srcDist, sub), join(destDist, sub), dryRun);
102
104
  }
103
- // Copy package.json (needed for CLI bin resolution)
105
+ // Copy package.json (needed for CLI bin resolution and version detection)
104
106
  try {
105
107
  if (!dryRun) {
106
108
  await copyFile(join(srcRoot, "package.json"), join(destRoot, "package.json"));
@@ -113,61 +115,59 @@ async function copyToolFiles(srcRoot, destRoot, dryRun) {
113
115
  return filesCopied;
114
116
  }
115
117
  /**
116
- * Install agent-metrics: copy tool files and configure SubagentStop hook.
118
+ * Install agent-metrics: copy tool files and configure hook.
119
+ * Skips entirely if the harness doesn't support hooks.
117
120
  */
118
- export async function installMetrics(dryRun) {
119
- const toolDir = getMetricsToolDir();
120
- const settingsPath = getSettingsPath();
121
- // Find source package
122
- const srcRoot = await findMetricsSource();
121
+ export async function installMetrics(profile, dryRun) {
122
+ if (!profile.hooks || !profile.paths.toolsDir || !profile.paths.settingsPath) {
123
+ return {
124
+ toolFilesCopied: 0,
125
+ hookConfigured: false,
126
+ hooksInstalledVersion: null,
127
+ skippedReason: "no-hook-support",
128
+ };
129
+ }
130
+ const toolDir = profile.paths.toolsDir;
131
+ const settingsPath = profile.paths.settingsPath;
132
+ const source = await findMetricsSource();
123
133
  let toolFilesCopied = 0;
124
- if (srcRoot) {
125
- // Copy tool files
134
+ if (source) {
126
135
  if (!dryRun) {
127
136
  await mkdir(toolDir, { recursive: true });
128
137
  }
129
- toolFilesCopied = await copyToolFiles(srcRoot, toolDir, dryRun);
138
+ toolFilesCopied = await copyToolFiles(source.pkgRoot, toolDir, dryRun);
130
139
  }
131
- else {
132
- // Check if already installed (from previous run or install.sh)
133
- try {
134
- await access(join(toolDir, "dist", "hook.js"));
135
- }
136
- catch {
137
- // Not found anywhere — skip tool installation, just configure hook
138
- // if files happen to exist
139
- }
140
- }
141
- // Configure hook in settings.json
142
140
  let hookConfigured = false;
143
- if (!dryRun) {
144
- const settings = await readSettings(settingsPath);
145
- const hookCommand = getHookCommand();
146
- const merged = mergeUluopsHook(settings, hookCommand);
147
- await writeSettings(settingsPath, merged);
141
+ const hookJsPath = join(toolDir, "dist", "hook.js");
142
+ const hookJsExists = await access(hookJsPath).then(() => true, () => false);
143
+ if (hookJsExists && !dryRun) {
144
+ const hookCommand = getHookCommand(profile);
145
+ await profile.hooks.install(settingsPath, hookCommand, false);
148
146
  hookConfigured = true;
149
147
  }
150
- else {
148
+ else if (hookJsExists && dryRun) {
151
149
  hookConfigured = true;
152
150
  }
153
- return { toolFilesCopied, hookConfigured };
151
+ // Prefer the source version (from import.meta.resolve); fall back to reading
152
+ // the just-copied package.json from the harness tree so the manifest still
153
+ // records something useful when the source resolution returned null but a
154
+ // prior install left files behind.
155
+ const hooksInstalledVersion = source?.version ?? (hookJsExists ? await readInstalledMetricsVersion(toolDir) : null);
156
+ return { toolFilesCopied, hookConfigured, hooksInstalledVersion };
154
157
  }
155
158
  /**
156
- * Uninstall agent-metrics: remove hook from settings and optionally remove tool files.
159
+ * Uninstall agent-metrics: remove hook and tool files.
157
160
  */
158
- export async function uninstallMetrics(dryRun) {
159
- const settingsPath = getSettingsPath();
160
- const toolDir = getMetricsToolDir();
161
- // Remove hook from settings.json
161
+ export async function uninstallMetrics(profile, dryRun) {
162
+ if (!profile.hooks || !profile.paths.toolsDir || !profile.paths.settingsPath) {
163
+ return;
164
+ }
162
165
  if (!dryRun) {
163
- const settings = await readSettings(settingsPath);
164
- const cleaned = removeUluopsHook(settings);
165
- await writeSettings(settingsPath, cleaned);
166
+ await profile.hooks.remove(profile.paths.settingsPath, false);
166
167
  }
167
- // Remove tool directory
168
168
  if (!dryRun) {
169
169
  try {
170
- await rm(toolDir, { recursive: true, force: true });
170
+ await rm(profile.paths.toolsDir, { recursive: true, force: true });
171
171
  }
172
172
  catch {
173
173
  // Already gone
@@ -1,2 +1,4 @@
1
+ /** Write a fenced ULUOPS_API_KEY export block into the user's shell profile, replacing any existing UluOps block. */
1
2
  export declare function writeShellExport(profilePath: string, apiKey: string, dryRun: boolean): Promise<void>;
3
+ /** Remove the fenced UluOps export block from the user's shell profile. */
2
4
  export declare function removeShellExport(profilePath: string): Promise<void>;
@@ -1,7 +1,17 @@
1
- import { readFile, writeFile } from "node:fs/promises";
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { atomicWrite } from "../lib/atomic-write.js";
4
+ import { backupFile } from "../lib/file-ops.js";
5
+ import { getUluopsDir } from "../lib/paths.js";
2
6
  const FENCE_START = "# --- UluOps (managed by @uluops/setup) ---";
3
7
  const FENCE_END = "# --- /UluOps ---";
8
+ /** Characters safe for shell variable values (no metacharacters). */
9
+ const SAFE_KEY_PATTERN = /^[a-zA-Z0-9_\-\.]+$/;
10
+ /** Write a fenced ULUOPS_API_KEY export block into the user's shell profile, replacing any existing UluOps block. */
4
11
  export async function writeShellExport(profilePath, apiKey, dryRun) {
12
+ if (!SAFE_KEY_PATTERN.test(apiKey)) {
13
+ throw new Error("API key contains characters unsafe for shell export. Only alphanumeric, underscore, hyphen, and dot are allowed.");
14
+ }
5
15
  const block = `${FENCE_START}\nexport ULUOPS_API_KEY="${apiKey}"\n${FENCE_END}`;
6
16
  let content;
7
17
  try {
@@ -9,27 +19,30 @@ export async function writeShellExport(profilePath, apiKey, dryRun) {
9
19
  }
10
20
  catch {
11
21
  if (!dryRun) {
12
- await writeFile(profilePath, block + "\n");
22
+ await atomicWrite(profilePath, block + "\n", { mode: 0o600 });
13
23
  }
14
24
  return;
15
25
  }
16
26
  const startIdx = content.indexOf(FENCE_START);
17
27
  const endIdx = content.indexOf(FENCE_END);
18
- if (startIdx !== -1 && endIdx !== -1) {
19
- // Replace existing fenced block
28
+ if (!dryRun) {
29
+ await backupProfile(profilePath);
30
+ }
31
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
32
+ // Replace existing fenced block (use last FENCE_END after FENCE_START to handle duplicates)
20
33
  const before = content.slice(0, startIdx);
21
34
  const after = content.slice(endIdx + FENCE_END.length);
22
35
  if (!dryRun) {
23
- await writeFile(profilePath, before + block + after);
36
+ await atomicWrite(profilePath, before + block + after);
24
37
  }
25
38
  }
26
39
  else {
27
- // Append
28
40
  if (!dryRun) {
29
- await writeFile(profilePath, content.trimEnd() + "\n\n" + block + "\n");
41
+ await atomicWrite(profilePath, content.trimEnd() + "\n\n" + block + "\n");
30
42
  }
31
43
  }
32
44
  }
45
+ /** Remove the fenced UluOps export block from the user's shell profile. */
33
46
  export async function removeShellExport(profilePath) {
34
47
  let content;
35
48
  try {
@@ -40,9 +53,13 @@ export async function removeShellExport(profilePath) {
40
53
  }
41
54
  const startIdx = content.indexOf(FENCE_START);
42
55
  const endIdx = content.indexOf(FENCE_END);
43
- if (startIdx !== -1 && endIdx !== -1) {
56
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
57
+ await backupProfile(profilePath);
44
58
  const before = content.slice(0, startIdx);
45
59
  const after = content.slice(endIdx + FENCE_END.length);
46
- await writeFile(profilePath, (before + after).replace(/\n{3,}/g, "\n\n"));
60
+ await atomicWrite(profilePath, (before + after).replace(/\n{3,}/g, "\n\n"));
47
61
  }
48
62
  }
63
+ async function backupProfile(profilePath) {
64
+ await backupFile(profilePath, join(getUluopsDir(), "backups", "shell"));
65
+ }
@@ -1,13 +1,16 @@
1
1
  import type { AuthResult } from "./auth.js";
2
2
  /**
3
- * Password complexity rules (matches ops-uluops-api validation).
4
- * Validated client-side for instant feedback before network round-trip.
3
+ * Advisory password hints. Returns warning strings for inquirer display,
4
+ * but all are non-blocking server validation is the authority.
5
+ * @internal Exported for testing only — not part of the public API.
5
6
  */
6
- declare function validatePassword(password: string): string | true;
7
+ declare function hintPassword(password: string): true;
8
+ /** @internal Exported for testing only — not part of the public API. */
7
9
  declare function validateEmail(email: string): string | true;
8
10
  /**
9
11
  * Interactive signup flow: create account + generate API key.
10
12
  * Returns the same AuthResult shape as resolveApiKey for seamless integration.
11
13
  */
12
14
  export declare function signup(): Promise<AuthResult>;
13
- export { validatePassword, validateEmail };
15
+ /** @internal Exported for testing only — not part of the public API. */
16
+ export { hintPassword, validateEmail };
@@ -1,21 +1,23 @@
1
1
  const API_BASE = "https://api.uluops.ai/api/v1";
2
2
  /**
3
- * Password complexity rules (matches ops-uluops-api validation).
4
- * Validated client-side for instant feedback before network round-trip.
3
+ * Advisory password hints. Returns warning strings for inquirer display,
4
+ * but all are non-blocking server validation is the authority.
5
+ * @internal Exported for testing only — not part of the public API.
5
6
  */
6
- function validatePassword(password) {
7
+ function hintPassword(password) {
7
8
  if (password.length < 8)
8
- return "Password must be at least 8 characters";
9
- if (password.length > 128)
10
- return "Password must be at most 128 characters";
11
- if (!/[a-z]/.test(password))
12
- return "Password must include a lowercase letter";
13
- if (!/[A-Z]/.test(password))
14
- return "Password must include an uppercase letter";
15
- if (!/[0-9]/.test(password))
16
- return "Password must include a number";
9
+ console.warn(" Hint: server may require at least 8 characters");
10
+ else if (password.length > 128)
11
+ console.warn(" Hint: server may reject passwords over 128 characters");
12
+ else if (!/[a-z]/.test(password))
13
+ console.warn(" Hint: server may require a lowercase letter");
14
+ else if (!/[A-Z]/.test(password))
15
+ console.warn(" Hint: server may require an uppercase letter");
16
+ else if (!/[0-9]/.test(password))
17
+ console.warn(" Hint: server may require a number");
17
18
  return true;
18
19
  }
20
+ /** @internal Exported for testing only — not part of the public API. */
19
21
  function validateEmail(email) {
20
22
  if (!email.trim())
21
23
  return "Email is required";
@@ -36,19 +38,19 @@ export async function signup() {
36
38
  const pwd = await password({
37
39
  message: "Password",
38
40
  mask: "*",
39
- validate: validatePassword,
41
+ validate: hintPassword,
40
42
  });
41
43
  // Register
42
- const registerRes = await callApi(`${API_BASE}/auth/register`, "POST", { email, password: pwd });
44
+ const registerRes = await callApi(`${API_BASE}/auth/register`, "POST", { email, password: pwd }, undefined, (res) => !!res.data?.sessionToken && typeof res.data.user?.email === "string");
43
45
  const sessionToken = registerRes.data.sessionToken;
44
46
  // Create API key using the session
45
- const keyRes = await callApi(`${API_BASE}/auth/keys`, "POST", { name: "Setup CLI" }, sessionToken);
47
+ const keyRes = await callApi(`${API_BASE}/auth/keys`, "POST", { name: "Setup CLI" }, sessionToken, (res) => !!res.data?.key);
46
48
  return {
47
49
  apiKey: keyRes.data.key,
48
- email: registerRes.data.user.email,
50
+ email: registerRes.data.user?.email ?? email,
49
51
  };
50
52
  }
51
- async function callApi(url, method, body, bearerToken) {
53
+ async function callApi(url, method, body, bearerToken, validate) {
52
54
  const headers = {
53
55
  "Content-Type": "application/json",
54
56
  };
@@ -71,7 +73,14 @@ async function callApi(url, method, body, bearerToken) {
71
73
  throw err;
72
74
  }
73
75
  if (res.ok) {
74
- return (await res.json());
76
+ const body = await res.json();
77
+ if (typeof body !== "object" || body === null) {
78
+ throw new Error("Unexpected API response shape");
79
+ }
80
+ if (validate && !validate(body)) {
81
+ throw new Error("API response failed structural validation");
82
+ }
83
+ return body;
75
84
  }
76
85
  // Handle known error codes
77
86
  const errorBody = await res.json().catch(() => null);
@@ -88,5 +97,5 @@ async function callApi(url, method, body, bearerToken) {
88
97
  }
89
98
  throw new Error(`Signup failed (${res.status}): ${message}`);
90
99
  }
91
- // Exported for testing
92
- export { validatePassword, validateEmail };
100
+ /** @internal Exported for testing only — not part of the public API. */
101
+ export { hintPassword, validateEmail };
@@ -1,4 +1,4 @@
1
- interface VerifyResult {
1
+ export interface VerifyResult {
2
2
  ok: boolean;
3
3
  checks: {
4
4
  label: string;
@@ -6,5 +6,5 @@ interface VerifyResult {
6
6
  detail?: string;
7
7
  }[];
8
8
  }
9
+ /** Run all verification checks against the current installation and return structured results. */
9
10
  export declare function verify(): Promise<VerifyResult>;
10
- export {};