@movp/cli 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +98 -73
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
// npx @movp/cli install --dir <path> — custom install location
|
|
7
7
|
// npx @movp/cli install --version <tag> — specific release
|
|
8
8
|
// npx @movp/cli install --init — also run init (opt-in)
|
|
9
|
-
// npx @movp/cli init —
|
|
10
|
-
// npx @movp/cli init --cursor — Cursor-specific setup
|
|
11
|
-
// npx @movp/cli init --codex — Codex-specific setup
|
|
9
|
+
// npx @movp/cli init — authenticate and open browser setup
|
|
10
|
+
// npx @movp/cli init --cursor — Cursor-specific setup
|
|
11
|
+
// npx @movp/cli init --codex — Codex-specific setup
|
|
12
12
|
// npx @movp/cli init --no-rules — skip writing movp-review rule (use when loading the plugin)
|
|
13
|
+
// npx @movp/cli init --url-only — print setup URL to stdout (headless/SSH)
|
|
13
14
|
// npx @movp/cli login — device auth login
|
|
14
15
|
// npx @movp/cli hook — run as PostToolUse hook (reads stdin)
|
|
15
16
|
// npx @movp/cli — alias for `hook`
|
|
@@ -36,7 +37,7 @@ if (command === "--help" || command === "-h") {
|
|
|
36
37
|
|
|
37
38
|
Commands:
|
|
38
39
|
install Install MoVP plugins to ~/.movp/plugins/
|
|
39
|
-
init
|
|
40
|
+
init Authenticate and open browser setup for the current project
|
|
40
41
|
login Device auth login
|
|
41
42
|
hook PostToolUse hook (default when no command given)
|
|
42
43
|
|
|
@@ -67,13 +68,15 @@ if (command === "install") {
|
|
|
67
68
|
const tool = requireFlagValue(toolIdx, "--tool");
|
|
68
69
|
const version = requireFlagValue(versionIdx, "--version");
|
|
69
70
|
const runInitAfter = args.includes("--init");
|
|
70
|
-
|
|
71
|
+
const installUrlOnly = args.includes("--url-only");
|
|
72
|
+
runInstall({ installDir, tool, version, runInitAfter, urlOnly: installUrlOnly }).catch((e) => { console.error(e.message); process.exit(1); });
|
|
71
73
|
} else if (command === "init") {
|
|
72
74
|
const forcedTool = args.includes("--cursor") ? "cursor"
|
|
73
75
|
: args.includes("--codex") ? "codex"
|
|
74
76
|
: null;
|
|
75
77
|
const noRules = args.includes("--no-rules");
|
|
76
|
-
|
|
78
|
+
const urlOnly = args.includes("--url-only");
|
|
79
|
+
runInit(forcedTool, { noRules, urlOnly }).catch((e) => { console.error(e.message); process.exit(1); });
|
|
77
80
|
} else if (command === "login") {
|
|
78
81
|
runLogin().catch((e) => { console.error(e.message); process.exit(1); });
|
|
79
82
|
} else {
|
|
@@ -94,6 +97,11 @@ function sleep(ms) {
|
|
|
94
97
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
// BFF wraps all /api/* responses in { data: ... }
|
|
101
|
+
function unwrap(body) {
|
|
102
|
+
return (body && typeof body === "object" && "data" in body) ? body.data : body;
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
function postJSON(baseUrl, urlPath, body, extraHeaders = {}) {
|
|
98
106
|
return new Promise((resolve, reject) => {
|
|
99
107
|
const data = JSON.stringify(body);
|
|
@@ -144,16 +152,18 @@ function loadCredentials() {
|
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
function writeCredentials(bffUrl, userId, tenantId) {
|
|
155
|
+
function writeCredentials(bffUrl, userId, tenantId, accessToken) {
|
|
148
156
|
const configDir = path.join(os.homedir(), ".config", "movp");
|
|
149
157
|
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
150
158
|
const credPath = path.join(configDir, "credentials");
|
|
159
|
+
const tokenLine = accessToken ? `WORKDESK_ACCESS_TOKEN=${accessToken}\n` : "";
|
|
151
160
|
const content =
|
|
152
161
|
`# MoVP device credentials — written by 'npx @movp/cli login'\n` +
|
|
153
162
|
`# Do not commit this file.\n` +
|
|
154
163
|
`WORKDESK_URL=${bffUrl}\n` +
|
|
155
164
|
`WORKDESK_USER=${userId}\n` +
|
|
156
|
-
`WORKDESK_TENANT=${tenantId}\n
|
|
165
|
+
`WORKDESK_TENANT=${tenantId}\n` +
|
|
166
|
+
tokenLine;
|
|
157
167
|
fs.writeFileSync(credPath, content, { mode: 0o600 });
|
|
158
168
|
return credPath;
|
|
159
169
|
}
|
|
@@ -292,6 +302,7 @@ function printInstallHelp() {
|
|
|
292
302
|
--dir <path> Install to a custom directory instead of ~/.movp/plugins/
|
|
293
303
|
--version <tag> Install a specific git tag/branch (default: main)
|
|
294
304
|
--init Also run \`init\` for the installed tool after install
|
|
305
|
+
--url-only With --init: print setup URL to stdout instead of opening a browser
|
|
295
306
|
-h, --help Show this help text
|
|
296
307
|
|
|
297
308
|
Environment:
|
|
@@ -306,10 +317,11 @@ function printInstallHelp() {
|
|
|
306
317
|
npx @movp/cli install --dir /opt/movp/plugins
|
|
307
318
|
npx @movp/cli install --version v1.2.0
|
|
308
319
|
npx @movp/cli install --tool cursor --init
|
|
320
|
+
npx @movp/cli install --tool claude --init --url-only
|
|
309
321
|
`);
|
|
310
322
|
}
|
|
311
323
|
|
|
312
|
-
async function runInstall({ installDir, tool, version, runInitAfter }) {
|
|
324
|
+
async function runInstall({ installDir, tool, version, runInitAfter, urlOnly = false }) {
|
|
313
325
|
const { spawnSync } = require("child_process");
|
|
314
326
|
|
|
315
327
|
const targetDir = installDir
|
|
@@ -437,7 +449,7 @@ async function runInstall({ installDir, tool, version, runInitAfter }) {
|
|
|
437
449
|
// Map install --tool names to runInit's expected tool identifiers
|
|
438
450
|
const forcedTool = tool === "claude" ? "claude-code" : tool === "cursor" ? "cursor" : tool === "codex" ? "codex" : null;
|
|
439
451
|
try {
|
|
440
|
-
await runInit(forcedTool);
|
|
452
|
+
await runInit(forcedTool, { urlOnly });
|
|
441
453
|
} catch (e) {
|
|
442
454
|
// Plugins are already on disk — distinguish install success from init failure.
|
|
443
455
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -547,8 +559,7 @@ function printInstallNextSteps(pluginsDir, tool) {
|
|
|
547
559
|
// init
|
|
548
560
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
549
561
|
|
|
550
|
-
async function runInit(forcedTool, { noRules = false } = {}) {
|
|
551
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
562
|
+
async function runInit(forcedTool, { noRules = false, urlOnly = false } = {}) {
|
|
552
563
|
const cwd = process.cwd();
|
|
553
564
|
|
|
554
565
|
console.log("\n MoVP CLI v1.0.0\n");
|
|
@@ -556,106 +567,96 @@ async function runInit(forcedTool, { noRules = false } = {}) {
|
|
|
556
567
|
// ── Step 0: Determine tool ─────────────────────────────────────────────────
|
|
557
568
|
let tool = forcedTool || detectTool();
|
|
558
569
|
if (!tool) {
|
|
570
|
+
// Only open readline for tool detection when needed
|
|
571
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
559
572
|
console.log(" Could not auto-detect AI coding tool.");
|
|
560
573
|
const answer = (await prompt(rl, " Tool [claude-code|cursor|codex]: ")).trim().toLowerCase();
|
|
574
|
+
rl.close();
|
|
561
575
|
tool = answer || "claude-code";
|
|
562
576
|
} else {
|
|
563
577
|
console.log(` Detected: ${tool}`);
|
|
564
578
|
}
|
|
565
579
|
|
|
566
580
|
if (!["claude-code", "cursor", "codex"].includes(tool)) {
|
|
567
|
-
console.error(` Unknown tool: ${tool}`);
|
|
568
|
-
rl.close();
|
|
581
|
+
console.error(` Unknown tool: ${tool}. Supported: claude-code, cursor, codex`);
|
|
569
582
|
process.exit(1);
|
|
570
583
|
}
|
|
571
584
|
|
|
572
585
|
// ── Step 1: Authentication ────────────────────────────────────────────────
|
|
573
|
-
console.log("\n Step 1/
|
|
586
|
+
console.log("\n Step 1/2: Authentication");
|
|
574
587
|
let creds = loadCredentials();
|
|
575
588
|
let bffUrl = creds.WORKDESK_URL || process.env.WORKDESK_URL || "";
|
|
576
589
|
let tenantId = creds.WORKDESK_TENANT || process.env.WORKDESK_TENANT || "";
|
|
577
|
-
let userId = creds.WORKDESK_USER || "";
|
|
578
590
|
|
|
579
591
|
if (bffUrl && tenantId) {
|
|
580
592
|
console.log(` Already authenticated (tenant: ${tenantId})`);
|
|
581
593
|
} else {
|
|
582
594
|
console.log(" Opening browser for device login...");
|
|
583
|
-
// runLogin() only writes to stdout and polls HTTP — rl stays open for prompts after
|
|
584
595
|
await runLogin();
|
|
585
596
|
creds = loadCredentials();
|
|
586
597
|
bffUrl = creds.WORKDESK_URL || "";
|
|
587
598
|
tenantId = creds.WORKDESK_TENANT || "";
|
|
588
|
-
userId = creds.WORKDESK_USER || "";
|
|
589
599
|
}
|
|
590
600
|
|
|
591
601
|
if (!bffUrl || !tenantId) {
|
|
592
602
|
console.error(" Authentication incomplete. Run `npx @movp/cli login` first.");
|
|
593
|
-
rl.close();
|
|
594
603
|
process.exit(1);
|
|
595
604
|
}
|
|
596
605
|
|
|
597
|
-
// ── Step 2:
|
|
598
|
-
|
|
599
|
-
|
|
606
|
+
// ── Step 2: Complete setup in browser ────────────────────────────────────
|
|
607
|
+
const frontendUrl = process.env.MOVP_FRONTEND_URL || "https://mostviableproduct.com";
|
|
608
|
+
|
|
609
|
+
// Validate URL: must be https (or http://localhost for dev). Prevents command
|
|
610
|
+
// injection via MOVP_FRONTEND_URL and blocks non-TLS origins in production.
|
|
600
611
|
try {
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}, {
|
|
606
|
-
"X-Tenant-ID": tenantId,
|
|
607
|
-
});
|
|
608
|
-
if (res.status === 200 || res.status === 201) {
|
|
609
|
-
apiKey = res.body.api_key || res.body.apiKey || "";
|
|
610
|
-
if (apiKey) {
|
|
611
|
-
console.log(" Agent source created.");
|
|
612
|
-
// Persist api_key to credentials file
|
|
613
|
-
const configDir = path.join(os.homedir(), ".config", "movp");
|
|
614
|
-
const credPath = path.join(configDir, "credentials");
|
|
615
|
-
let existing = "";
|
|
616
|
-
try { existing = fs.readFileSync(credPath, "utf8"); } catch {}
|
|
617
|
-
if (!existing.includes("WORKDESK_API_KEY=")) {
|
|
618
|
-
fs.appendFileSync(credPath, `WORKDESK_API_KEY=${apiKey}\n`);
|
|
619
|
-
}
|
|
620
|
-
} else {
|
|
621
|
-
console.log(" Agent source configured (no new key returned — using existing).");
|
|
622
|
-
apiKey = process.env.WORKDESK_API_KEY || "";
|
|
623
|
-
}
|
|
624
|
-
} else {
|
|
625
|
-
console.log(` Agent source setup returned HTTP ${res.status} — continuing with existing config.`);
|
|
626
|
-
apiKey = process.env.WORKDESK_API_KEY || "";
|
|
612
|
+
const parsed = new URL(frontendUrl);
|
|
613
|
+
if (parsed.protocol !== "https:" && !parsed.hostname.match(/^(localhost|127\.0\.0\.1)$/)) {
|
|
614
|
+
console.error(` MOVP_FRONTEND_URL must be https (got ${parsed.protocol})`);
|
|
615
|
+
process.exit(1);
|
|
627
616
|
}
|
|
628
|
-
} catch
|
|
629
|
-
console.
|
|
630
|
-
|
|
631
|
-
apiKey = process.env.WORKDESK_API_KEY || "";
|
|
617
|
+
} catch {
|
|
618
|
+
console.error(` Invalid MOVP_FRONTEND_URL: ${frontendUrl}`);
|
|
619
|
+
process.exit(1);
|
|
632
620
|
}
|
|
633
621
|
|
|
634
|
-
//
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const envPath = process.env.MOVP_MCP_SERVER_PATH || "";
|
|
638
|
-
const inputPath = await prompt(rl, `\n MCP server path (dist/index.js) [${envPath || "skip"}]: `);
|
|
639
|
-
mcpServerPath = inputPath.trim() || envPath;
|
|
640
|
-
}
|
|
622
|
+
// Strip trailing slash to avoid "https://example.com//setup/agent"
|
|
623
|
+
const baseUrl = frontendUrl.replace(/\/+$/, "");
|
|
624
|
+
const setupUrl = `${baseUrl}/setup/agent?tool=${encodeURIComponent(tool)}&from=cli`;
|
|
641
625
|
|
|
642
|
-
|
|
626
|
+
if (urlOnly) {
|
|
627
|
+
// Headless/SSH mode: URL to stdout (pipeable), hint to stderr
|
|
628
|
+
process.stdout.write(setupUrl + "\n");
|
|
629
|
+
process.stderr.write("Open this URL in a browser to complete setup.\n");
|
|
630
|
+
} else {
|
|
631
|
+
console.log("\n Step 2/2: Complete setup in your browser\n");
|
|
632
|
+
console.log(` ${setupUrl}\n`);
|
|
643
633
|
|
|
644
|
-
|
|
645
|
-
|
|
634
|
+
// Open browser using execFileSync with array args (no shell interpolation).
|
|
635
|
+
// Non-fatal — URL is always printed above as fallback.
|
|
636
|
+
// Windows note: `start` is a cmd.exe built-in — use cmd.exe /c start.
|
|
637
|
+
try {
|
|
638
|
+
const { execFileSync } = require("child_process");
|
|
639
|
+
if (process.platform === "darwin") {
|
|
640
|
+
execFileSync("open", [setupUrl], { stdio: "ignore" });
|
|
641
|
+
} else if (process.platform === "win32") {
|
|
642
|
+
execFileSync("cmd.exe", ["/c", "start", "", setupUrl], { stdio: "ignore" });
|
|
643
|
+
} else {
|
|
644
|
+
execFileSync("xdg-open", [setupUrl], { stdio: "ignore" });
|
|
645
|
+
}
|
|
646
|
+
} catch { /* non-fatal — URL printed above */ }
|
|
646
647
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
} else if (tool === "cursor") {
|
|
650
|
-
writeCursorConfig(cwd, bffUrl, apiKey, tenantId, mcpServerPath, noRules);
|
|
651
|
-
} else if (tool === "codex") {
|
|
652
|
-
writeCodexConfig(cwd, bffUrl, tenantId, apiKey);
|
|
648
|
+
console.log(" Follow the steps in your browser to get your config snippet.");
|
|
649
|
+
console.log(" Once done, paste the config into your project.\n");
|
|
653
650
|
}
|
|
654
651
|
|
|
655
|
-
// .movp/config.yaml
|
|
652
|
+
// ── Write .movp/config.yaml ───────────────────────────────────────────────
|
|
653
|
+
// All other config (.claude/settings.json, env vars, API key) is handled
|
|
654
|
+
// by the browser setup page's download script.
|
|
656
655
|
writeMovpConfig(cwd);
|
|
657
656
|
|
|
658
|
-
|
|
657
|
+
if (!urlOnly) {
|
|
658
|
+
console.log(" Ready. Type /movp review to begin.\n");
|
|
659
|
+
}
|
|
659
660
|
}
|
|
660
661
|
|
|
661
662
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -863,7 +864,7 @@ async function runLogin() {
|
|
|
863
864
|
}
|
|
864
865
|
|
|
865
866
|
const { device_code, user_code, verification_uri_complete, expires_in, interval } =
|
|
866
|
-
authorizeRes.body;
|
|
867
|
+
unwrap(authorizeRes.body);
|
|
867
868
|
|
|
868
869
|
console.log(` Your verification code: ${user_code}`);
|
|
869
870
|
console.log(`\n Open this URL to approve:\n`);
|
|
@@ -899,16 +900,40 @@ async function runLogin() {
|
|
|
899
900
|
process.exit(1);
|
|
900
901
|
}
|
|
901
902
|
|
|
903
|
+
if (tokenRes.status === 409) {
|
|
904
|
+
console.log("\n This authorization was already completed in another session.");
|
|
905
|
+
console.log(" Run `npx @movp/cli login` to start a fresh device flow.\n");
|
|
906
|
+
process.exit(1);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (tokenRes.status >= 500) {
|
|
910
|
+
const b = unwrap(tokenRes.body);
|
|
911
|
+
const msg = b?.error?.message || b?.message || `HTTP ${tokenRes.status}`;
|
|
912
|
+
console.log(`\n Server error while polling: ${msg}`);
|
|
913
|
+
console.log(" Try again in a moment.\n");
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
|
|
902
917
|
if (tokenRes.status === 200) {
|
|
903
|
-
const { status, user_id, tenant_id } = tokenRes.body;
|
|
918
|
+
const { status, user_id, tenant_id, access_token } = unwrap(tokenRes.body);
|
|
904
919
|
if (status === "authorized") {
|
|
920
|
+
if (!access_token) {
|
|
921
|
+
console.log("\n Server returned authorized status but no access_token (contract error).");
|
|
922
|
+
console.log(" Please file an issue with the MoVP team.\n");
|
|
923
|
+
process.exit(1);
|
|
924
|
+
}
|
|
905
925
|
console.log("\n\n Login successful!\n");
|
|
906
|
-
const credPath = writeCredentials(bffUrl, user_id, tenant_id);
|
|
926
|
+
const credPath = writeCredentials(bffUrl, user_id, tenant_id, access_token);
|
|
907
927
|
console.log(` Credentials: ${credPath}`);
|
|
908
928
|
console.log(` User ID: ${user_id}`);
|
|
909
929
|
console.log(` Tenant ID: ${tenant_id}\n`);
|
|
910
930
|
return;
|
|
911
931
|
}
|
|
932
|
+
if (status === "consumed") {
|
|
933
|
+
console.log("\n Device token was already issued to a previous session.");
|
|
934
|
+
console.log(" Run `npx @movp/cli login` to start a fresh device flow.\n");
|
|
935
|
+
process.exit(1);
|
|
936
|
+
}
|
|
912
937
|
if (status === "denied") {
|
|
913
938
|
console.log("\n Login denied by user.");
|
|
914
939
|
process.exit(1);
|