@movp/cli 1.0.1 → 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 +92 -72
- 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 {
|
|
@@ -149,16 +152,18 @@ function loadCredentials() {
|
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
function writeCredentials(bffUrl, userId, tenantId) {
|
|
155
|
+
function writeCredentials(bffUrl, userId, tenantId, accessToken) {
|
|
153
156
|
const configDir = path.join(os.homedir(), ".config", "movp");
|
|
154
157
|
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
155
158
|
const credPath = path.join(configDir, "credentials");
|
|
159
|
+
const tokenLine = accessToken ? `WORKDESK_ACCESS_TOKEN=${accessToken}\n` : "";
|
|
156
160
|
const content =
|
|
157
161
|
`# MoVP device credentials — written by 'npx @movp/cli login'\n` +
|
|
158
162
|
`# Do not commit this file.\n` +
|
|
159
163
|
`WORKDESK_URL=${bffUrl}\n` +
|
|
160
164
|
`WORKDESK_USER=${userId}\n` +
|
|
161
|
-
`WORKDESK_TENANT=${tenantId}\n
|
|
165
|
+
`WORKDESK_TENANT=${tenantId}\n` +
|
|
166
|
+
tokenLine;
|
|
162
167
|
fs.writeFileSync(credPath, content, { mode: 0o600 });
|
|
163
168
|
return credPath;
|
|
164
169
|
}
|
|
@@ -297,6 +302,7 @@ function printInstallHelp() {
|
|
|
297
302
|
--dir <path> Install to a custom directory instead of ~/.movp/plugins/
|
|
298
303
|
--version <tag> Install a specific git tag/branch (default: main)
|
|
299
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
|
|
300
306
|
-h, --help Show this help text
|
|
301
307
|
|
|
302
308
|
Environment:
|
|
@@ -311,10 +317,11 @@ function printInstallHelp() {
|
|
|
311
317
|
npx @movp/cli install --dir /opt/movp/plugins
|
|
312
318
|
npx @movp/cli install --version v1.2.0
|
|
313
319
|
npx @movp/cli install --tool cursor --init
|
|
320
|
+
npx @movp/cli install --tool claude --init --url-only
|
|
314
321
|
`);
|
|
315
322
|
}
|
|
316
323
|
|
|
317
|
-
async function runInstall({ installDir, tool, version, runInitAfter }) {
|
|
324
|
+
async function runInstall({ installDir, tool, version, runInitAfter, urlOnly = false }) {
|
|
318
325
|
const { spawnSync } = require("child_process");
|
|
319
326
|
|
|
320
327
|
const targetDir = installDir
|
|
@@ -442,7 +449,7 @@ async function runInstall({ installDir, tool, version, runInitAfter }) {
|
|
|
442
449
|
// Map install --tool names to runInit's expected tool identifiers
|
|
443
450
|
const forcedTool = tool === "claude" ? "claude-code" : tool === "cursor" ? "cursor" : tool === "codex" ? "codex" : null;
|
|
444
451
|
try {
|
|
445
|
-
await runInit(forcedTool);
|
|
452
|
+
await runInit(forcedTool, { urlOnly });
|
|
446
453
|
} catch (e) {
|
|
447
454
|
// Plugins are already on disk — distinguish install success from init failure.
|
|
448
455
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -552,8 +559,7 @@ function printInstallNextSteps(pluginsDir, tool) {
|
|
|
552
559
|
// init
|
|
553
560
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
554
561
|
|
|
555
|
-
async function runInit(forcedTool, { noRules = false } = {}) {
|
|
556
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
562
|
+
async function runInit(forcedTool, { noRules = false, urlOnly = false } = {}) {
|
|
557
563
|
const cwd = process.cwd();
|
|
558
564
|
|
|
559
565
|
console.log("\n MoVP CLI v1.0.0\n");
|
|
@@ -561,106 +567,96 @@ async function runInit(forcedTool, { noRules = false } = {}) {
|
|
|
561
567
|
// ── Step 0: Determine tool ─────────────────────────────────────────────────
|
|
562
568
|
let tool = forcedTool || detectTool();
|
|
563
569
|
if (!tool) {
|
|
570
|
+
// Only open readline for tool detection when needed
|
|
571
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
564
572
|
console.log(" Could not auto-detect AI coding tool.");
|
|
565
573
|
const answer = (await prompt(rl, " Tool [claude-code|cursor|codex]: ")).trim().toLowerCase();
|
|
574
|
+
rl.close();
|
|
566
575
|
tool = answer || "claude-code";
|
|
567
576
|
} else {
|
|
568
577
|
console.log(` Detected: ${tool}`);
|
|
569
578
|
}
|
|
570
579
|
|
|
571
580
|
if (!["claude-code", "cursor", "codex"].includes(tool)) {
|
|
572
|
-
console.error(` Unknown tool: ${tool}`);
|
|
573
|
-
rl.close();
|
|
581
|
+
console.error(` Unknown tool: ${tool}. Supported: claude-code, cursor, codex`);
|
|
574
582
|
process.exit(1);
|
|
575
583
|
}
|
|
576
584
|
|
|
577
585
|
// ── Step 1: Authentication ────────────────────────────────────────────────
|
|
578
|
-
console.log("\n Step 1/
|
|
586
|
+
console.log("\n Step 1/2: Authentication");
|
|
579
587
|
let creds = loadCredentials();
|
|
580
588
|
let bffUrl = creds.WORKDESK_URL || process.env.WORKDESK_URL || "";
|
|
581
589
|
let tenantId = creds.WORKDESK_TENANT || process.env.WORKDESK_TENANT || "";
|
|
582
|
-
let userId = creds.WORKDESK_USER || "";
|
|
583
590
|
|
|
584
591
|
if (bffUrl && tenantId) {
|
|
585
592
|
console.log(` Already authenticated (tenant: ${tenantId})`);
|
|
586
593
|
} else {
|
|
587
594
|
console.log(" Opening browser for device login...");
|
|
588
|
-
// runLogin() only writes to stdout and polls HTTP — rl stays open for prompts after
|
|
589
595
|
await runLogin();
|
|
590
596
|
creds = loadCredentials();
|
|
591
597
|
bffUrl = creds.WORKDESK_URL || "";
|
|
592
598
|
tenantId = creds.WORKDESK_TENANT || "";
|
|
593
|
-
userId = creds.WORKDESK_USER || "";
|
|
594
599
|
}
|
|
595
600
|
|
|
596
601
|
if (!bffUrl || !tenantId) {
|
|
597
602
|
console.error(" Authentication incomplete. Run `npx @movp/cli login` first.");
|
|
598
|
-
rl.close();
|
|
599
603
|
process.exit(1);
|
|
600
604
|
}
|
|
601
605
|
|
|
602
|
-
// ── Step 2:
|
|
603
|
-
|
|
604
|
-
|
|
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.
|
|
605
611
|
try {
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}, {
|
|
611
|
-
"X-Tenant-ID": tenantId,
|
|
612
|
-
});
|
|
613
|
-
if (res.status === 200 || res.status === 201) {
|
|
614
|
-
apiKey = unwrap(res.body).api_key || unwrap(res.body).apiKey || "";
|
|
615
|
-
if (apiKey) {
|
|
616
|
-
console.log(" Agent source created.");
|
|
617
|
-
// Persist api_key to credentials file
|
|
618
|
-
const configDir = path.join(os.homedir(), ".config", "movp");
|
|
619
|
-
const credPath = path.join(configDir, "credentials");
|
|
620
|
-
let existing = "";
|
|
621
|
-
try { existing = fs.readFileSync(credPath, "utf8"); } catch {}
|
|
622
|
-
if (!existing.includes("WORKDESK_API_KEY=")) {
|
|
623
|
-
fs.appendFileSync(credPath, `WORKDESK_API_KEY=${apiKey}\n`);
|
|
624
|
-
}
|
|
625
|
-
} else {
|
|
626
|
-
console.log(" Agent source configured (no new key returned — using existing).");
|
|
627
|
-
apiKey = process.env.WORKDESK_API_KEY || "";
|
|
628
|
-
}
|
|
629
|
-
} else {
|
|
630
|
-
console.log(` Agent source setup returned HTTP ${res.status} — continuing with existing config.`);
|
|
631
|
-
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);
|
|
632
616
|
}
|
|
633
|
-
} catch
|
|
634
|
-
console.
|
|
635
|
-
|
|
636
|
-
apiKey = process.env.WORKDESK_API_KEY || "";
|
|
617
|
+
} catch {
|
|
618
|
+
console.error(` Invalid MOVP_FRONTEND_URL: ${frontendUrl}`);
|
|
619
|
+
process.exit(1);
|
|
637
620
|
}
|
|
638
621
|
|
|
639
|
-
//
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const envPath = process.env.MOVP_MCP_SERVER_PATH || "";
|
|
643
|
-
const inputPath = await prompt(rl, `\n MCP server path (dist/index.js) [${envPath || "skip"}]: `);
|
|
644
|
-
mcpServerPath = inputPath.trim() || envPath;
|
|
645
|
-
}
|
|
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`;
|
|
646
625
|
|
|
647
|
-
|
|
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`);
|
|
648
633
|
|
|
649
|
-
|
|
650
|
-
|
|
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 */ }
|
|
651
647
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
} else if (tool === "cursor") {
|
|
655
|
-
writeCursorConfig(cwd, bffUrl, apiKey, tenantId, mcpServerPath, noRules);
|
|
656
|
-
} else if (tool === "codex") {
|
|
657
|
-
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");
|
|
658
650
|
}
|
|
659
651
|
|
|
660
|
-
// .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.
|
|
661
655
|
writeMovpConfig(cwd);
|
|
662
656
|
|
|
663
|
-
|
|
657
|
+
if (!urlOnly) {
|
|
658
|
+
console.log(" Ready. Type /movp review to begin.\n");
|
|
659
|
+
}
|
|
664
660
|
}
|
|
665
661
|
|
|
666
662
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -904,16 +900,40 @@ async function runLogin() {
|
|
|
904
900
|
process.exit(1);
|
|
905
901
|
}
|
|
906
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
|
+
|
|
907
917
|
if (tokenRes.status === 200) {
|
|
908
|
-
const { status, user_id, tenant_id } = unwrap(tokenRes.body);
|
|
918
|
+
const { status, user_id, tenant_id, access_token } = unwrap(tokenRes.body);
|
|
909
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
|
+
}
|
|
910
925
|
console.log("\n\n Login successful!\n");
|
|
911
|
-
const credPath = writeCredentials(bffUrl, user_id, tenant_id);
|
|
926
|
+
const credPath = writeCredentials(bffUrl, user_id, tenant_id, access_token);
|
|
912
927
|
console.log(` Credentials: ${credPath}`);
|
|
913
928
|
console.log(` User ID: ${user_id}`);
|
|
914
929
|
console.log(` Tenant ID: ${tenant_id}\n`);
|
|
915
930
|
return;
|
|
916
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
|
+
}
|
|
917
937
|
if (status === "denied") {
|
|
918
938
|
console.log("\n Login denied by user.");
|
|
919
939
|
process.exit(1);
|