@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.
Files changed (2) hide show
  1. package/bin/cli.js +92 -72
  2. 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 — auto-detect tool, write all config
10
- // npx @movp/cli init --cursor — Cursor-specific setup (MCP + rules only)
11
- // npx @movp/cli init --codex — Codex-specific setup (MCP config only)
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 Write MCP + hook config into the current project
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
- runInstall({ installDir, tool, version, runInitAfter }).catch((e) => { console.error(e.message); process.exit(1); });
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
- runInit(forcedTool, { noRules }).catch((e) => { console.error(e.message); process.exit(1); });
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/3: Authentication");
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: Agent Source ──────────────────────────────────────────────────
603
- console.log("\n Step 2/3: Agent Source");
604
- let apiKey = "";
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 res = await postJSON(bffUrl, "/api/workdesk/agents/setup", {
607
- agent_type: tool,
608
- user_id: userId,
609
- tenant_id: tenantId,
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 (e) {
634
- console.log(` Could not reach ${bffUrl}: ${e.message}`);
635
- console.log(" Continuing with manual config...");
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
- // Ask for MCP server path (required for settings.json)
640
- let mcpServerPath = "";
641
- if (tool === "claude-code" || tool === "cursor") {
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
- rl.close();
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
- // ── Step 3: Write configuration ───────────────────────────────────────────
650
- console.log("\n Step 3/3: Configuration");
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
- if (tool === "claude-code") {
653
- writeClaudeCodeConfig(cwd, bffUrl, apiKey, tenantId, mcpServerPath, noRules);
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 (all tools)
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
- console.log("\n Ready. Type /movp review to begin.\n");
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@movp/cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "MoVP CLI — configure AI coding tools with the MoVP control plane (PostToolUse hook + MCP setup)",
5
5
  "main": "hook.js",
6
6
  "bin": {