@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/steps/mcp.js
CHANGED
|
@@ -1,40 +1,65 @@
|
|
|
1
|
-
import { readFile,
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
1
|
+
import { readFile, access, mkdir, copyFile } from "node:fs/promises";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import { checkMcpPackageAvailability } from "../lib/config-merger.js";
|
|
4
|
+
import { findProjectRoot, getBackupDir } from "../lib/paths.js";
|
|
5
|
+
import { atomicWrite } from "../lib/atomic-write.js";
|
|
6
|
+
/** Write UluOps MCP server entries into a harness's config file. */
|
|
7
|
+
export async function installMcp(profile, apiKey, scope, dryRun) {
|
|
8
|
+
const configPath = scope === "global"
|
|
9
|
+
? profile.paths.globalMcpConfig
|
|
10
|
+
: join(await findProjectRoot(), profile.paths.localMcpConfig);
|
|
11
|
+
const config = await profile.mcpConfig.read(configPath);
|
|
12
|
+
const merged = profile.mcpConfig.merge(config, apiKey);
|
|
13
|
+
const packageWarnings = [];
|
|
14
|
+
const { missing } = await checkMcpPackageAvailability();
|
|
15
|
+
if (missing.length > 0) {
|
|
16
|
+
packageWarnings.push(`npm packages not found in registry: ${missing.join(", ")}. MCP servers may fail to start.`);
|
|
17
|
+
}
|
|
9
18
|
if (!dryRun) {
|
|
10
|
-
|
|
19
|
+
// Backup before first write
|
|
20
|
+
await backupConfig(profile.name, configPath);
|
|
21
|
+
await profile.mcpConfig.write(configPath, merged);
|
|
11
22
|
}
|
|
12
|
-
// If local scope in a git repo, add .mcp.json to .gitignore
|
|
13
23
|
if (scope === "local" && !dryRun) {
|
|
14
|
-
await addToGitignore();
|
|
24
|
+
await addToGitignore(profile.paths.localMcpConfig);
|
|
15
25
|
}
|
|
16
|
-
return { configPath, scope };
|
|
26
|
+
return { configPath, scope, packageWarnings };
|
|
27
|
+
}
|
|
28
|
+
/** Remove UluOps MCP server entries from the harness config. */
|
|
29
|
+
export async function uninstallMcp(profile, configPath) {
|
|
30
|
+
await backupConfig(profile.name, configPath);
|
|
31
|
+
const config = await profile.mcpConfig.read(configPath);
|
|
32
|
+
const cleaned = profile.mcpConfig.remove(config);
|
|
33
|
+
await profile.mcpConfig.write(configPath, cleaned);
|
|
17
34
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
35
|
+
async function backupConfig(harnessName, configPath) {
|
|
36
|
+
try {
|
|
37
|
+
await access(configPath);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return; // Nothing to back up
|
|
41
|
+
}
|
|
42
|
+
const backupDir = getBackupDir(harnessName);
|
|
43
|
+
await mkdir(backupDir, { recursive: true });
|
|
44
|
+
const filename = basename(configPath);
|
|
45
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
46
|
+
await copyFile(configPath, join(backupDir, `${filename}.${timestamp}.bak`));
|
|
22
47
|
}
|
|
23
|
-
async function addToGitignore() {
|
|
48
|
+
async function addToGitignore(localConfigFilename) {
|
|
24
49
|
const gitignorePath = join(process.cwd(), ".gitignore");
|
|
25
50
|
try {
|
|
26
51
|
await access(join(process.cwd(), ".git"));
|
|
27
52
|
}
|
|
28
53
|
catch {
|
|
29
|
-
return;
|
|
54
|
+
return;
|
|
30
55
|
}
|
|
31
56
|
try {
|
|
32
57
|
const content = await readFile(gitignorePath, "utf-8");
|
|
33
|
-
if (content.includes(
|
|
58
|
+
if (content.includes(localConfigFilename))
|
|
34
59
|
return;
|
|
35
|
-
await
|
|
60
|
+
await atomicWrite(gitignorePath, content.trimEnd() + `\n${localConfigFilename}\n`);
|
|
36
61
|
}
|
|
37
62
|
catch {
|
|
38
|
-
await
|
|
63
|
+
await atomicWrite(gitignorePath, `${localConfigFilename}\n`);
|
|
39
64
|
}
|
|
40
65
|
}
|
package/dist/steps/metrics.d.ts
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Metrics Step
|
|
3
3
|
*
|
|
4
|
-
* Installs agent-metrics tool files
|
|
5
|
-
*
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import type { HarnessProfile } from "../harnesses/index.js";
|
|
8
|
+
/**
|
|
9
|
+
* The hook command that runs on SubagentStop.
|
|
10
|
+
* @internal Exported for testing only — not part of the public API.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getHookCommand(profile: HarnessProfile): string;
|
|
11
13
|
export interface MetricsResult {
|
|
12
14
|
toolFilesCopied: number;
|
|
13
15
|
hookConfigured: boolean;
|
|
16
|
+
skippedReason?: string;
|
|
14
17
|
}
|
|
15
18
|
/**
|
|
16
|
-
* Install agent-metrics: copy tool files and configure
|
|
19
|
+
* Install agent-metrics: copy tool files and configure hook.
|
|
20
|
+
* Skips entirely if the harness doesn't support hooks.
|
|
17
21
|
*/
|
|
18
|
-
export declare function installMetrics(dryRun: boolean): Promise<MetricsResult>;
|
|
22
|
+
export declare function installMetrics(profile: HarnessProfile, dryRun: boolean): Promise<MetricsResult>;
|
|
19
23
|
/**
|
|
20
|
-
* Uninstall agent-metrics: remove hook
|
|
24
|
+
* Uninstall agent-metrics: remove hook and tool files.
|
|
21
25
|
*/
|
|
22
|
-
export declare function uninstallMetrics(dryRun: boolean): Promise<void>;
|
|
26
|
+
export declare function uninstallMetrics(profile: HarnessProfile, dryRun: boolean): Promise<void>;
|
package/dist/steps/metrics.js
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Metrics Step
|
|
3
3
|
*
|
|
4
|
-
* Installs agent-metrics tool files
|
|
5
|
-
*
|
|
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
7
|
import { mkdir, readdir, copyFile, rm, access } from "node:fs/promises";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
return `"${process.execPath}" "${join(toolDir, "dist", "hook.js")}"`;
|
|
23
22
|
}
|
|
24
23
|
/**
|
|
25
24
|
* Find the agent-metrics package source directory.
|
|
@@ -43,62 +42,37 @@ async function findMetricsSource() {
|
|
|
43
42
|
* Copy agent-metrics dist files to the tool directory.
|
|
44
43
|
* Copies all .js files needed for the hook and CLI.
|
|
45
44
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
45
|
+
/** Copy .js files from a source dir to a dest dir, skipping test files. */
|
|
46
|
+
async function copyJsDir(srcDir, destDir, dryRun) {
|
|
47
|
+
let count = 0;
|
|
70
48
|
try {
|
|
71
|
-
const
|
|
72
|
-
for (const file of
|
|
73
|
-
if (!file.endsWith(".js"))
|
|
74
|
-
continue;
|
|
75
|
-
if (file.includes(".test."))
|
|
49
|
+
const files = await readdir(srcDir);
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
if (!file.endsWith(".js") || file.includes(".test.") || file === "test-utils.js")
|
|
76
52
|
continue;
|
|
77
|
-
if (!dryRun)
|
|
78
|
-
await copyFile(join(
|
|
79
|
-
|
|
80
|
-
filesCopied++;
|
|
53
|
+
if (!dryRun)
|
|
54
|
+
await copyFile(join(srcDir, file), join(destDir, file));
|
|
55
|
+
count++;
|
|
81
56
|
}
|
|
82
57
|
}
|
|
83
58
|
catch {
|
|
84
|
-
//
|
|
59
|
+
// Directory doesn't exist — not critical
|
|
85
60
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
filesCopied++;
|
|
61
|
+
return count;
|
|
62
|
+
}
|
|
63
|
+
async function copyToolFiles(srcRoot, destRoot, dryRun) {
|
|
64
|
+
const srcDist = join(srcRoot, "dist");
|
|
65
|
+
const destDist = join(destRoot, "dist");
|
|
66
|
+
const subDirs = ["commands", "display"];
|
|
67
|
+
if (!dryRun) {
|
|
68
|
+
await mkdir(destDist, { recursive: true });
|
|
69
|
+
for (const sub of subDirs) {
|
|
70
|
+
await mkdir(join(destDist, sub), { recursive: true });
|
|
98
71
|
}
|
|
99
72
|
}
|
|
100
|
-
|
|
101
|
-
|
|
73
|
+
let filesCopied = await copyJsDir(srcDist, destDist, dryRun);
|
|
74
|
+
for (const sub of subDirs) {
|
|
75
|
+
filesCopied += await copyJsDir(join(srcDist, sub), join(destDist, sub), dryRun);
|
|
102
76
|
}
|
|
103
77
|
// Copy package.json (needed for CLI bin resolution)
|
|
104
78
|
try {
|
|
@@ -113,61 +87,57 @@ async function copyToolFiles(srcRoot, destRoot, dryRun) {
|
|
|
113
87
|
return filesCopied;
|
|
114
88
|
}
|
|
115
89
|
/**
|
|
116
|
-
* Install agent-metrics: copy tool files and configure
|
|
90
|
+
* Install agent-metrics: copy tool files and configure hook.
|
|
91
|
+
* Skips entirely if the harness doesn't support hooks.
|
|
117
92
|
*/
|
|
118
|
-
export async function installMetrics(dryRun) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
93
|
+
export async function installMetrics(profile, dryRun) {
|
|
94
|
+
if (!profile.hooks || !profile.paths.toolsDir || !profile.paths.settingsPath) {
|
|
95
|
+
return { toolFilesCopied: 0, hookConfigured: false, skippedReason: "no-hook-support" };
|
|
96
|
+
}
|
|
97
|
+
const toolDir = profile.paths.toolsDir;
|
|
98
|
+
const settingsPath = profile.paths.settingsPath;
|
|
122
99
|
const srcRoot = await findMetricsSource();
|
|
123
100
|
let toolFilesCopied = 0;
|
|
124
101
|
if (srcRoot) {
|
|
125
|
-
// Copy tool files
|
|
126
102
|
if (!dryRun) {
|
|
127
103
|
await mkdir(toolDir, { recursive: true });
|
|
128
104
|
}
|
|
129
105
|
toolFilesCopied = await copyToolFiles(srcRoot, toolDir, dryRun);
|
|
130
106
|
}
|
|
131
107
|
else {
|
|
132
|
-
// Check if already installed (from previous run or install.sh)
|
|
133
108
|
try {
|
|
134
109
|
await access(join(toolDir, "dist", "hook.js"));
|
|
135
110
|
}
|
|
136
111
|
catch {
|
|
137
|
-
// Not found anywhere
|
|
138
|
-
// if files happen to exist
|
|
112
|
+
// Not found anywhere
|
|
139
113
|
}
|
|
140
114
|
}
|
|
141
|
-
// Configure hook in settings.json
|
|
142
115
|
let hookConfigured = false;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
await
|
|
116
|
+
const hookJsPath = join(toolDir, "dist", "hook.js");
|
|
117
|
+
const hookJsExists = await access(hookJsPath).then(() => true, () => false);
|
|
118
|
+
if (hookJsExists && !dryRun) {
|
|
119
|
+
const hookCommand = getHookCommand(profile);
|
|
120
|
+
await profile.hooks.install(settingsPath, hookCommand, false);
|
|
148
121
|
hookConfigured = true;
|
|
149
122
|
}
|
|
150
|
-
else {
|
|
123
|
+
else if (hookJsExists && dryRun) {
|
|
151
124
|
hookConfigured = true;
|
|
152
125
|
}
|
|
153
126
|
return { toolFilesCopied, hookConfigured };
|
|
154
127
|
}
|
|
155
128
|
/**
|
|
156
|
-
* Uninstall agent-metrics: remove hook
|
|
129
|
+
* Uninstall agent-metrics: remove hook and tool files.
|
|
157
130
|
*/
|
|
158
|
-
export async function uninstallMetrics(dryRun) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
131
|
+
export async function uninstallMetrics(profile, dryRun) {
|
|
132
|
+
if (!profile.hooks || !profile.paths.toolsDir || !profile.paths.settingsPath) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
162
135
|
if (!dryRun) {
|
|
163
|
-
|
|
164
|
-
const cleaned = removeUluopsHook(settings);
|
|
165
|
-
await writeSettings(settingsPath, cleaned);
|
|
136
|
+
await profile.hooks.remove(profile.paths.settingsPath, false);
|
|
166
137
|
}
|
|
167
|
-
// Remove tool directory
|
|
168
138
|
if (!dryRun) {
|
|
169
139
|
try {
|
|
170
|
-
await rm(
|
|
140
|
+
await rm(profile.paths.toolsDir, { recursive: true, force: true });
|
|
171
141
|
}
|
|
172
142
|
catch {
|
|
173
143
|
// Already gone
|
package/dist/steps/shell.d.ts
CHANGED
|
@@ -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>;
|
package/dist/steps/shell.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { readFile
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { atomicWrite } from "../lib/atomic-write.js";
|
|
2
3
|
const FENCE_START = "# --- UluOps (managed by @uluops/setup) ---";
|
|
3
4
|
const FENCE_END = "# --- /UluOps ---";
|
|
5
|
+
/** Characters safe for shell variable values (no metacharacters). */
|
|
6
|
+
const SAFE_KEY_PATTERN = /^[a-zA-Z0-9_\-\.]+$/;
|
|
7
|
+
/** Write a fenced ULUOPS_API_KEY export block into the user's shell profile, replacing any existing UluOps block. */
|
|
4
8
|
export async function writeShellExport(profilePath, apiKey, dryRun) {
|
|
9
|
+
if (!SAFE_KEY_PATTERN.test(apiKey)) {
|
|
10
|
+
throw new Error("API key contains characters unsafe for shell export. Only alphanumeric, underscore, hyphen, and dot are allowed.");
|
|
11
|
+
}
|
|
5
12
|
const block = `${FENCE_START}\nexport ULUOPS_API_KEY="${apiKey}"\n${FENCE_END}`;
|
|
6
13
|
let content;
|
|
7
14
|
try {
|
|
@@ -9,27 +16,27 @@ export async function writeShellExport(profilePath, apiKey, dryRun) {
|
|
|
9
16
|
}
|
|
10
17
|
catch {
|
|
11
18
|
if (!dryRun) {
|
|
12
|
-
await
|
|
19
|
+
await atomicWrite(profilePath, block + "\n");
|
|
13
20
|
}
|
|
14
21
|
return;
|
|
15
22
|
}
|
|
16
23
|
const startIdx = content.indexOf(FENCE_START);
|
|
17
24
|
const endIdx = content.indexOf(FENCE_END);
|
|
18
|
-
if (startIdx !== -1 && endIdx !== -1) {
|
|
19
|
-
// Replace existing fenced block
|
|
25
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
26
|
+
// Replace existing fenced block (use last FENCE_END after FENCE_START to handle duplicates)
|
|
20
27
|
const before = content.slice(0, startIdx);
|
|
21
28
|
const after = content.slice(endIdx + FENCE_END.length);
|
|
22
29
|
if (!dryRun) {
|
|
23
|
-
await
|
|
30
|
+
await atomicWrite(profilePath, before + block + after);
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
33
|
else {
|
|
27
|
-
// Append
|
|
28
34
|
if (!dryRun) {
|
|
29
|
-
await
|
|
35
|
+
await atomicWrite(profilePath, content.trimEnd() + "\n\n" + block + "\n");
|
|
30
36
|
}
|
|
31
37
|
}
|
|
32
38
|
}
|
|
39
|
+
/** Remove the fenced UluOps export block from the user's shell profile. */
|
|
33
40
|
export async function removeShellExport(profilePath) {
|
|
34
41
|
let content;
|
|
35
42
|
try {
|
|
@@ -40,9 +47,9 @@ export async function removeShellExport(profilePath) {
|
|
|
40
47
|
}
|
|
41
48
|
const startIdx = content.indexOf(FENCE_START);
|
|
42
49
|
const endIdx = content.indexOf(FENCE_END);
|
|
43
|
-
if (startIdx !== -1 && endIdx !== -1) {
|
|
50
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
44
51
|
const before = content.slice(0, startIdx);
|
|
45
52
|
const after = content.slice(endIdx + FENCE_END.length);
|
|
46
|
-
await
|
|
53
|
+
await atomicWrite(profilePath, (before + after).replace(/\n{3,}/g, "\n\n"));
|
|
47
54
|
}
|
|
48
55
|
}
|
package/dist/steps/signup.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { AuthResult } from "./auth.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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):
|
|
7
|
+
declare function validatePassword(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>;
|
|
15
|
+
/** @internal Exported for testing only — not part of the public API. */
|
|
13
16
|
export { validatePassword, validateEmail };
|
package/dist/steps/signup.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
const API_BASE = "https://api.uluops.ai/api/v1";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
7
|
function validatePassword(password) {
|
|
7
8
|
if (password.length < 8)
|
|
8
|
-
|
|
9
|
-
if (password.length > 128)
|
|
10
|
-
|
|
11
|
-
if (!/[a-z]/.test(password))
|
|
12
|
-
|
|
13
|
-
if (!/[A-Z]/.test(password))
|
|
14
|
-
|
|
15
|
-
if (!/[0-9]/.test(password))
|
|
16
|
-
|
|
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";
|
|
@@ -40,12 +42,18 @@ export async function signup() {
|
|
|
40
42
|
});
|
|
41
43
|
// Register
|
|
42
44
|
const registerRes = await callApi(`${API_BASE}/auth/register`, "POST", { email, password: pwd });
|
|
45
|
+
if (!registerRes.data?.sessionToken) {
|
|
46
|
+
throw new Error("Registration succeeded but response missing session token");
|
|
47
|
+
}
|
|
43
48
|
const sessionToken = registerRes.data.sessionToken;
|
|
44
49
|
// Create API key using the session
|
|
45
50
|
const keyRes = await callApi(`${API_BASE}/auth/keys`, "POST", { name: "Setup CLI" }, sessionToken);
|
|
51
|
+
if (!keyRes.data?.key) {
|
|
52
|
+
throw new Error("API key creation succeeded but response missing key");
|
|
53
|
+
}
|
|
46
54
|
return {
|
|
47
55
|
apiKey: keyRes.data.key,
|
|
48
|
-
email: registerRes.data.user
|
|
56
|
+
email: registerRes.data.user?.email ?? email,
|
|
49
57
|
};
|
|
50
58
|
}
|
|
51
59
|
async function callApi(url, method, body, bearerToken) {
|
|
@@ -71,7 +79,11 @@ async function callApi(url, method, body, bearerToken) {
|
|
|
71
79
|
throw err;
|
|
72
80
|
}
|
|
73
81
|
if (res.ok) {
|
|
74
|
-
|
|
82
|
+
const body = await res.json();
|
|
83
|
+
if (typeof body !== "object" || body === null) {
|
|
84
|
+
throw new Error("Unexpected API response shape");
|
|
85
|
+
}
|
|
86
|
+
return body;
|
|
75
87
|
}
|
|
76
88
|
// Handle known error codes
|
|
77
89
|
const errorBody = await res.json().catch(() => null);
|
|
@@ -88,5 +100,5 @@ async function callApi(url, method, body, bearerToken) {
|
|
|
88
100
|
}
|
|
89
101
|
throw new Error(`Signup failed (${res.status}): ${message}`);
|
|
90
102
|
}
|
|
91
|
-
|
|
103
|
+
/** @internal Exported for testing only — not part of the public API. */
|
|
92
104
|
export { validatePassword, validateEmail };
|
package/dist/steps/verify.d.ts
CHANGED
|
@@ -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 {};
|