@uluops/setup 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -53
- package/assets/agents/anxiety-reader-agent.md +464 -0
- package/assets/commands/agents/anxiety-reader.md +160 -0
- package/assets/commands/agents/api-contract.md +1 -0
- package/assets/commands/agents/architect.md +1 -0
- package/assets/commands/agents/aristotle-analyst.md +1 -0
- package/assets/commands/agents/aristotle-explorer.md +1 -0
- package/assets/commands/agents/aristotle-forecaster.md +1 -0
- package/assets/commands/agents/aristotle-validator.md +1 -0
- package/assets/commands/agents/assumption-excavator.md +1 -0
- package/assets/commands/agents/audit.md +1 -0
- package/assets/commands/agents/{validate.md → code-validate.md} +6 -5
- package/assets/commands/agents/docs-validate.md +1 -0
- package/assets/commands/agents/frontend.md +1 -0
- package/assets/commands/agents/mcp-validate.md +1 -0
- package/assets/commands/agents/optimize.md +1 -0
- package/assets/commands/agents/pattern-analyzer.md +1 -0
- package/assets/commands/agents/prompt-quality.md +1 -0
- package/assets/commands/agents/prompt-validate.md +1 -0
- package/assets/commands/agents/public-interface.md +1 -0
- package/assets/commands/agents/release.md +1 -0
- package/assets/commands/agents/security.md +1 -0
- package/assets/commands/agents/test-review.md +1 -0
- package/assets/commands/agents/type-safety.md +1 -0
- package/assets/commands/agents/workflow-synthesis.md +1 -0
- package/assets/commands/pipelines/aristotle.md +143 -0
- package/assets/commands/pipelines/ship.md +188 -0
- package/assets/commands/workflows/prompt-audit.md +37 -747
- package/dist/cli.js +251 -207
- package/dist/harnesses/claude-code.d.ts +8 -0
- package/dist/harnesses/claude-code.js +72 -0
- package/dist/harnesses/codex.d.ts +15 -0
- package/dist/harnesses/codex.js +53 -0
- package/dist/harnesses/gemini-cli.d.ts +16 -0
- package/dist/harnesses/gemini-cli.js +54 -0
- package/dist/harnesses/index.d.ts +18 -0
- package/dist/harnesses/index.js +45 -0
- package/dist/harnesses/opencode.d.ts +14 -0
- package/dist/harnesses/opencode.js +130 -0
- package/dist/harnesses/types.d.ts +87 -0
- package/dist/harnesses/types.js +24 -0
- package/dist/lib/agent-transform.d.ts +12 -0
- package/dist/lib/agent-transform.js +129 -0
- package/dist/lib/asset-catalog.d.ts +9 -0
- package/dist/lib/asset-catalog.js +56 -0
- package/dist/lib/atomic-write.d.ts +11 -0
- package/dist/lib/atomic-write.js +28 -0
- package/dist/lib/config-merger.d.ts +7 -1
- package/dist/lib/config-merger.js +34 -5
- package/dist/lib/display.d.ts +14 -0
- package/dist/lib/display.js +66 -0
- package/dist/lib/file-ops.d.ts +6 -0
- package/dist/lib/file-ops.js +22 -1
- package/dist/lib/hash.d.ts +1 -0
- package/dist/lib/hash.js +1 -0
- package/dist/lib/health.d.ts +2 -0
- package/dist/lib/health.js +10 -0
- package/dist/lib/manifest.d.ts +22 -5
- package/dist/lib/manifest.js +148 -13
- package/dist/lib/paths.d.ts +15 -3
- package/dist/lib/paths.js +71 -13
- package/dist/lib/settings-merger.d.ts +9 -1
- package/dist/lib/settings-merger.js +45 -17
- package/dist/steps/agents.d.ts +5 -1
- package/dist/steps/agents.js +59 -9
- package/dist/steps/auth.js +26 -10
- package/dist/steps/commands.d.ts +6 -1
- package/dist/steps/commands.js +87 -9
- package/dist/steps/detect.d.ts +3 -0
- package/dist/steps/detect.js +7 -0
- package/dist/steps/mcp.d.ts +6 -2
- package/dist/steps/mcp.js +46 -21
- package/dist/steps/metrics.d.ts +14 -10
- package/dist/steps/metrics.js +59 -89
- package/dist/steps/shell.d.ts +2 -0
- package/dist/steps/shell.js +16 -9
- package/dist/steps/signup.d.ts +6 -3
- package/dist/steps/signup.js +26 -14
- package/dist/steps/verify.d.ts +2 -2
- package/dist/steps/verify.js +84 -117
- package/package.json +32 -7
- package/assets/commands/workflows/aristotle.md +0 -543
- package/assets/commands/workflows/ship.md +0 -721
- package/dist/test/auth.test.d.ts +0 -1
- package/dist/test/auth.test.js +0 -43
- package/dist/test/config-io.test.d.ts +0 -1
- package/dist/test/config-io.test.js +0 -56
- package/dist/test/config-merger.test.d.ts +0 -1
- package/dist/test/config-merger.test.js +0 -94
- package/dist/test/detect.test.d.ts +0 -1
- package/dist/test/detect.test.js +0 -25
- package/dist/test/file-ops.test.d.ts +0 -1
- package/dist/test/file-ops.test.js +0 -100
- package/dist/test/hash.test.d.ts +0 -1
- package/dist/test/hash.test.js +0 -14
- package/dist/test/manifest.test.d.ts +0 -1
- package/dist/test/manifest.test.js +0 -78
- package/dist/test/paths.test.d.ts +0 -1
- package/dist/test/paths.test.js +0 -30
- package/dist/test/settings-merger.test.d.ts +0 -1
- package/dist/test/settings-merger.test.js +0 -167
- package/dist/test/shell-profile.test.d.ts +0 -1
- package/dist/test/shell-profile.test.js +0 -40
- package/dist/test/shell.test.d.ts +0 -1
- package/dist/test/shell.test.js +0 -71
- package/dist/test/signup.test.d.ts +0 -1
- package/dist/test/signup.test.js +0 -83
package/dist/lib/paths.js
CHANGED
|
@@ -1,33 +1,91 @@
|
|
|
1
1
|
import { homedir, platform } from "node:os";
|
|
2
|
-
import { join, dirname } from "node:path";
|
|
2
|
+
import { join, dirname, isAbsolute } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { access } from "node:fs/promises";
|
|
4
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
6
|
/** Root of the npm package (where assets/ lives) */
|
|
6
7
|
export const PACKAGE_ROOT = join(__dirname, "..", "..");
|
|
7
8
|
/** Assets directory containing pre-rendered .md files */
|
|
8
9
|
export const ASSETS_DIR = join(PACKAGE_ROOT, "assets");
|
|
10
|
+
/** Explicit project root override via --project-root flag or env var. */
|
|
11
|
+
let projectRootOverride = null;
|
|
12
|
+
export function setProjectRoot(path) {
|
|
13
|
+
projectRootOverride = path;
|
|
14
|
+
}
|
|
15
|
+
/** Walk upward from cwd to find the nearest directory containing .git or package.json. Falls back to cwd. */
|
|
16
|
+
export async function findProjectRoot() {
|
|
17
|
+
if (projectRootOverride)
|
|
18
|
+
return projectRootOverride;
|
|
19
|
+
const envRoot = process.env["ULUOPS_PROJECT_ROOT"];
|
|
20
|
+
if (envRoot) {
|
|
21
|
+
if (!isAbsolute(envRoot)) {
|
|
22
|
+
throw new Error(`ULUOPS_PROJECT_ROOT must be an absolute path, got: ${envRoot}`);
|
|
23
|
+
}
|
|
24
|
+
if (envRoot.includes("..")) {
|
|
25
|
+
throw new Error(`ULUOPS_PROJECT_ROOT must not contain traversal sequences: ${envRoot}`);
|
|
26
|
+
}
|
|
27
|
+
return envRoot;
|
|
28
|
+
}
|
|
29
|
+
let dir = process.cwd();
|
|
30
|
+
const root = dirname(dir);
|
|
31
|
+
while (dir !== root) {
|
|
32
|
+
if (await isProjectMarker(dir))
|
|
33
|
+
return dir;
|
|
34
|
+
dir = dirname(dir);
|
|
35
|
+
}
|
|
36
|
+
if (await isProjectMarker(dir))
|
|
37
|
+
return dir;
|
|
38
|
+
return process.cwd();
|
|
39
|
+
}
|
|
40
|
+
async function isProjectMarker(dir) {
|
|
41
|
+
try {
|
|
42
|
+
await access(join(dir, ".git"));
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// continue
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
await access(join(dir, "package.json"));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// continue
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/** Return the Claude config home directory (~/.claude by default, or CLAUDE_HOME env override). */
|
|
9
58
|
export function getClaudeHome() {
|
|
10
|
-
return join(homedir(), ".claude");
|
|
59
|
+
return process.env["CLAUDE_HOME"] ?? join(homedir(), ".claude");
|
|
11
60
|
}
|
|
61
|
+
/** Return the path to Claude's global config file (~/.claude.json by default, or CLAUDE_JSON_PATH env override). */
|
|
12
62
|
export function getClaudeJsonPath() {
|
|
63
|
+
const envPath = process.env["CLAUDE_JSON_PATH"];
|
|
64
|
+
if (envPath)
|
|
65
|
+
return envPath;
|
|
13
66
|
return join(homedir(), ".claude.json");
|
|
14
67
|
}
|
|
15
|
-
|
|
16
|
-
|
|
68
|
+
/** Return the path to the project-local MCP config file (.mcp.json in project root). */
|
|
69
|
+
export async function getLocalMcpPath() {
|
|
70
|
+
return join(await findProjectRoot(), ".mcp.json");
|
|
17
71
|
}
|
|
72
|
+
/** Return the UluOps state directory (~/.uluops/). Harness-neutral. */
|
|
73
|
+
export function getUluopsDir() {
|
|
74
|
+
return join(homedir(), ".uluops");
|
|
75
|
+
}
|
|
76
|
+
/** Return the path to the UluOps install manifest file (new location). */
|
|
18
77
|
export function getManifestPath() {
|
|
19
|
-
return join(
|
|
78
|
+
return join(getUluopsDir(), "manifest.json");
|
|
20
79
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return join(getClaudeHome(), "agents");
|
|
80
|
+
/** Return the legacy manifest path for migration. */
|
|
81
|
+
export function getLegacyManifestPath() {
|
|
82
|
+
return join(getClaudeHome(), "uluops-manifest.json");
|
|
25
83
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return join(getClaudeHome(), "commands");
|
|
84
|
+
/** Return the backup directory for a harness's config files. */
|
|
85
|
+
export function getBackupDir(harnessName) {
|
|
86
|
+
return join(getUluopsDir(), "backups", harnessName);
|
|
30
87
|
}
|
|
88
|
+
/** Detect the user's shell and return its name and profile path, or null if unsupported. */
|
|
31
89
|
export function getShellProfile() {
|
|
32
90
|
const shell = process.env["SHELL"] ?? "";
|
|
33
91
|
const home = homedir();
|
|
@@ -13,13 +13,21 @@ interface HookMatcher {
|
|
|
13
13
|
matcher?: string;
|
|
14
14
|
hooks: HookEntry[];
|
|
15
15
|
}
|
|
16
|
-
interface ClaudeSettings {
|
|
16
|
+
export interface ClaudeSettings {
|
|
17
17
|
permissions?: Record<string, unknown>;
|
|
18
18
|
hooks?: Record<string, HookMatcher[]>;
|
|
19
19
|
[key: string]: unknown;
|
|
20
20
|
}
|
|
21
|
+
export interface HookProbeResult {
|
|
22
|
+
hookType: string;
|
|
23
|
+
supported: boolean;
|
|
24
|
+
warning?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Check whether the configured hook event type is in the known supported set. Returns the resolved hook type and a warning if unsupported. */
|
|
27
|
+
export declare function probeHookSupport(): HookProbeResult;
|
|
21
28
|
/**
|
|
22
29
|
* Read an existing settings.json, or return empty object if it doesn't exist.
|
|
30
|
+
* Throws on malformed JSON to prevent silent data loss during merge+write.
|
|
23
31
|
*/
|
|
24
32
|
export declare function readSettings(path: string): Promise<ClaudeSettings>;
|
|
25
33
|
/**
|
|
@@ -4,37 +4,63 @@
|
|
|
4
4
|
* Safe read/merge/remove for Claude Code's settings.json.
|
|
5
5
|
* Only touches UluOps-managed hook entries — all other settings preserved.
|
|
6
6
|
*/
|
|
7
|
-
import { readFile
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { atomicWrite } from "./atomic-write.js";
|
|
8
9
|
/** Marker embedded in hook commands to identify UluOps-managed entries */
|
|
9
10
|
const ULUOPS_HOOK_MARKER = "tools/agent-metrics";
|
|
11
|
+
/** Supported hook event types in Claude Code. Update when Claude Code adds/renames types. */
|
|
12
|
+
const SUPPORTED_HOOK_TYPES = new Set([
|
|
13
|
+
"SubagentStop",
|
|
14
|
+
"PreToolUse",
|
|
15
|
+
"PostToolUse",
|
|
16
|
+
"Notification",
|
|
17
|
+
"Stop",
|
|
18
|
+
]);
|
|
19
|
+
/** Configurable hook type via env var. Falls back to SubagentStop. */
|
|
20
|
+
function getHookEventType() {
|
|
21
|
+
return process.env["ULUOPS_HOOK_TYPE"] ?? "SubagentStop";
|
|
22
|
+
}
|
|
23
|
+
/** Check whether the configured hook event type is in the known supported set. Returns the resolved hook type and a warning if unsupported. */
|
|
24
|
+
export function probeHookSupport() {
|
|
25
|
+
const hookType = getHookEventType();
|
|
26
|
+
if (SUPPORTED_HOOK_TYPES.has(hookType)) {
|
|
27
|
+
return { hookType, supported: true };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
hookType,
|
|
31
|
+
supported: false,
|
|
32
|
+
warning: `Hook type "${hookType}" is not in the known supported set {${[...SUPPORTED_HOOK_TYPES].join(", ")}}. Metrics may silently fail if this hook type does not exist in Claude Code.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
10
35
|
/**
|
|
11
36
|
* Read an existing settings.json, or return empty object if it doesn't exist.
|
|
37
|
+
* Throws on malformed JSON to prevent silent data loss during merge+write.
|
|
12
38
|
*/
|
|
13
39
|
export async function readSettings(path) {
|
|
40
|
+
let raw;
|
|
14
41
|
try {
|
|
15
|
-
|
|
16
|
-
return JSON.parse(raw);
|
|
42
|
+
raw = await readFile(path, "utf-8");
|
|
17
43
|
}
|
|
18
44
|
catch {
|
|
19
|
-
return {};
|
|
45
|
+
return {}; // File doesn't exist — fresh config
|
|
20
46
|
}
|
|
47
|
+
return JSON.parse(raw);
|
|
21
48
|
}
|
|
22
49
|
/**
|
|
23
50
|
* Write settings back to file with stable formatting.
|
|
24
51
|
*/
|
|
25
52
|
export async function writeSettings(path, settings) {
|
|
26
|
-
await
|
|
53
|
+
await atomicWrite(path, JSON.stringify(settings, null, 2) + "\n");
|
|
27
54
|
}
|
|
28
55
|
/**
|
|
29
56
|
* Merge the UluOps SubagentStop hook into settings, preserving all other
|
|
30
57
|
* hooks and settings. If a UluOps hook already exists, it is replaced.
|
|
31
58
|
*/
|
|
32
59
|
export function mergeUluopsHook(settings, hookCommand) {
|
|
60
|
+
const hookType = getHookEventType();
|
|
33
61
|
const hooks = settings.hooks ?? {};
|
|
34
|
-
const existing = hooks[
|
|
35
|
-
// Remove any existing UluOps hook entries
|
|
62
|
+
const existing = hooks[hookType] ?? [];
|
|
36
63
|
const filtered = existing.filter((m) => !m.hooks.some((h) => h.command.includes(ULUOPS_HOOK_MARKER)));
|
|
37
|
-
// Add the new UluOps hook
|
|
38
64
|
const uluopsHook = {
|
|
39
65
|
hooks: [
|
|
40
66
|
{
|
|
@@ -48,7 +74,7 @@ export function mergeUluopsHook(settings, hookCommand) {
|
|
|
48
74
|
...settings,
|
|
49
75
|
hooks: {
|
|
50
76
|
...hooks,
|
|
51
|
-
|
|
77
|
+
[hookType]: [...filtered, uluopsHook],
|
|
52
78
|
},
|
|
53
79
|
};
|
|
54
80
|
}
|
|
@@ -57,19 +83,20 @@ export function mergeUluopsHook(settings, hookCommand) {
|
|
|
57
83
|
* the key is removed. If hooks becomes empty, the key is removed.
|
|
58
84
|
*/
|
|
59
85
|
export function removeUluopsHook(settings) {
|
|
86
|
+
const hookType = getHookEventType();
|
|
60
87
|
const hooks = settings.hooks;
|
|
61
88
|
if (!hooks)
|
|
62
89
|
return settings;
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
90
|
+
const hookEntries = hooks[hookType];
|
|
91
|
+
if (!hookEntries)
|
|
65
92
|
return settings;
|
|
66
|
-
const filtered =
|
|
93
|
+
const filtered = hookEntries.filter((m) => !m.hooks.some((h) => h.command.includes(ULUOPS_HOOK_MARKER)));
|
|
67
94
|
const updatedHooks = { ...hooks };
|
|
68
95
|
if (filtered.length === 0) {
|
|
69
|
-
delete updatedHooks[
|
|
96
|
+
delete updatedHooks[hookType];
|
|
70
97
|
}
|
|
71
98
|
else {
|
|
72
|
-
updatedHooks[
|
|
99
|
+
updatedHooks[hookType] = filtered;
|
|
73
100
|
}
|
|
74
101
|
const result = { ...settings };
|
|
75
102
|
if (Object.keys(updatedHooks).length === 0) {
|
|
@@ -84,8 +111,9 @@ export function removeUluopsHook(settings) {
|
|
|
84
111
|
* Check if a UluOps hook is configured in settings.
|
|
85
112
|
*/
|
|
86
113
|
export function hasUluopsHook(settings) {
|
|
87
|
-
const
|
|
88
|
-
|
|
114
|
+
const hookType = getHookEventType();
|
|
115
|
+
const hookEntries = settings.hooks?.[hookType];
|
|
116
|
+
if (!hookEntries)
|
|
89
117
|
return false;
|
|
90
|
-
return
|
|
118
|
+
return hookEntries.some((m) => m.hooks.some((h) => h.command.includes(ULUOPS_HOOK_MARKER)));
|
|
91
119
|
}
|
package/dist/steps/agents.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type { HarnessProfile } from "../harnesses/index.js";
|
|
1
2
|
export interface AgentsResult {
|
|
2
3
|
copied: number;
|
|
3
4
|
skipped: number;
|
|
4
5
|
removed: number;
|
|
5
6
|
files: string[];
|
|
6
7
|
}
|
|
7
|
-
|
|
8
|
+
/** Copy agent definition files from assets to the harness agents directory,
|
|
9
|
+
* transforming frontmatter to match the target harness format. */
|
|
10
|
+
export declare function installAgents(profile: HarnessProfile, localDefs: boolean, dryRun: boolean, existingManifestAgents?: string[]): Promise<AgentsResult>;
|
|
11
|
+
/** Remove previously installed agent files by name. */
|
|
8
12
|
export declare function uninstallAgents(files: string[], defsPath: string): Promise<number>;
|
package/dist/steps/agents.js
CHANGED
|
@@ -1,14 +1,64 @@
|
|
|
1
|
+
import { readFile, readdir, mkdir, unlink } from "node:fs/promises";
|
|
1
2
|
import { join } from "node:path";
|
|
2
|
-
import { ASSETS_DIR,
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import { ASSETS_DIR, findProjectRoot } from "../lib/paths.js";
|
|
4
|
+
import { copyIfChanged, writeIfChanged, unlinkFiles } from "../lib/file-ops.js";
|
|
5
|
+
import { transformAgent } from "../lib/agent-transform.js";
|
|
6
|
+
/** Source directory for agent assets (single set, Claude Code format). */
|
|
7
|
+
const AGENTS_SRC = join(ASSETS_DIR, "agents");
|
|
8
|
+
/** Copy agent definition files from assets to the harness agents directory,
|
|
9
|
+
* transforming frontmatter to match the target harness format. */
|
|
10
|
+
export async function installAgents(profile, localDefs, dryRun, existingManifestAgents) {
|
|
11
|
+
const destDir = localDefs
|
|
12
|
+
? join(await findProjectRoot(), "uluops", "agents")
|
|
13
|
+
: profile.paths.agentsDir;
|
|
14
|
+
if (!dryRun) {
|
|
15
|
+
await mkdir(destDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
const needsTransform = profile.name !== "claude-code";
|
|
18
|
+
let files;
|
|
19
|
+
try {
|
|
20
|
+
files = (await readdir(AGENTS_SRC)).filter((f) => f.endsWith(".md"));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { copied: 0, skipped: 0, removed: 0, files: [] };
|
|
24
|
+
}
|
|
25
|
+
let copied = 0;
|
|
26
|
+
let skipped = 0;
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
let result;
|
|
29
|
+
if (needsTransform) {
|
|
30
|
+
const markdown = await readFile(join(AGENTS_SRC, file), "utf-8");
|
|
31
|
+
const transformed = transformAgent(markdown, profile.name);
|
|
32
|
+
result = await writeIfChanged(join(destDir, file), transformed, dryRun);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
result = await copyIfChanged(join(AGENTS_SRC, file), join(destDir, file), dryRun);
|
|
36
|
+
}
|
|
37
|
+
if (result === "copied")
|
|
38
|
+
copied++;
|
|
39
|
+
else
|
|
40
|
+
skipped++;
|
|
41
|
+
}
|
|
42
|
+
// Remove files that were in the old manifest but no longer in the package
|
|
43
|
+
let removed = 0;
|
|
44
|
+
if (existingManifestAgents) {
|
|
45
|
+
for (const oldFile of existingManifestAgents) {
|
|
46
|
+
if (!files.includes(oldFile)) {
|
|
47
|
+
if (!dryRun) {
|
|
48
|
+
try {
|
|
49
|
+
await unlink(join(destDir, oldFile));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Already gone
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
removed++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { copied, skipped, removed, files };
|
|
11
60
|
}
|
|
61
|
+
/** Remove previously installed agent files by name. */
|
|
12
62
|
export async function uninstallAgents(files, defsPath) {
|
|
13
63
|
return unlinkFiles(join(defsPath, "agents"), files);
|
|
14
64
|
}
|
package/dist/steps/auth.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
function getKeyPrefix() {
|
|
5
|
+
return process.env["ULUOPS_KEY_PREFIX"] ?? "ulr_";
|
|
6
|
+
}
|
|
4
7
|
/**
|
|
5
8
|
* Resolve API key from flags, env, credentials file, or interactive prompt.
|
|
6
9
|
*/
|
|
@@ -20,20 +23,22 @@ export async function resolveApiKey(options) {
|
|
|
20
23
|
if (!options.interactive) {
|
|
21
24
|
throw new Error("No API key found. Pass --api-key or set ULUOPS_API_KEY. Get one at app.uluops.ai/settings/api-keys");
|
|
22
25
|
}
|
|
26
|
+
const prefix = getKeyPrefix();
|
|
23
27
|
const { input } = await import("@inquirer/prompts");
|
|
24
28
|
apiKey = await input({
|
|
25
29
|
message: "Enter your UluOps API key",
|
|
26
30
|
validate: (val) => {
|
|
27
31
|
if (!val.trim())
|
|
28
32
|
return "Get a key at app.uluops.ai/settings/api-keys";
|
|
29
|
-
if (!val.startsWith(
|
|
30
|
-
return
|
|
33
|
+
if (!val.startsWith(prefix))
|
|
34
|
+
return `API keys typically start with ${prefix} — if your key has a different format, just paste it and server validation will check it`;
|
|
31
35
|
return true;
|
|
32
36
|
},
|
|
33
37
|
});
|
|
34
38
|
}
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
const prefix = getKeyPrefix();
|
|
40
|
+
if (!apiKey.startsWith(prefix) && !options.skipValidation) {
|
|
41
|
+
process.stderr.write(` ⚠ Key does not start with expected prefix "${prefix}" — proceeding with server validation\n`);
|
|
37
42
|
}
|
|
38
43
|
// Validate against server
|
|
39
44
|
if (!options.skipValidation) {
|
|
@@ -43,22 +48,33 @@ export async function resolveApiKey(options) {
|
|
|
43
48
|
return { apiKey, email: null };
|
|
44
49
|
}
|
|
45
50
|
async function readCredentialsFile() {
|
|
51
|
+
const credsPath = join(homedir(), ".uluops", "credentials.json");
|
|
52
|
+
let raw;
|
|
46
53
|
try {
|
|
47
|
-
|
|
48
|
-
const raw = await readFile(credsPath, "utf-8");
|
|
49
|
-
const creds = JSON.parse(raw);
|
|
50
|
-
const defaultProfile = creds["default"];
|
|
51
|
-
return defaultProfile?.apiKey ?? defaultProfile?.api_key;
|
|
54
|
+
raw = await readFile(credsPath, "utf-8");
|
|
52
55
|
}
|
|
53
56
|
catch {
|
|
54
|
-
return undefined;
|
|
57
|
+
return undefined; // File doesn't exist
|
|
58
|
+
}
|
|
59
|
+
let creds;
|
|
60
|
+
try {
|
|
61
|
+
creds = JSON.parse(raw);
|
|
55
62
|
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
throw new Error(`Malformed credentials file at ${credsPath}: ${err instanceof Error ? err.message : "invalid JSON"}`);
|
|
65
|
+
}
|
|
66
|
+
if (typeof creds !== "object" || creds === null)
|
|
67
|
+
return undefined;
|
|
68
|
+
const profiles = creds;
|
|
69
|
+
const defaultProfile = profiles["default"];
|
|
70
|
+
return defaultProfile?.apiKey ?? defaultProfile?.api_key;
|
|
56
71
|
}
|
|
57
72
|
async function validateKey(apiKey) {
|
|
58
73
|
const url = "https://api.uluops.ai/api/v1/registry/users/me";
|
|
59
74
|
try {
|
|
60
75
|
const res = await fetch(url, {
|
|
61
76
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
77
|
+
signal: AbortSignal.timeout(15000),
|
|
62
78
|
});
|
|
63
79
|
if (res.status === 401) {
|
|
64
80
|
throw new Error("Invalid API key — generate a new one at app.uluops.ai/settings/api-keys");
|
package/dist/steps/commands.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import type { HarnessProfile } from "../harnesses/index.js";
|
|
1
2
|
export interface CommandsResult {
|
|
2
3
|
agentCommands: number;
|
|
3
4
|
workflowCommands: number;
|
|
5
|
+
pipelineCommands: number;
|
|
4
6
|
skipped: number;
|
|
5
7
|
removed: number;
|
|
6
8
|
files: string[];
|
|
9
|
+
skippedReason?: string;
|
|
7
10
|
}
|
|
8
|
-
|
|
11
|
+
/** Install slash-command files, transforming to target format as needed. */
|
|
12
|
+
export declare function installCommands(profile: HarnessProfile, localDefs: boolean, dryRun: boolean, existingManifestCommands?: string[]): Promise<CommandsResult>;
|
|
13
|
+
/** Remove previously installed command files by relative path. Returns count of successfully removed files. */
|
|
9
14
|
export declare function uninstallCommands(files: string[], defsPath: string): Promise<number>;
|
package/dist/steps/commands.js
CHANGED
|
@@ -1,13 +1,75 @@
|
|
|
1
|
-
import { readdir, mkdir, unlink } from "node:fs/promises";
|
|
1
|
+
import { readFile, readdir, mkdir, unlink } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { ASSETS_DIR,
|
|
4
|
-
import { copyIfChanged } from "../lib/file-ops.js";
|
|
5
|
-
const SUBDIRS = ["agents", "workflows"];
|
|
6
|
-
|
|
3
|
+
import { ASSETS_DIR, findProjectRoot } from "../lib/paths.js";
|
|
4
|
+
import { copyIfChanged, writeIfChanged } from "../lib/file-ops.js";
|
|
5
|
+
const SUBDIRS = ["agents", "workflows", "pipelines"];
|
|
6
|
+
/** Harnesses that support command installation. */
|
|
7
|
+
const SUPPORTED_HARNESSES = new Set(["claude-code", "gemini-cli"]);
|
|
8
|
+
// --- Gemini CLI transform ---
|
|
9
|
+
/**
|
|
10
|
+
* Strip YAML frontmatter from rendered markdown, returning just the body.
|
|
11
|
+
*/
|
|
12
|
+
function stripFrontmatter(markdown) {
|
|
13
|
+
const first = markdown.indexOf("---");
|
|
14
|
+
if (first === -1)
|
|
15
|
+
return markdown;
|
|
16
|
+
const second = markdown.indexOf("---", first + 3);
|
|
17
|
+
if (second === -1)
|
|
18
|
+
return markdown;
|
|
19
|
+
return markdown.substring(second + 3);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Escape a string for use in a TOML basic string (double-quoted).
|
|
23
|
+
*/
|
|
24
|
+
function escapeToml(value) {
|
|
25
|
+
return value
|
|
26
|
+
.replace(/\\/g, "\\\\")
|
|
27
|
+
.replace(/"/g, '\\"')
|
|
28
|
+
.replace(/\n/g, "\\n")
|
|
29
|
+
.replace(/\r/g, "\\r")
|
|
30
|
+
.replace(/\t/g, "\\t");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Transform a Claude Code markdown command to a Gemini CLI TOML command.
|
|
34
|
+
* Strips frontmatter, substitutes $ARGUMENTS → {{args}}, wraps in TOML.
|
|
35
|
+
*/
|
|
36
|
+
function transformToGeminiToml(markdown, description) {
|
|
37
|
+
const body = stripFrontmatter(markdown)
|
|
38
|
+
.replace(/\$ARGUMENTS/g, "{{args}}")
|
|
39
|
+
.trim();
|
|
40
|
+
// Escape """ in body for TOML multi-line strings
|
|
41
|
+
const escaped = body.replace(/"""/g, '""\\"');
|
|
42
|
+
return `description = "${escapeToml(description)}"\nprompt = """\n${escaped}\n"""\n`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Extract the description from a markdown command's YAML frontmatter.
|
|
46
|
+
*/
|
|
47
|
+
function extractDescription(markdown) {
|
|
48
|
+
const match = markdown.match(/^description:\s*(.+)$/m);
|
|
49
|
+
return match?.[1]?.trim() ?? "";
|
|
50
|
+
}
|
|
51
|
+
// --- Install ---
|
|
52
|
+
/** Install slash-command files, transforming to target format as needed. */
|
|
53
|
+
export async function installCommands(profile, localDefs, dryRun, existingManifestCommands) {
|
|
54
|
+
if (!SUPPORTED_HARNESSES.has(profile.name)) {
|
|
55
|
+
return {
|
|
56
|
+
agentCommands: 0,
|
|
57
|
+
workflowCommands: 0,
|
|
58
|
+
pipelineCommands: 0,
|
|
59
|
+
skipped: 0,
|
|
60
|
+
removed: 0,
|
|
61
|
+
files: [],
|
|
62
|
+
skippedReason: "not-supported",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
7
65
|
const srcBase = join(ASSETS_DIR, "commands");
|
|
8
|
-
const destBase =
|
|
66
|
+
const destBase = localDefs
|
|
67
|
+
? join(await findProjectRoot(), "uluops", "commands")
|
|
68
|
+
: profile.paths.commandsDir;
|
|
69
|
+
const needsTransform = profile.name === "gemini-cli";
|
|
9
70
|
let agentCommands = 0;
|
|
10
71
|
let workflowCommands = 0;
|
|
72
|
+
let pipelineCommands = 0;
|
|
11
73
|
let skipped = 0;
|
|
12
74
|
const allFiles = [];
|
|
13
75
|
for (const subdir of SUBDIRS) {
|
|
@@ -24,13 +86,27 @@ export async function installCommands(localDefs, dryRun, existingManifestCommand
|
|
|
24
86
|
continue;
|
|
25
87
|
}
|
|
26
88
|
for (const file of files) {
|
|
27
|
-
const
|
|
28
|
-
|
|
89
|
+
const destFile = needsTransform
|
|
90
|
+
? file.replace(/\.md$/, ".toml")
|
|
91
|
+
: file;
|
|
92
|
+
const relativePath = `${subdir}/${destFile}`;
|
|
93
|
+
let result;
|
|
94
|
+
if (needsTransform) {
|
|
95
|
+
const markdown = await readFile(join(srcDir, file), "utf-8");
|
|
96
|
+
const description = extractDescription(markdown);
|
|
97
|
+
const toml = transformToGeminiToml(markdown, description);
|
|
98
|
+
result = await writeIfChanged(join(destDir, destFile), toml, dryRun);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
result = await copyIfChanged(join(srcDir, file), join(destDir, destFile), dryRun);
|
|
102
|
+
}
|
|
29
103
|
if (result === "copied") {
|
|
30
104
|
if (subdir === "agents")
|
|
31
105
|
agentCommands++;
|
|
32
|
-
else
|
|
106
|
+
else if (subdir === "workflows")
|
|
33
107
|
workflowCommands++;
|
|
108
|
+
else
|
|
109
|
+
pipelineCommands++;
|
|
34
110
|
}
|
|
35
111
|
else {
|
|
36
112
|
skipped++;
|
|
@@ -58,11 +134,13 @@ export async function installCommands(localDefs, dryRun, existingManifestCommand
|
|
|
58
134
|
return {
|
|
59
135
|
agentCommands,
|
|
60
136
|
workflowCommands,
|
|
137
|
+
pipelineCommands,
|
|
61
138
|
skipped,
|
|
62
139
|
removed,
|
|
63
140
|
files: allFiles,
|
|
64
141
|
};
|
|
65
142
|
}
|
|
143
|
+
/** Remove previously installed command files by relative path. Returns count of successfully removed files. */
|
|
66
144
|
export async function uninstallCommands(files, defsPath) {
|
|
67
145
|
const { unlinkFiles } = await import("../lib/file-ops.js");
|
|
68
146
|
return unlinkFiles(join(defsPath, "commands"), files);
|
package/dist/steps/detect.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HarnessProfile } from "../harnesses/index.js";
|
|
1
2
|
export interface Environment {
|
|
2
3
|
os: "linux" | "darwin" | "win32";
|
|
3
4
|
isWsl: boolean;
|
|
@@ -5,5 +6,7 @@ export interface Environment {
|
|
|
5
6
|
shellProfile: string | null;
|
|
6
7
|
nodeVersion: string;
|
|
7
8
|
claudeHomeExists: boolean;
|
|
9
|
+
detectedHarnesses: HarnessProfile[];
|
|
8
10
|
}
|
|
11
|
+
/** Detect the current environment: OS, shell, Node version, harnesses, and Claude home status. */
|
|
9
12
|
export declare function detect(): Promise<Environment>;
|
package/dist/steps/detect.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { platform, release } from "node:os";
|
|
2
2
|
import { access } from "node:fs/promises";
|
|
3
3
|
import { getClaudeHome, getShellProfile } from "../lib/paths.js";
|
|
4
|
+
import { detectHarnesses } from "../harnesses/index.js";
|
|
4
5
|
const SUPPORTED_PLATFORMS = new Set(["linux", "darwin", "win32"]);
|
|
6
|
+
/** Detect the current environment: OS, shell, Node version, harnesses, and Claude home status. */
|
|
5
7
|
export async function detect() {
|
|
6
8
|
const p = platform();
|
|
7
9
|
if (!SUPPORTED_PLATFORMS.has(p)) {
|
|
8
10
|
throw new Error(`Unsupported platform: ${p}. Expected linux, darwin, or win32.`);
|
|
9
11
|
}
|
|
10
12
|
const os = p;
|
|
13
|
+
if (os === "win32") {
|
|
14
|
+
throw new Error("Windows (native) is not supported. Please use WSL2 (Ubuntu) and run setup inside WSL.");
|
|
15
|
+
}
|
|
11
16
|
const isWsl = os === "linux" && release().toLowerCase().includes("microsoft");
|
|
12
17
|
const profile = getShellProfile();
|
|
13
18
|
const nodeVersion = process.version;
|
|
@@ -19,6 +24,7 @@ export async function detect() {
|
|
|
19
24
|
catch {
|
|
20
25
|
// Does not exist
|
|
21
26
|
}
|
|
27
|
+
const detectedHarnesses = detectHarnesses();
|
|
22
28
|
return {
|
|
23
29
|
os,
|
|
24
30
|
isWsl,
|
|
@@ -26,5 +32,6 @@ export async function detect() {
|
|
|
26
32
|
shellProfile: profile?.path ?? null,
|
|
27
33
|
nodeVersion,
|
|
28
34
|
claudeHomeExists,
|
|
35
|
+
detectedHarnesses,
|
|
29
36
|
};
|
|
30
37
|
}
|
package/dist/steps/mcp.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type { HarnessProfile } from "../harnesses/index.js";
|
|
1
2
|
export interface McpResult {
|
|
2
3
|
configPath: string;
|
|
3
4
|
scope: "global" | "local";
|
|
5
|
+
packageWarnings: string[];
|
|
4
6
|
}
|
|
5
|
-
|
|
6
|
-
export declare function
|
|
7
|
+
/** Write UluOps MCP server entries into a harness's config file. */
|
|
8
|
+
export declare function installMcp(profile: HarnessProfile, apiKey: string, scope: "global" | "local", dryRun: boolean): Promise<McpResult>;
|
|
9
|
+
/** Remove UluOps MCP server entries from the harness config. */
|
|
10
|
+
export declare function uninstallMcp(profile: HarnessProfile, configPath: string): Promise<void>;
|