@waniwani/cli 0.0.43 → 0.0.46-beta.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/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { Command } from "commander";
14
14
 
15
15
  // src/lib/config.ts
16
16
  import { existsSync } from "fs";
17
- import { mkdir, readFile, writeFile } from "fs/promises";
17
+ import { chmod, mkdir, readFile, writeFile } from "fs/promises";
18
18
  import { join } from "path";
19
19
  import { z } from "zod";
20
20
  var LOCAL_CONFIG_DIR = ".waniwani";
@@ -22,6 +22,8 @@ var CONFIG_FILE_NAME = "settings.json";
22
22
  var LOCAL_DIR = join(process.cwd(), LOCAL_CONFIG_DIR);
23
23
  var LOCAL_FILE = join(LOCAL_DIR, CONFIG_FILE_NAME);
24
24
  var DEFAULT_API_URL = "https://app.waniwani.ai";
25
+ var CONFIG_DIR_MODE = 448;
26
+ var CONFIG_FILE_MODE = 384;
25
27
  var ConfigSchema = z.object({
26
28
  // Settings
27
29
  sessionId: z.string().nullable().default(null),
@@ -41,6 +43,10 @@ var Config = class {
41
43
  this.dir = LOCAL_DIR;
42
44
  this.file = LOCAL_FILE;
43
45
  }
46
+ async setSecurePermissions() {
47
+ await chmod(this.dir, CONFIG_DIR_MODE);
48
+ await chmod(this.file, CONFIG_FILE_MODE);
49
+ }
44
50
  async load() {
45
51
  if (!this.cache) {
46
52
  try {
@@ -55,15 +61,17 @@ var Config = class {
55
61
  }
56
62
  async save(data) {
57
63
  this.cache = data;
58
- await mkdir(this.dir, { recursive: true });
64
+ await mkdir(this.dir, { recursive: true, mode: CONFIG_DIR_MODE });
59
65
  await writeFile(this.file, JSON.stringify(data, null, " "));
66
+ await this.setSecurePermissions();
60
67
  }
61
68
  /**
62
69
  * Ensure the .waniwani directory exists in cwd.
63
70
  * Used by login to create config before saving tokens.
64
71
  */
65
72
  async ensureConfigDir() {
66
- await mkdir(this.dir, { recursive: true });
73
+ await mkdir(this.dir, { recursive: true, mode: CONFIG_DIR_MODE });
74
+ await chmod(this.dir, CONFIG_DIR_MODE);
67
75
  }
68
76
  /**
69
77
  * Check if a .waniwani config directory exists in cwd.
@@ -134,9 +142,11 @@ var config = new Config();
134
142
  async function initConfigAt(dir, overrides = {}) {
135
143
  const configDir = join(dir, LOCAL_CONFIG_DIR);
136
144
  const configPath = join(configDir, CONFIG_FILE_NAME);
137
- await mkdir(configDir, { recursive: true });
145
+ await mkdir(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
138
146
  const data = ConfigSchema.parse(overrides);
139
147
  await writeFile(configPath, JSON.stringify(data, null, " "), "utf-8");
148
+ await chmod(configDir, CONFIG_DIR_MODE);
149
+ await chmod(configPath, CONFIG_FILE_MODE);
140
150
  return { path: configPath, config: data };
141
151
  }
142
152
 
@@ -287,12 +297,16 @@ var configInitCommand = new Command("init").description("Initialize .waniwani co
287
297
  // src/commands/config/index.ts
288
298
  var configCommand = new Command2("config").description("Manage WaniWani configuration").addCommand(configInitCommand);
289
299
 
290
- // src/commands/login.ts
291
- import { spawn } from "child_process";
292
- import { createServer } from "http";
293
- import chalk3 from "chalk";
300
+ // src/commands/git-credential-helper.ts
301
+ import { readFileSync } from "fs";
302
+ import { join as join5 } from "path";
294
303
  import { Command as Command3 } from "commander";
295
- import ora from "ora";
304
+
305
+ // src/lib/git-auth.ts
306
+ import { execFileSync } from "child_process";
307
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "fs";
308
+ import { tmpdir } from "os";
309
+ import { join as join3 } from "path";
296
310
 
297
311
  // src/lib/auth.ts
298
312
  var AuthManager = class {
@@ -346,10 +360,466 @@ var AuthManager = class {
346
360
  return false;
347
361
  }
348
362
  }
349
- };
350
- var auth = new AuthManager();
363
+ };
364
+ var auth = new AuthManager();
365
+
366
+ // src/lib/api.ts
367
+ var ApiError = class extends CLIError {
368
+ constructor(message, code, statusCode, details) {
369
+ super(message, code, details);
370
+ this.statusCode = statusCode;
371
+ this.name = "ApiError";
372
+ }
373
+ };
374
+ async function request(method, path, options) {
375
+ const {
376
+ body,
377
+ requireAuth = true,
378
+ headers: extraHeaders = {}
379
+ } = options || {};
380
+ const headers = {
381
+ "Content-Type": "application/json",
382
+ ...extraHeaders
383
+ };
384
+ if (requireAuth) {
385
+ const token = await auth.getAccessToken();
386
+ if (!token) {
387
+ throw new AuthError(
388
+ "Not logged in. Run 'waniwani login' to authenticate."
389
+ );
390
+ }
391
+ headers.Authorization = `Bearer ${token}`;
392
+ }
393
+ const baseUrl = await config.getApiUrl();
394
+ const url = `${baseUrl}${path}`;
395
+ const response = await fetch(url, {
396
+ method,
397
+ headers,
398
+ body: body ? JSON.stringify(body) : void 0
399
+ });
400
+ if (response.status === 204) {
401
+ return void 0;
402
+ }
403
+ let data;
404
+ let rawBody;
405
+ try {
406
+ rawBody = await response.text();
407
+ data = JSON.parse(rawBody);
408
+ } catch {
409
+ throw new ApiError(
410
+ rawBody || `Request failed with status ${response.status}`,
411
+ "API_ERROR",
412
+ response.status,
413
+ { statusText: response.statusText }
414
+ );
415
+ }
416
+ if (!response.ok || data.error) {
417
+ const errorObject = typeof data.error === "object" && data.error !== null ? data.error : void 0;
418
+ const errorString = typeof data.error === "string" ? data.error : void 0;
419
+ const errorMessage = errorObject?.message || data.message || errorString || rawBody || `Request failed with status ${response.status}`;
420
+ const errorCode = errorString || errorObject?.code || data.code || data.message || errorObject?.message || "API_ERROR";
421
+ const errorDetails = {
422
+ ...errorObject?.details,
423
+ statusText: response.statusText,
424
+ ...errorObject ? {} : { rawResponse: data }
425
+ };
426
+ const error = {
427
+ code: errorCode,
428
+ message: errorMessage,
429
+ details: errorDetails
430
+ };
431
+ if (response.status === 401) {
432
+ const refreshed = await auth.tryRefreshToken();
433
+ if (refreshed) {
434
+ return request(method, path, options);
435
+ }
436
+ throw new AuthError(
437
+ "Session expired. Run 'waniwani login' to re-authenticate."
438
+ );
439
+ }
440
+ throw new ApiError(
441
+ error.message,
442
+ error.code,
443
+ response.status,
444
+ error.details
445
+ );
446
+ }
447
+ return data.data;
448
+ }
449
+ var api = {
450
+ get: (path, options) => request("GET", path, options),
451
+ post: (path, body, options) => request("POST", path, { body, ...options }),
452
+ delete: (path, options) => request("DELETE", path, options),
453
+ getBaseUrl: () => config.getApiUrl()
454
+ };
455
+
456
+ // src/lib/git-auth.ts
457
+ function getGitHubApiBaseUrl(remoteUrl) {
458
+ try {
459
+ const parsed = new URL(remoteUrl);
460
+ return parsed.hostname === "github.com" || parsed.hostname === "www.github.com" ? "https://api.github.com" : `${parsed.protocol}//${parsed.host}/api/v3`;
461
+ } catch {
462
+ return null;
463
+ }
464
+ }
465
+ function parseCloneUrlAuth(cloneUrl) {
466
+ try {
467
+ const parsed = new URL(cloneUrl);
468
+ const username = decodeURIComponent(parsed.username);
469
+ const password = decodeURIComponent(parsed.password);
470
+ if (username && password) {
471
+ parsed.username = "";
472
+ parsed.password = "";
473
+ const remoteUrl = parsed.toString();
474
+ return {
475
+ cloneUrl,
476
+ remoteUrl,
477
+ credentials: { username, password },
478
+ githubApiBaseUrl: getGitHubApiBaseUrl(remoteUrl)
479
+ };
480
+ }
481
+ } catch {
482
+ }
483
+ return {
484
+ cloneUrl,
485
+ remoteUrl: cloneUrl,
486
+ credentials: null,
487
+ githubApiBaseUrl: null
488
+ };
489
+ }
490
+ async function getGitAuthContext(mcpId) {
491
+ try {
492
+ const gitAuth = await api.get(
493
+ `/api/mcp/repositories/${mcpId}/git-auth`
494
+ );
495
+ const parsedRemote = new URL(gitAuth.remoteUrl);
496
+ parsedRemote.username = gitAuth.username;
497
+ parsedRemote.password = gitAuth.token;
498
+ const cloneUrl = parsedRemote.toString();
499
+ return {
500
+ cloneUrl,
501
+ remoteUrl: gitAuth.remoteUrl,
502
+ credentials: {
503
+ username: gitAuth.username,
504
+ password: gitAuth.token
505
+ },
506
+ githubApiBaseUrl: getGitHubApiBaseUrl(gitAuth.remoteUrl)
507
+ };
508
+ } catch (error) {
509
+ if (error instanceof ApiError && error.statusCode !== 404) {
510
+ throw error;
511
+ }
512
+ const { cloneUrl } = await api.get(
513
+ `/api/mcp/repositories/${mcpId}/clone-url`
514
+ );
515
+ return parseCloneUrlAuth(cloneUrl);
516
+ }
517
+ }
518
+ function runGitWithCredentials(args, options) {
519
+ const { cwd, stdio = "ignore", credentials = null } = options ?? {};
520
+ if (!credentials) {
521
+ execFileSync("git", args, { cwd, stdio });
522
+ return;
523
+ }
524
+ const dir = mkdtempSync(join3(tmpdir(), "waniwani-askpass-"));
525
+ const askpassPath = join3(dir, "askpass.sh");
526
+ try {
527
+ writeFileSync(
528
+ askpassPath,
529
+ `#!/bin/sh
530
+ case "$1" in
531
+ *sername*) printf '%s\\n' "$WANIWANI_GIT_USERNAME" ;;
532
+ *assword*) printf '%s\\n' "$WANIWANI_GIT_PASSWORD" ;;
533
+ *) printf '\\n' ;;
534
+ esac
535
+ `,
536
+ "utf-8"
537
+ );
538
+ chmodSync(askpassPath, 448);
539
+ execFileSync("git", ["-c", "credential.helper=", ...args], {
540
+ cwd,
541
+ stdio,
542
+ env: {
543
+ ...process.env,
544
+ GIT_ASKPASS: askpassPath,
545
+ GIT_TERMINAL_PROMPT: "0",
546
+ WANIWANI_GIT_USERNAME: credentials.username,
547
+ WANIWANI_GIT_PASSWORD: credentials.password
548
+ }
549
+ });
550
+ } finally {
551
+ rmSync(dir, { recursive: true, force: true });
552
+ }
553
+ }
554
+ var REVOKE_TIMEOUT_MS = 5e3;
555
+ async function revokeGitHubInstallationToken(auth2) {
556
+ if (!auth2.credentials?.password || !auth2.githubApiBaseUrl) return;
557
+ const controller = new AbortController();
558
+ const timeout = setTimeout(() => controller.abort(), REVOKE_TIMEOUT_MS);
559
+ try {
560
+ await fetch(`${auth2.githubApiBaseUrl}/installation/token`, {
561
+ method: "DELETE",
562
+ headers: {
563
+ Accept: "application/vnd.github+json",
564
+ Authorization: `Bearer ${auth2.credentials.password}`,
565
+ "X-GitHub-Api-Version": "2022-11-28"
566
+ },
567
+ signal: controller.signal
568
+ });
569
+ } catch {
570
+ } finally {
571
+ clearTimeout(timeout);
572
+ }
573
+ }
574
+
575
+ // src/lib/sync.ts
576
+ import { existsSync as existsSync3 } from "fs";
577
+ import { mkdir as mkdir2, readdir, readFile as readFile2, stat, writeFile as writeFile2 } from "fs/promises";
578
+ import { dirname, join as join4, relative } from "path";
579
+ import ignore from "ignore";
580
+
581
+ // src/lib/utils.ts
582
+ async function requireMcpId(mcpId) {
583
+ if (mcpId) return mcpId;
584
+ const configMcpId = await config.getMcpId();
585
+ if (!configMcpId) {
586
+ throw new McpError(
587
+ "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
588
+ );
589
+ }
590
+ return configMcpId;
591
+ }
592
+ async function requireSessionId() {
593
+ const sessionId = await config.getSessionId();
594
+ if (!sessionId) {
595
+ throw new McpError(
596
+ "No active session. Run 'waniwani mcp preview' to start development."
597
+ );
598
+ }
599
+ return sessionId;
600
+ }
601
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
602
+ ".png",
603
+ ".jpg",
604
+ ".jpeg",
605
+ ".gif",
606
+ ".ico",
607
+ ".webp",
608
+ ".svg",
609
+ ".woff",
610
+ ".woff2",
611
+ ".ttf",
612
+ ".eot",
613
+ ".otf",
614
+ ".zip",
615
+ ".tar",
616
+ ".gz",
617
+ ".pdf",
618
+ ".exe",
619
+ ".dll",
620
+ ".so",
621
+ ".dylib",
622
+ ".bin",
623
+ ".mp3",
624
+ ".mp4",
625
+ ".wav",
626
+ ".ogg",
627
+ ".webm"
628
+ ]);
629
+ function isBinaryPath(filePath) {
630
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
631
+ return BINARY_EXTENSIONS.has(ext);
632
+ }
633
+ function detectBinary(buffer) {
634
+ const sample = buffer.subarray(0, 8192);
635
+ return sample.includes(0);
636
+ }
637
+
638
+ // src/lib/sync.ts
639
+ var PROJECT_DIR = ".waniwani";
640
+ async function findProjectRoot(startDir) {
641
+ let current = startDir;
642
+ const root = dirname(current);
643
+ while (current !== root) {
644
+ if (existsSync3(join4(current, PROJECT_DIR))) {
645
+ return current;
646
+ }
647
+ const parent = dirname(current);
648
+ if (parent === current) break;
649
+ current = parent;
650
+ }
651
+ if (existsSync3(join4(current, PROJECT_DIR))) {
652
+ return current;
653
+ }
654
+ return null;
655
+ }
656
+ var DEFAULT_IGNORE_PATTERNS = [
657
+ ".waniwani",
658
+ ".git",
659
+ "node_modules",
660
+ ".DS_Store",
661
+ "*.log",
662
+ ".cache",
663
+ "dist",
664
+ "coverage",
665
+ ".turbo",
666
+ ".next",
667
+ ".nuxt",
668
+ ".vercel"
669
+ ];
670
+ var DEFAULT_ENV_IGNORE_PATTERNS = [".env", ".env.*"];
671
+ async function loadIgnorePatterns(projectRoot, options = {}) {
672
+ const { includeEnvFiles = false } = options;
673
+ const ig = ignore();
674
+ ig.add(DEFAULT_IGNORE_PATTERNS);
675
+ if (!includeEnvFiles) {
676
+ ig.add(DEFAULT_ENV_IGNORE_PATTERNS);
677
+ }
678
+ const gitignorePath = join4(projectRoot, ".gitignore");
679
+ if (existsSync3(gitignorePath)) {
680
+ try {
681
+ const content = await readFile2(gitignorePath, "utf-8");
682
+ ig.add(content);
683
+ } catch {
684
+ }
685
+ }
686
+ if (includeEnvFiles) {
687
+ ig.add(["!.env", "!.env.*"]);
688
+ }
689
+ return ig;
690
+ }
691
+ async function collectFiles(projectRoot, options = {}) {
692
+ const ig = await loadIgnorePatterns(projectRoot, options);
693
+ const files = [];
694
+ async function walk(dir) {
695
+ const entries = await readdir(dir, { withFileTypes: true });
696
+ for (const entry of entries) {
697
+ const fullPath = join4(dir, entry.name);
698
+ const relativePath = relative(projectRoot, fullPath);
699
+ if (ig.ignores(relativePath)) {
700
+ continue;
701
+ }
702
+ if (entry.isDirectory()) {
703
+ await walk(fullPath);
704
+ } else if (entry.isFile()) {
705
+ try {
706
+ const content = await readFile2(fullPath);
707
+ const isBinary = isBinaryPath(fullPath) || detectBinary(content);
708
+ files.push({
709
+ path: relativePath,
710
+ content: isBinary ? content.toString("base64") : content.toString("utf8"),
711
+ encoding: isBinary ? "base64" : "utf8"
712
+ });
713
+ } catch {
714
+ }
715
+ }
716
+ }
717
+ }
718
+ await walk(projectRoot);
719
+ return files;
720
+ }
721
+ async function pullFilesFromGithub(mcpId, targetDir) {
722
+ const result = await api.get(
723
+ `/api/mcp/repositories/${mcpId}/files`
724
+ );
725
+ const writtenFiles = [];
726
+ for (const file of result.files) {
727
+ const localPath = join4(targetDir, file.path);
728
+ const dir = dirname(localPath);
729
+ await mkdir2(dir, { recursive: true });
730
+ if (file.encoding === "base64") {
731
+ await writeFile2(localPath, Buffer.from(file.content, "base64"));
732
+ } else {
733
+ await writeFile2(localPath, file.content, "utf8");
734
+ }
735
+ writtenFiles.push(file.path);
736
+ }
737
+ return { count: writtenFiles.length, files: writtenFiles };
738
+ }
739
+ async function collectSingleFile(projectRoot, filePath) {
740
+ const fullPath = join4(projectRoot, filePath);
741
+ const relativePath = relative(projectRoot, fullPath);
742
+ if (!existsSync3(fullPath)) {
743
+ return null;
744
+ }
745
+ try {
746
+ const fileStat = await stat(fullPath);
747
+ if (!fileStat.isFile()) {
748
+ return null;
749
+ }
750
+ const content = await readFile2(fullPath);
751
+ const isBinary = isBinaryPath(fullPath) || detectBinary(content);
752
+ return {
753
+ path: relativePath,
754
+ content: isBinary ? content.toString("base64") : content.toString("utf8"),
755
+ encoding: isBinary ? "base64" : "utf8"
756
+ };
757
+ } catch {
758
+ return null;
759
+ }
760
+ }
761
+
762
+ // src/commands/git-credential-helper.ts
763
+ function parseCredentialInput(input) {
764
+ const fields = {};
765
+ for (const line of input.split("\n")) {
766
+ const trimmed = line.trim();
767
+ if (!trimmed) continue;
768
+ const eqIdx = trimmed.indexOf("=");
769
+ if (eqIdx > 0) {
770
+ fields[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
771
+ }
772
+ }
773
+ return fields;
774
+ }
775
+ var gitCredentialHelperCommand = new Command3("git-credential-helper").description("Git credential helper (used by git, not called directly)").argument("<operation>", "get, store, or erase").action(async (operation) => {
776
+ if (operation !== "get") {
777
+ process.exit(0);
778
+ }
779
+ try {
780
+ const input = readFileSync(0, "utf-8");
781
+ const fields = parseCredentialInput(input);
782
+ if (fields.protocol && fields.protocol !== "https") {
783
+ process.exit(0);
784
+ }
785
+ const projectRoot = await findProjectRoot(process.cwd());
786
+ if (!projectRoot) {
787
+ process.stderr.write(
788
+ "waniwani: not in a WaniWani project (no .waniwani/ found)\n"
789
+ );
790
+ process.exit(1);
791
+ }
792
+ const configPath = join5(projectRoot, LOCAL_CONFIG_DIR, CONFIG_FILE_NAME);
793
+ const configData = JSON.parse(readFileSync(configPath, "utf-8"));
794
+ const mcpId = configData.mcpId;
795
+ if (!mcpId) {
796
+ process.stderr.write("waniwani: no mcpId in config\n");
797
+ process.exit(1);
798
+ }
799
+ const gitAuth = await getGitAuthContext(mcpId);
800
+ if (!gitAuth.credentials) {
801
+ process.stderr.write("waniwani: no credentials returned from API\n");
802
+ process.exit(1);
803
+ }
804
+ process.stdout.write(
805
+ `username=${gitAuth.credentials.username}
806
+ password=${gitAuth.credentials.password}
807
+ `
808
+ );
809
+ } catch (error) {
810
+ const message = error instanceof Error ? error.message : String(error);
811
+ process.stderr.write(`waniwani credential helper error: ${message}
812
+ `);
813
+ process.exit(1);
814
+ }
815
+ });
351
816
 
352
817
  // src/commands/login.ts
818
+ import { spawn } from "child_process";
819
+ import { createServer } from "http";
820
+ import chalk3 from "chalk";
821
+ import { Command as Command4 } from "commander";
822
+ import ora from "ora";
353
823
  var LOGO_LINES = [
354
824
  "\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557",
355
825
  "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551",
@@ -655,7 +1125,7 @@ async function exchangeCodeForToken(code, codeVerifier, clientId, resource) {
655
1125
  }
656
1126
  return response.json();
657
1127
  }
658
- var loginCommand = new Command3("login").description("Log in to WaniWani").option("--no-browser", "Don't open the browser automatically").action(async (options, command) => {
1128
+ var loginCommand = new Command4("login").description("Log in to WaniWani").option("--no-browser", "Don't open the browser automatically").action(async (options, command) => {
659
1129
  const globalOptions = command.optsWithGlobals();
660
1130
  const json = globalOptions.json ?? false;
661
1131
  try {
@@ -761,8 +1231,8 @@ var loginCommand = new Command3("login").description("Log in to WaniWani").optio
761
1231
  });
762
1232
 
763
1233
  // src/commands/logout.ts
764
- import { Command as Command4 } from "commander";
765
- var logoutCommand = new Command4("logout").description("Log out from WaniWani").action(async (_, command) => {
1234
+ import { Command as Command5 } from "commander";
1235
+ var logoutCommand = new Command5("logout").description("Log out from WaniWani").action(async (_options, command) => {
766
1236
  const globalOptions = command.optsWithGlobals();
767
1237
  const json = globalOptions.json ?? false;
768
1238
  try {
@@ -790,109 +1260,34 @@ var logoutCommand = new Command4("logout").description("Log out from WaniWani").
790
1260
  import { Command as Command21 } from "commander";
791
1261
 
792
1262
  // src/commands/mcp/clone.ts
793
- import { execSync } from "child_process";
794
- import { existsSync as existsSync3 } from "fs";
795
- import { readFile as readFile2 } from "fs/promises";
796
- import { join as join3 } from "path";
797
- import { Command as Command5 } from "commander";
1263
+ import { execFileSync as execFileSync3, execSync } from "child_process";
1264
+ import { existsSync as existsSync4 } from "fs";
1265
+ import { readFile as readFile3 } from "fs/promises";
1266
+ import { join as join6 } from "path";
1267
+ import { Command as Command6 } from "commander";
798
1268
  import ora2 from "ora";
799
1269
 
800
- // src/lib/api.ts
801
- var ApiError = class extends CLIError {
802
- constructor(message, code, statusCode, details) {
803
- super(message, code, details);
804
- this.statusCode = statusCode;
805
- this.name = "ApiError";
806
- }
807
- };
808
- async function request(method, path, options) {
809
- const {
810
- body,
811
- requireAuth = true,
812
- headers: extraHeaders = {}
813
- } = options || {};
814
- const headers = {
815
- "Content-Type": "application/json",
816
- ...extraHeaders
817
- };
818
- if (requireAuth) {
819
- const token = await auth.getAccessToken();
820
- if (!token) {
821
- throw new AuthError(
822
- "Not logged in. Run 'waniwani login' to authenticate."
823
- );
824
- }
825
- headers.Authorization = `Bearer ${token}`;
826
- }
827
- const baseUrl = await config.getApiUrl();
828
- const url = `${baseUrl}${path}`;
829
- const response = await fetch(url, {
830
- method,
831
- headers,
832
- body: body ? JSON.stringify(body) : void 0
833
- });
834
- if (response.status === 204) {
835
- return void 0;
836
- }
837
- let data;
838
- let rawBody;
839
- try {
840
- rawBody = await response.text();
841
- data = JSON.parse(rawBody);
842
- } catch {
843
- throw new ApiError(
844
- rawBody || `Request failed with status ${response.status}`,
845
- "API_ERROR",
846
- response.status,
847
- { statusText: response.statusText }
848
- );
849
- }
850
- if (!response.ok || data.error) {
851
- const errorMessage = data.error?.message || data.message || data.error || rawBody || `Request failed with status ${response.status}`;
852
- const errorCode = data.error?.code || data.code || "API_ERROR";
853
- const errorDetails = {
854
- ...data.error?.details,
855
- statusText: response.statusText,
856
- ...data.error ? {} : { rawResponse: data }
857
- };
858
- const error = {
859
- code: errorCode,
860
- message: errorMessage,
861
- details: errorDetails
862
- };
863
- if (response.status === 401) {
864
- const refreshed = await auth.tryRefreshToken();
865
- if (refreshed) {
866
- return request(method, path, options);
867
- }
868
- throw new AuthError(
869
- "Session expired. Run 'waniwani login' to re-authenticate."
870
- );
871
- }
872
- throw new ApiError(
873
- error.message,
874
- error.code,
875
- response.status,
876
- error.details
877
- );
878
- }
879
- return data.data;
1270
+ // src/lib/credential-helper-setup.ts
1271
+ import { execFileSync as execFileSync2 } from "child_process";
1272
+ import { realpathSync } from "fs";
1273
+ function setupGitCredentialHelper(repoDir) {
1274
+ const binaryPath = realpathSync(process.argv[1]);
1275
+ const helperCommand = `!${process.execPath} ${binaryPath} git-credential-helper`;
1276
+ execFileSync2(
1277
+ "git",
1278
+ ["config", "--local", "credential.helper", helperCommand],
1279
+ { cwd: repoDir, stdio: "ignore" }
1280
+ );
880
1281
  }
881
- var api = {
882
- get: (path, options) => request("GET", path, options),
883
- post: (path, body, options) => request("POST", path, { body, ...options }),
884
- delete: (path, options) => request("DELETE", path, options),
885
- getBaseUrl: () => config.getApiUrl()
886
- };
887
1282
 
888
1283
  // src/commands/mcp/clone.ts
889
1284
  async function loadParentConfig(cwd) {
890
- const parentConfigPath = join3(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE_NAME);
891
- if (!existsSync3(parentConfigPath)) {
1285
+ const parentConfigPath = join6(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE_NAME);
1286
+ if (!existsSync4(parentConfigPath)) {
892
1287
  return null;
893
1288
  }
894
1289
  try {
895
- const content = await readFile2(parentConfigPath, "utf-8");
1290
+ const content = await readFile3(parentConfigPath, "utf-8");
896
1291
  const config2 = JSON.parse(content);
897
1292
  const { mcpId: _, sessionId: __, ...rest } = config2;
898
1293
  return rest;
@@ -910,97 +1305,110 @@ function checkGitInstalled() {
910
1305
  );
911
1306
  }
912
1307
  }
913
- var cloneCommand = new Command5("clone").description("Clone an existing MCP project to a local directory").argument("<name>", "Name of the MCP to clone").argument("[directory]", "Directory to clone into (defaults to MCP name)").action(async (name, directory, command) => {
914
- const globalOptions = command.optsWithGlobals();
915
- const json = globalOptions.json ?? false;
916
- try {
917
- const cwd = process.cwd();
918
- const dirName = directory ?? name;
919
- const projectDir = join3(cwd, dirName);
920
- if (existsSync3(projectDir)) {
1308
+ var cloneCommand = new Command6("clone").description("Clone an existing MCP project to a local directory").argument("<name>", "Name of the MCP to clone").argument("[directory]", "Directory to clone into (defaults to MCP name)").action(
1309
+ async (name, directory, _options, command) => {
1310
+ const globalOptions = command.optsWithGlobals();
1311
+ const json = globalOptions.json ?? false;
1312
+ try {
1313
+ const cwd = process.cwd();
1314
+ const dirName = directory ?? name;
1315
+ const projectDir = join6(cwd, dirName);
1316
+ if (existsSync4(projectDir)) {
1317
+ if (json) {
1318
+ formatOutput(
1319
+ {
1320
+ success: false,
1321
+ error: `Directory "${dirName}" already exists`
1322
+ },
1323
+ true
1324
+ );
1325
+ } else {
1326
+ console.error(`Error: Directory "${dirName}" already exists`);
1327
+ }
1328
+ process.exit(1);
1329
+ }
1330
+ checkGitInstalled();
1331
+ const spinner = ora2("Fetching MCPs...").start();
1332
+ const mcps = await api.get(
1333
+ "/api/mcp/repositories"
1334
+ );
1335
+ const mcp = mcps.find((m) => m.name === name);
1336
+ if (!mcp) {
1337
+ spinner.fail("MCP not found");
1338
+ throw new McpError(
1339
+ `MCP "${name}" not found. Run 'waniwani mcp list' to see available MCPs.`
1340
+ );
1341
+ }
1342
+ spinner.text = "Cloning repository...";
1343
+ const gitAuth = await getGitAuthContext(mcp.id);
1344
+ try {
1345
+ runGitWithCredentials(["clone", gitAuth.remoteUrl, projectDir], {
1346
+ stdio: "ignore",
1347
+ credentials: gitAuth.credentials
1348
+ });
1349
+ } catch {
1350
+ spinner.fail("Failed to clone repository");
1351
+ throw new CLIError(
1352
+ "Failed to clone repository. Ensure git is configured correctly.",
1353
+ "CLONE_FAILED"
1354
+ );
1355
+ } finally {
1356
+ await revokeGitHubInstallationToken(gitAuth);
1357
+ }
1358
+ execFileSync3(
1359
+ "git",
1360
+ ["remote", "set-url", "origin", mcp.githubCloneUrl],
1361
+ {
1362
+ cwd: projectDir,
1363
+ stdio: "ignore"
1364
+ }
1365
+ );
1366
+ const parentConfig = await loadParentConfig(cwd);
1367
+ await initConfigAt(projectDir, {
1368
+ ...parentConfig,
1369
+ mcpId: mcp.id
1370
+ });
1371
+ setupGitCredentialHelper(projectDir);
1372
+ spinner.succeed("Repository cloned");
921
1373
  if (json) {
922
1374
  formatOutput(
923
1375
  {
924
- success: false,
925
- error: `Directory "${dirName}" already exists`
1376
+ success: true,
1377
+ projectDir,
1378
+ mcpId: mcp.id
926
1379
  },
927
1380
  true
928
1381
  );
929
1382
  } else {
930
- console.error(`Error: Directory "${dirName}" already exists`);
1383
+ console.log();
1384
+ formatSuccess(`MCP "${name}" cloned!`, false);
1385
+ console.log();
1386
+ console.log("Next steps:");
1387
+ console.log(` cd ${dirName}`);
1388
+ console.log(" waniwani mcp preview # Start developing");
1389
+ console.log(" git push origin main # Deploy");
931
1390
  }
1391
+ } catch (error) {
1392
+ handleError(error, json);
932
1393
  process.exit(1);
933
1394
  }
934
- checkGitInstalled();
935
- const spinner = ora2("Fetching MCPs...").start();
936
- const mcps = await api.get(
937
- "/api/mcp/repositories"
938
- );
939
- const mcp = mcps.find((m) => m.name === name);
940
- if (!mcp) {
941
- spinner.fail("MCP not found");
942
- throw new McpError(
943
- `MCP "${name}" not found. Run 'waniwani mcp list' to see available MCPs.`
944
- );
945
- }
946
- spinner.text = "Cloning repository...";
947
- const { cloneUrl } = await api.get(
948
- `/api/mcp/repositories/${mcp.id}/clone-url`
949
- );
950
- try {
951
- execSync(`git clone "${cloneUrl}" "${projectDir}"`, {
952
- stdio: "ignore"
953
- });
954
- } catch {
955
- spinner.fail("Failed to clone repository");
956
- throw new CLIError(
957
- "Failed to clone repository. Ensure git is configured correctly.",
958
- "CLONE_FAILED"
959
- );
960
- }
961
- const parentConfig = await loadParentConfig(cwd);
962
- await initConfigAt(projectDir, {
963
- ...parentConfig,
964
- mcpId: mcp.id
965
- });
966
- spinner.succeed("Repository cloned");
967
- if (json) {
968
- formatOutput(
969
- {
970
- success: true,
971
- projectDir,
972
- mcpId: mcp.id
973
- },
974
- true
975
- );
976
- } else {
977
- console.log();
978
- formatSuccess(`MCP "${name}" cloned!`, false);
979
- console.log();
980
- console.log("Next steps:");
981
- console.log(` cd ${dirName}`);
982
- console.log(" waniwani mcp preview # Start developing");
983
- }
984
- } catch (error) {
985
- handleError(error, json);
986
- process.exit(1);
987
1395
  }
988
- });
1396
+ );
989
1397
 
990
1398
  // src/commands/mcp/create.ts
991
- import { execSync as execSync2 } from "child_process";
992
- import { existsSync as existsSync4 } from "fs";
993
- import { readFile as readFile3 } from "fs/promises";
994
- import { join as join4 } from "path";
995
- import { Command as Command6 } from "commander";
1399
+ import { execFileSync as execFileSync4, execSync as execSync2 } from "child_process";
1400
+ import { existsSync as existsSync5 } from "fs";
1401
+ import { readFile as readFile4 } from "fs/promises";
1402
+ import { join as join7 } from "path";
1403
+ import { Command as Command7 } from "commander";
996
1404
  import ora3 from "ora";
997
1405
  async function loadParentConfig2(cwd) {
998
- const parentConfigPath = join4(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE_NAME);
999
- if (!existsSync4(parentConfigPath)) {
1406
+ const parentConfigPath = join7(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE_NAME);
1407
+ if (!existsSync5(parentConfigPath)) {
1000
1408
  return null;
1001
1409
  }
1002
1410
  try {
1003
- const content = await readFile3(parentConfigPath, "utf-8");
1411
+ const content = await readFile4(parentConfigPath, "utf-8");
1004
1412
  const config2 = JSON.parse(content);
1005
1413
  const { mcpId: _, sessionId: __, ...rest } = config2;
1006
1414
  return rest;
@@ -1018,13 +1426,13 @@ function checkGitInstalled2() {
1018
1426
  );
1019
1427
  }
1020
1428
  }
1021
- var createCommand = new Command6("create").description("Create a new MCP project").argument("<name>", "Name for the MCP project").action(async (name, _options, command) => {
1429
+ var createCommand = new Command7("create").description("Create a new MCP project").argument("<name>", "Name for the MCP project").action(async (name, _options, command) => {
1022
1430
  const globalOptions = command.optsWithGlobals();
1023
1431
  const json = globalOptions.json ?? false;
1024
1432
  try {
1025
1433
  const cwd = process.cwd();
1026
- const projectDir = join4(cwd, name);
1027
- if (existsSync4(projectDir)) {
1434
+ const projectDir = join7(cwd, name);
1435
+ if (existsSync5(projectDir)) {
1028
1436
  if (json) {
1029
1437
  formatOutput(
1030
1438
  {
@@ -1044,12 +1452,11 @@ var createCommand = new Command6("create").description("Create a new MCP project
1044
1452
  name
1045
1453
  });
1046
1454
  spinner.text = "Cloning repository...";
1047
- const { cloneUrl } = await api.get(
1048
- `/api/mcp/repositories/${result.id}/clone-url`
1049
- );
1455
+ const gitAuth = await getGitAuthContext(result.id);
1050
1456
  try {
1051
- execSync2(`git clone "${cloneUrl}" "${projectDir}"`, {
1052
- stdio: "ignore"
1457
+ runGitWithCredentials(["clone", gitAuth.remoteUrl, projectDir], {
1458
+ stdio: "ignore",
1459
+ credentials: gitAuth.credentials
1053
1460
  });
1054
1461
  } catch {
1055
1462
  spinner.fail("Failed to clone repository");
@@ -1057,12 +1464,23 @@ var createCommand = new Command6("create").description("Create a new MCP project
1057
1464
  `Failed to clone repository. Ensure git is configured correctly.`,
1058
1465
  "CLONE_FAILED"
1059
1466
  );
1467
+ } finally {
1468
+ await revokeGitHubInstallationToken(gitAuth);
1060
1469
  }
1470
+ execFileSync4(
1471
+ "git",
1472
+ ["remote", "set-url", "origin", result.githubCloneUrl],
1473
+ {
1474
+ cwd: projectDir,
1475
+ stdio: "ignore"
1476
+ }
1477
+ );
1061
1478
  const parentConfig = await loadParentConfig2(cwd);
1062
1479
  await initConfigAt(projectDir, {
1063
1480
  ...parentConfig,
1064
1481
  mcpId: result.id
1065
1482
  });
1483
+ setupGitCredentialHelper(projectDir);
1066
1484
  spinner.succeed("MCP project created");
1067
1485
  if (json) {
1068
1486
  formatOutput(
@@ -1080,6 +1498,7 @@ var createCommand = new Command6("create").description("Create a new MCP project
1080
1498
  console.log("Next steps:");
1081
1499
  console.log(` cd ${name}`);
1082
1500
  console.log(" waniwani mcp preview # Start developing");
1501
+ console.log(" git push origin main # Deploy");
1083
1502
  }
1084
1503
  } catch (error) {
1085
1504
  handleError(error, json);
@@ -1090,68 +1509,9 @@ var createCommand = new Command6("create").description("Create a new MCP project
1090
1509
  // src/commands/mcp/delete.ts
1091
1510
  import { confirm } from "@inquirer/prompts";
1092
1511
  import chalk4 from "chalk";
1093
- import { Command as Command7 } from "commander";
1094
- import ora4 from "ora";
1095
-
1096
- // src/lib/utils.ts
1097
- async function requireMcpId(mcpId) {
1098
- if (mcpId) return mcpId;
1099
- const configMcpId = await config.getMcpId();
1100
- if (!configMcpId) {
1101
- throw new McpError(
1102
- "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
1103
- );
1104
- }
1105
- return configMcpId;
1106
- }
1107
- async function requireSessionId() {
1108
- const sessionId = await config.getSessionId();
1109
- if (!sessionId) {
1110
- throw new McpError(
1111
- "No active session. Run 'waniwani mcp preview' to start development."
1112
- );
1113
- }
1114
- return sessionId;
1115
- }
1116
- var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
1117
- ".png",
1118
- ".jpg",
1119
- ".jpeg",
1120
- ".gif",
1121
- ".ico",
1122
- ".webp",
1123
- ".svg",
1124
- ".woff",
1125
- ".woff2",
1126
- ".ttf",
1127
- ".eot",
1128
- ".otf",
1129
- ".zip",
1130
- ".tar",
1131
- ".gz",
1132
- ".pdf",
1133
- ".exe",
1134
- ".dll",
1135
- ".so",
1136
- ".dylib",
1137
- ".bin",
1138
- ".mp3",
1139
- ".mp4",
1140
- ".wav",
1141
- ".ogg",
1142
- ".webm"
1143
- ]);
1144
- function isBinaryPath(filePath) {
1145
- const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
1146
- return BINARY_EXTENSIONS.has(ext);
1147
- }
1148
- function detectBinary(buffer) {
1149
- const sample = buffer.subarray(0, 8192);
1150
- return sample.includes(0);
1151
- }
1152
-
1153
- // src/commands/mcp/delete.ts
1154
- var deleteCommand = new Command7("delete").description("Delete the MCP (includes all associated resources)").option("--mcp-id <id>", "Specific MCP ID").option("--force", "Skip confirmation prompt").action(async (options, command) => {
1512
+ import { Command as Command8 } from "commander";
1513
+ import ora4 from "ora";
1514
+ var deleteCommand = new Command8("delete").description("Delete the MCP (includes all associated resources)").option("--mcp-id <id>", "Specific MCP ID").option("--force", "Skip confirmation prompt").action(async (options, command) => {
1155
1515
  const globalOptions = command.optsWithGlobals();
1156
1516
  const json = globalOptions.json ?? false;
1157
1517
  try {
@@ -1195,13 +1555,13 @@ var deleteCommand = new Command7("delete").description("Delete the MCP (includes
1195
1555
  });
1196
1556
 
1197
1557
  // src/commands/mcp/file/index.ts
1198
- import { Command as Command11 } from "commander";
1558
+ import { Command as Command12 } from "commander";
1199
1559
 
1200
1560
  // src/commands/mcp/file/list.ts
1201
1561
  import chalk5 from "chalk";
1202
- import { Command as Command8 } from "commander";
1562
+ import { Command as Command9 } from "commander";
1203
1563
  import ora5 from "ora";
1204
- var listCommand = new Command8("list").description("List files in the MCP sandbox").argument("[path]", "Directory path (defaults to /app)", "/app").option("--mcp-id <id>", "Specific MCP ID").action(async (path, options, command) => {
1564
+ var listCommand = new Command9("list").description("List files in the MCP sandbox").argument("[path]", "Directory path (defaults to /app)", "/app").option("--mcp-id <id>", "Specific MCP ID").action(async (path, options, command) => {
1205
1565
  const globalOptions = command.optsWithGlobals();
1206
1566
  const json = globalOptions.json ?? false;
1207
1567
  try {
@@ -1243,10 +1603,10 @@ function formatSize(bytes) {
1243
1603
  }
1244
1604
 
1245
1605
  // src/commands/mcp/file/read.ts
1246
- import { writeFile as writeFile2 } from "fs/promises";
1247
- import { Command as Command9 } from "commander";
1606
+ import { writeFile as writeFile3 } from "fs/promises";
1607
+ import { Command as Command10 } from "commander";
1248
1608
  import ora6 from "ora";
1249
- var readCommand = new Command9("read").description("Read a file from the MCP sandbox").argument("<path>", "Path in sandbox (e.g., /app/src/index.ts)").option("--mcp-id <id>", "Specific MCP ID").option("--output <file>", "Write to local file instead of stdout").option("--base64", "Output as base64 (for binary files)").action(async (path, options, command) => {
1609
+ var readCommand = new Command10("read").description("Read a file from the MCP sandbox").argument("<path>", "Path in sandbox (e.g., /app/src/index.ts)").option("--mcp-id <id>", "Specific MCP ID").option("--output <file>", "Write to local file instead of stdout").option("--base64", "Output as base64 (for binary files)").action(async (path, options, command) => {
1250
1610
  const globalOptions = command.optsWithGlobals();
1251
1611
  const json = globalOptions.json ?? false;
1252
1612
  try {
@@ -1263,7 +1623,7 @@ var readCommand = new Command9("read").description("Read a file from the MCP san
1263
1623
  }
1264
1624
  if (options.output) {
1265
1625
  const buffer = result.encoding === "base64" ? Buffer.from(result.content, "base64") : Buffer.from(result.content, "utf8");
1266
- await writeFile2(options.output, buffer);
1626
+ await writeFile3(options.output, buffer);
1267
1627
  if (json) {
1268
1628
  formatOutput({ path, savedTo: options.output }, true);
1269
1629
  } else {
@@ -1284,10 +1644,10 @@ var readCommand = new Command9("read").description("Read a file from the MCP san
1284
1644
  });
1285
1645
 
1286
1646
  // src/commands/mcp/file/write.ts
1287
- import { readFile as readFile4 } from "fs/promises";
1288
- import { Command as Command10 } from "commander";
1647
+ import { readFile as readFile5 } from "fs/promises";
1648
+ import { Command as Command11 } from "commander";
1289
1649
  import ora7 from "ora";
1290
- var writeCommand = new Command10("write").description("Write a file to the MCP sandbox").argument("<path>", "Path in sandbox (e.g., /app/src/index.ts)").option("--mcp-id <id>", "Specific MCP ID").option("--content <content>", "Content to write").option("--file <localFile>", "Local file to upload").option("--base64", "Treat content as base64 encoded").action(async (path, options, command) => {
1650
+ var writeCommand = new Command11("write").description("Write a file to the MCP sandbox").argument("<path>", "Path in sandbox (e.g., /app/src/index.ts)").option("--mcp-id <id>", "Specific MCP ID").option("--content <content>", "Content to write").option("--file <localFile>", "Local file to upload").option("--base64", "Treat content as base64 encoded").action(async (path, options, command) => {
1291
1651
  const globalOptions = command.optsWithGlobals();
1292
1652
  const json = globalOptions.json ?? false;
1293
1653
  try {
@@ -1301,7 +1661,7 @@ var writeCommand = new Command10("write").description("Write a file to the MCP s
1301
1661
  encoding = "base64";
1302
1662
  }
1303
1663
  } else if (options.file) {
1304
- const fileBuffer = await readFile4(options.file);
1664
+ const fileBuffer = await readFile5(options.file);
1305
1665
  if (options.base64) {
1306
1666
  content = fileBuffer.toString("base64");
1307
1667
  encoding = "base64";
@@ -1334,13 +1694,13 @@ var writeCommand = new Command10("write").description("Write a file to the MCP s
1334
1694
  });
1335
1695
 
1336
1696
  // src/commands/mcp/file/index.ts
1337
- var fileCommand = new Command11("file").description("File operations in MCP sandbox").addCommand(readCommand).addCommand(writeCommand).addCommand(listCommand);
1697
+ var fileCommand = new Command12("file").description("File operations in MCP sandbox").addCommand(readCommand).addCommand(writeCommand).addCommand(listCommand);
1338
1698
 
1339
1699
  // src/commands/mcp/list.ts
1340
1700
  import chalk6 from "chalk";
1341
- import { Command as Command12 } from "commander";
1701
+ import { Command as Command13 } from "commander";
1342
1702
  import ora8 from "ora";
1343
- var listCommand2 = new Command12("list").description("List all MCPs in your organization").action(async (_, command) => {
1703
+ var listCommand2 = new Command13("list").description("List all MCPs in your organization").action(async (_options, command) => {
1344
1704
  const globalOptions = command.optsWithGlobals();
1345
1705
  const json = globalOptions.json ?? false;
1346
1706
  try {
@@ -1398,9 +1758,9 @@ var listCommand2 = new Command12("list").description("List all MCPs in your orga
1398
1758
 
1399
1759
  // src/commands/mcp/logs.ts
1400
1760
  import chalk7 from "chalk";
1401
- import { Command as Command13 } from "commander";
1761
+ import { Command as Command14 } from "commander";
1402
1762
  import ora9 from "ora";
1403
- var logsCommand = new Command13("logs").description("Stream logs from the MCP server").argument("[cmdId]", "Command ID (defaults to running server)").option("--mcp-id <id>", "Specific MCP ID").option("-f, --follow", "Keep streaming logs (default)", true).option("--no-follow", "Fetch logs and exit").action(async (cmdIdArg, options, command) => {
1763
+ var logsCommand = new Command14("logs").description("Stream logs from the MCP server").argument("[cmdId]", "Command ID (defaults to running server)").option("--mcp-id <id>", "Specific MCP ID").option("-f, --follow", "Keep streaming logs (default)", true).option("--no-follow", "Fetch logs and exit").action(async (cmdIdArg, options, command) => {
1404
1764
  const globalOptions = command.optsWithGlobals();
1405
1765
  const json = globalOptions.json ?? false;
1406
1766
  let reader;
@@ -1540,136 +1900,198 @@ Error: ${event.error}`));
1540
1900
  });
1541
1901
 
1542
1902
  // src/commands/mcp/preview.ts
1903
+ import { existsSync as existsSync6 } from "fs";
1904
+ import { join as join8 } from "path";
1543
1905
  import { watch } from "chokidar";
1544
- import { Command as Command14 } from "commander";
1906
+ import { Command as Command15, InvalidArgumentError } from "commander";
1545
1907
  import ora10 from "ora";
1546
1908
 
1547
- // src/lib/sync.ts
1548
- import { existsSync as existsSync5 } from "fs";
1549
- import { mkdir as mkdir2, readdir, readFile as readFile5, stat, writeFile as writeFile3 } from "fs/promises";
1550
- import { dirname, join as join5, relative } from "path";
1551
- import ignore from "ignore";
1552
- var PROJECT_DIR = ".waniwani";
1553
- async function findProjectRoot(startDir) {
1554
- let current = startDir;
1555
- const root = dirname(current);
1556
- while (current !== root) {
1557
- if (existsSync5(join5(current, PROJECT_DIR))) {
1558
- return current;
1909
+ // src/lib/async.ts
1910
+ async function withTimeout(promise, timeoutMs, options) {
1911
+ let timer;
1912
+ try {
1913
+ return await Promise.race([
1914
+ promise,
1915
+ new Promise((resolve) => {
1916
+ timer = setTimeout(() => {
1917
+ options?.onTimeout?.();
1918
+ resolve(null);
1919
+ }, timeoutMs);
1920
+ })
1921
+ ]);
1922
+ } finally {
1923
+ if (timer) {
1924
+ clearTimeout(timer);
1559
1925
  }
1560
- const parent = dirname(current);
1561
- if (parent === current) break;
1562
- current = parent;
1563
- }
1564
- if (existsSync5(join5(current, PROJECT_DIR))) {
1565
- return current;
1566
1926
  }
1567
- return null;
1568
1927
  }
1569
- var DEFAULT_IGNORE_PATTERNS = [
1570
- ".waniwani",
1571
- ".git",
1572
- "node_modules",
1573
- ".env",
1574
- ".env.*",
1575
- ".DS_Store",
1576
- "*.log",
1577
- ".cache",
1578
- "dist",
1579
- "coverage",
1580
- ".turbo",
1581
- ".next",
1582
- ".nuxt",
1583
- ".vercel"
1584
- ];
1585
- async function loadIgnorePatterns(projectRoot) {
1586
- const ig = ignore();
1587
- ig.add(DEFAULT_IGNORE_PATTERNS);
1588
- const gitignorePath = join5(projectRoot, ".gitignore");
1589
- if (existsSync5(gitignorePath)) {
1590
- try {
1591
- const content = await readFile5(gitignorePath, "utf-8");
1592
- ig.add(content);
1593
- } catch {
1594
- }
1928
+
1929
+ // src/commands/mcp/preview.ts
1930
+ var SHUTDOWN_MAX_WAIT_MS = 3e3;
1931
+ var SHUTDOWN_STEP_TIMEOUT_MS = 1200;
1932
+ var DEV_SERVER_VERIFY_ATTEMPTS = 4;
1933
+ var DEV_SERVER_VERIFY_INTERVAL_MS = 750;
1934
+ var DEFAULT_DEV_SERVER_MONITOR_INTERVAL_MS = 6e4;
1935
+ var MAX_INSTALL_TIMEOUT_MS = 3e5;
1936
+ function resolveSessionInfo(response) {
1937
+ const maybeLegacy = response;
1938
+ if (maybeLegacy?.sandbox?.id && maybeLegacy?.previewUrl) {
1939
+ return {
1940
+ id: maybeLegacy.sandbox.id,
1941
+ previewUrl: maybeLegacy.previewUrl,
1942
+ sandboxId: maybeLegacy.sandbox.sandboxId
1943
+ };
1595
1944
  }
1596
- return ig;
1945
+ const maybeSession = response;
1946
+ if (maybeSession?.id && maybeSession?.previewUrl) {
1947
+ return {
1948
+ id: maybeSession.id,
1949
+ previewUrl: maybeSession.previewUrl,
1950
+ sandboxId: maybeSession.sandboxId
1951
+ };
1952
+ }
1953
+ throw new CLIError("Invalid session response from API", "SESSION_ERROR");
1597
1954
  }
1598
- async function collectFiles(projectRoot) {
1599
- const ig = await loadIgnorePatterns(projectRoot);
1600
- const files = [];
1601
- async function walk(dir) {
1602
- const entries = await readdir(dir, { withFileTypes: true });
1603
- for (const entry of entries) {
1604
- const fullPath = join5(dir, entry.name);
1605
- const relativePath = relative(projectRoot, fullPath);
1606
- if (ig.ignores(relativePath)) {
1607
- continue;
1608
- }
1609
- if (entry.isDirectory()) {
1610
- await walk(fullPath);
1611
- } else if (entry.isFile()) {
1612
- try {
1613
- const content = await readFile5(fullPath);
1614
- const isBinary = isBinaryPath(fullPath) || detectBinary(content);
1615
- files.push({
1616
- path: relativePath,
1617
- content: isBinary ? content.toString("base64") : content.toString("utf8"),
1618
- encoding: isBinary ? "base64" : "utf8"
1619
- });
1620
- } catch {
1621
- }
1622
- }
1623
- }
1955
+ function detectPackageManager(projectRoot) {
1956
+ if (existsSync6(join8(projectRoot, "bun.lock")) || existsSync6(join8(projectRoot, "bun.lockb"))) {
1957
+ return "bun";
1624
1958
  }
1625
- await walk(projectRoot);
1626
- return files;
1959
+ if (existsSync6(join8(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
1960
+ if (existsSync6(join8(projectRoot, "yarn.lock"))) return "yarn";
1961
+ if (existsSync6(join8(projectRoot, "package-lock.json")) || existsSync6(join8(projectRoot, "npm-shrinkwrap.json"))) {
1962
+ return "npm-ci";
1963
+ }
1964
+ return "npm";
1627
1965
  }
1628
- async function pullFilesFromGithub(mcpId, targetDir) {
1629
- const result = await api.get(
1630
- `/api/mcp/repositories/${mcpId}/files`
1631
- );
1632
- const writtenFiles = [];
1633
- for (const file of result.files) {
1634
- const localPath = join5(targetDir, file.path);
1635
- const dir = dirname(localPath);
1636
- await mkdir2(dir, { recursive: true });
1637
- if (file.encoding === "base64") {
1638
- await writeFile3(localPath, Buffer.from(file.content, "base64"));
1639
- } else {
1640
- await writeFile3(localPath, file.content, "utf8");
1641
- }
1642
- writtenFiles.push(file.path);
1966
+ function getInstallCommand(pm) {
1967
+ switch (pm) {
1968
+ case "bun":
1969
+ return { command: "bun", args: ["install", "--frozen-lockfile"] };
1970
+ case "pnpm":
1971
+ return {
1972
+ command: "pnpm",
1973
+ args: ["install", "--frozen-lockfile", "--prefer-offline"]
1974
+ };
1975
+ case "yarn":
1976
+ return {
1977
+ command: "yarn",
1978
+ args: ["install", "--frozen-lockfile", "--prefer-offline"]
1979
+ };
1980
+ case "npm-ci":
1981
+ return {
1982
+ command: "npm",
1983
+ args: ["ci", "--no-audit", "--no-fund", "--prefer-offline"]
1984
+ };
1985
+ default:
1986
+ return {
1987
+ command: "npm",
1988
+ args: ["install", "--no-audit", "--no-fund"]
1989
+ };
1643
1990
  }
1644
- return { count: writtenFiles.length, files: writtenFiles };
1645
1991
  }
1646
- async function collectSingleFile(projectRoot, filePath) {
1647
- const fullPath = join5(projectRoot, filePath);
1648
- const relativePath = relative(projectRoot, fullPath);
1649
- if (!existsSync5(fullPath)) {
1650
- return null;
1992
+ function getNonFrozenInstallCommand(pm) {
1993
+ switch (pm) {
1994
+ case "bun":
1995
+ return { command: "bun", args: ["install"] };
1996
+ case "pnpm":
1997
+ return { command: "pnpm", args: ["install", "--prefer-offline"] };
1998
+ case "yarn":
1999
+ return { command: "yarn", args: ["install", "--prefer-offline"] };
2000
+ case "npm-ci":
2001
+ return { command: "npm", args: ["install", "--no-audit", "--no-fund"] };
2002
+ default:
2003
+ return null;
1651
2004
  }
2005
+ }
2006
+ function shouldRetryWithoutFrozenLockfile(result) {
2007
+ const output = `${result.stderr}
2008
+ ${result.stdout}`.toLowerCase();
2009
+ const npmLockMismatchDetected = output.includes("update your lock file") && output.includes("npm install") || output.includes("package-lock.json") && output.includes("not in sync") || output.includes("package-lock.json") && output.includes("are not in sync") || output.includes("npm ci") && output.includes("can only install packages") && output.includes("package-lock.json");
2010
+ return output.includes("frozen-lockfile") || output.includes("lockfile had changes") || output.includes("lockfile is frozen") || output.includes("lockfile") && output.includes("out of date") || npmLockMismatchDetected;
2011
+ }
2012
+ function isAlreadyRunningServerError(error) {
2013
+ const normalized = getNormalizedError(error);
2014
+ return normalized.includes("already_running") || normalized.includes("already running");
2015
+ }
2016
+ function isServerNotRunningError(error) {
2017
+ const normalized = getNormalizedError(error);
2018
+ return normalized.includes("server_not_running") || normalized.includes("server not running");
2019
+ }
2020
+ function getNormalizedError(error) {
2021
+ if (!(error instanceof CLIError)) return "";
2022
+ const details = JSON.stringify(error.details ?? {}).toLowerCase();
2023
+ return `${error.code} ${error.message} ${details}`.toLowerCase();
2024
+ }
2025
+ async function getServerStatusOrNull(sessionId) {
1652
2026
  try {
1653
- const fileStat = await stat(fullPath);
1654
- if (!fileStat.isFile()) {
2027
+ return await api.get(
2028
+ `/api/mcp/sessions/${sessionId}/server`
2029
+ );
2030
+ } catch (error) {
2031
+ if (isServerNotRunningError(error)) {
1655
2032
  return null;
1656
2033
  }
1657
- const content = await readFile5(fullPath);
1658
- const isBinary = isBinaryPath(fullPath) || detectBinary(content);
1659
- return {
1660
- path: relativePath,
1661
- content: isBinary ? content.toString("base64") : content.toString("utf8"),
1662
- encoding: isBinary ? "base64" : "utf8"
1663
- };
2034
+ throw error;
2035
+ }
2036
+ }
2037
+ function getDevCommand(pm) {
2038
+ switch (pm) {
2039
+ case "bun":
2040
+ return "bun run dev";
2041
+ case "pnpm":
2042
+ return "pnpm run dev";
2043
+ case "yarn":
2044
+ return "yarn run dev";
2045
+ default:
2046
+ return "npm run dev";
2047
+ }
2048
+ }
2049
+ function sleep(ms) {
2050
+ return new Promise((resolve) => {
2051
+ setTimeout(resolve, ms);
2052
+ });
2053
+ }
2054
+ function parseStatusPollIntervalMs(value) {
2055
+ const parsed = Number(value);
2056
+ if (!Number.isInteger(parsed) || parsed < 500) {
2057
+ throw new InvalidArgumentError(
2058
+ "Status poll interval must be an integer >= 500 milliseconds."
2059
+ );
2060
+ }
2061
+ return parsed;
2062
+ }
2063
+ async function getCommandOutputOrNull(sessionId, cmdId) {
2064
+ try {
2065
+ return await api.get(
2066
+ `/api/mcp/sessions/${sessionId}/commands/${cmdId}`
2067
+ );
1664
2068
  } catch {
1665
2069
  return null;
1666
2070
  }
1667
2071
  }
1668
-
1669
- // src/commands/mcp/preview.ts
1670
- var previewCommand = new Command14("preview").description("Start live development with sandbox and file watching").option("--mcp-id <id>", "Specific MCP ID").option("--no-watch", "Skip file watching").option("--no-logs", "Don't stream logs to terminal").action(async (options, command) => {
2072
+ async function cleanupPreviewSession(sessionId) {
2073
+ await withTimeout(
2074
+ api.post(`/api/mcp/sessions/${sessionId}/server`, { action: "stop" }).catch(() => void 0),
2075
+ SHUTDOWN_STEP_TIMEOUT_MS
2076
+ );
2077
+ await withTimeout(
2078
+ api.delete(`/api/mcp/sessions/${sessionId}`).catch(() => void 0),
2079
+ SHUTDOWN_STEP_TIMEOUT_MS
2080
+ );
2081
+ await withTimeout(
2082
+ config.setSessionId(null).catch(() => void 0),
2083
+ SHUTDOWN_STEP_TIMEOUT_MS
2084
+ );
2085
+ }
2086
+ var previewCommand = new Command15("preview").description("Start live development with sandbox and file watching").option("--mcp-id <id>", "Specific MCP ID").option("--no-watch", "Skip file watching").option("--no-logs", "Don't stream logs to terminal").option(
2087
+ "--status-poll-interval-ms <ms>",
2088
+ "Watch-mode server status polling interval in milliseconds (default: 60000)",
2089
+ parseStatusPollIntervalMs,
2090
+ DEFAULT_DEV_SERVER_MONITOR_INTERVAL_MS
2091
+ ).action(async (options, command) => {
1671
2092
  const globalOptions = command.optsWithGlobals();
1672
2093
  const json = globalOptions.json ?? false;
2094
+ const statusPollIntervalMs = options.statusPollIntervalMs ?? DEFAULT_DEV_SERVER_MONITOR_INTERVAL_MS;
1673
2095
  try {
1674
2096
  const projectRoot = await findProjectRoot(process.cwd());
1675
2097
  if (!projectRoot) {
@@ -1692,13 +2114,16 @@ var previewCommand = new Command14("preview").description("Start live developmen
1692
2114
  spinner.text = "Starting session...";
1693
2115
  let sessionId;
1694
2116
  let previewUrl;
2117
+ let sandboxId;
1695
2118
  try {
1696
2119
  const sessionResponse = await api.post(
1697
2120
  `/api/mcp/repositories/${mcpId}/session`,
1698
2121
  {}
1699
2122
  );
1700
- sessionId = sessionResponse.sandbox.id;
1701
- previewUrl = sessionResponse.previewUrl;
2123
+ const sessionInfo = resolveSessionInfo(sessionResponse);
2124
+ sessionId = sessionInfo.id;
2125
+ previewUrl = sessionInfo.previewUrl;
2126
+ sandboxId = sessionInfo.sandboxId;
1702
2127
  } catch {
1703
2128
  const existing = await api.get(
1704
2129
  `/api/mcp/repositories/${mcpId}/session`
@@ -1706,33 +2131,156 @@ var previewCommand = new Command14("preview").description("Start live developmen
1706
2131
  if (!existing) {
1707
2132
  throw new CLIError("Failed to start session", "SESSION_ERROR");
1708
2133
  }
1709
- sessionId = existing.id;
1710
- previewUrl = existing.previewUrl;
2134
+ const sessionInfo = resolveSessionInfo(existing);
2135
+ sessionId = sessionInfo.id;
2136
+ previewUrl = sessionInfo.previewUrl;
2137
+ sandboxId = sessionInfo.sandboxId;
1711
2138
  }
1712
2139
  await config.setSessionId(sessionId);
1713
2140
  spinner.text = "Syncing files to sandbox...";
1714
- const files = await collectFiles(projectRoot);
2141
+ const files = await collectFiles(projectRoot, { includeEnvFiles: true });
1715
2142
  if (files.length > 0) {
1716
2143
  await api.post(
1717
2144
  `/api/mcp/sessions/${sessionId}/files`,
1718
2145
  { files }
1719
2146
  );
1720
2147
  }
2148
+ const packageManager = detectPackageManager(projectRoot);
2149
+ let installCommand = getInstallCommand(packageManager);
2150
+ spinner.text = `Installing dependencies (${installCommand.command})...`;
2151
+ let installResult = await api.post(
2152
+ `/api/mcp/sessions/${sessionId}/commands`,
2153
+ {
2154
+ command: installCommand.command,
2155
+ args: installCommand.args,
2156
+ timeout: MAX_INSTALL_TIMEOUT_MS
2157
+ }
2158
+ );
2159
+ if (installResult.exitCode !== 0) {
2160
+ const nonFrozenInstallCommand = getNonFrozenInstallCommand(packageManager);
2161
+ if (nonFrozenInstallCommand && shouldRetryWithoutFrozenLockfile(installResult)) {
2162
+ installCommand = nonFrozenInstallCommand;
2163
+ spinner.text = `Retrying install without frozen lockfile (${installCommand.command})...`;
2164
+ installResult = await api.post(
2165
+ `/api/mcp/sessions/${sessionId}/commands`,
2166
+ {
2167
+ command: installCommand.command,
2168
+ args: installCommand.args,
2169
+ timeout: MAX_INSTALL_TIMEOUT_MS
2170
+ }
2171
+ );
2172
+ }
2173
+ }
2174
+ if (installResult.exitCode !== 0) {
2175
+ throw new CLIError(
2176
+ installResult.stderr || `Dependency install failed with exit code ${installResult.exitCode}`,
2177
+ "SANDBOX_HYDRATION_FAILED",
2178
+ {
2179
+ command: [installCommand.command, ...installCommand.args].join(" "),
2180
+ exitCode: installResult.exitCode
2181
+ }
2182
+ );
2183
+ }
2184
+ const devCommand = getDevCommand(packageManager);
2185
+ let serverCmdId;
2186
+ let didStartServer = false;
2187
+ const serverStatus = await getServerStatusOrNull(sessionId);
2188
+ const serverStatusPreviewUrl = serverStatus?.previewUrl;
2189
+ if (serverStatusPreviewUrl) {
2190
+ previewUrl = serverStatusPreviewUrl;
2191
+ }
2192
+ if (serverStatus?.running) {
2193
+ spinner.text = "Server already running, attaching...";
2194
+ serverCmdId = serverStatus.cmdId;
2195
+ } else {
2196
+ spinner.text = `Starting server (${devCommand})...`;
2197
+ try {
2198
+ const serverStart = await api.post(
2199
+ `/api/mcp/sessions/${sessionId}/server`,
2200
+ {
2201
+ action: "start",
2202
+ command: devCommand
2203
+ }
2204
+ );
2205
+ const serverStartPreviewUrl = serverStart.previewUrl;
2206
+ if (serverStartPreviewUrl) {
2207
+ previewUrl = serverStartPreviewUrl;
2208
+ }
2209
+ serverCmdId = serverStart.cmdId;
2210
+ didStartServer = true;
2211
+ } catch (error) {
2212
+ if (!isAlreadyRunningServerError(error)) {
2213
+ throw error;
2214
+ }
2215
+ const refreshedStatus = await getServerStatusOrNull(sessionId);
2216
+ if (!refreshedStatus?.running) {
2217
+ throw error;
2218
+ }
2219
+ const refreshedPreviewUrl = refreshedStatus.previewUrl;
2220
+ if (refreshedPreviewUrl) {
2221
+ previewUrl = refreshedPreviewUrl;
2222
+ }
2223
+ serverCmdId = refreshedStatus.cmdId;
2224
+ spinner.text = "Server already running, attaching...";
2225
+ }
2226
+ }
2227
+ if (didStartServer && !serverCmdId) {
2228
+ await cleanupPreviewSession(sessionId);
2229
+ throw new CLIError(
2230
+ "Server start command ID was not returned by the API.",
2231
+ "SERVER_START_FAILED",
2232
+ { command: devCommand }
2233
+ );
2234
+ }
2235
+ if (didStartServer && serverCmdId) {
2236
+ spinner.text = "Verifying server startup...";
2237
+ for (let attempt = 0; attempt < DEV_SERVER_VERIFY_ATTEMPTS; attempt++) {
2238
+ if (attempt > 0) {
2239
+ await sleep(DEV_SERVER_VERIFY_INTERVAL_MS);
2240
+ }
2241
+ const commandStatus = await api.get(
2242
+ `/api/mcp/sessions/${sessionId}/commands/${serverCmdId}?statusOnly=true`
2243
+ );
2244
+ if (commandStatus.exitCode === null) {
2245
+ continue;
2246
+ }
2247
+ const commandOutput = await api.get(
2248
+ `/api/mcp/sessions/${sessionId}/commands/${serverCmdId}`
2249
+ ).catch(() => null);
2250
+ await cleanupPreviewSession(sessionId);
2251
+ throw new CLIError(
2252
+ `Dev server command exited during startup with code ${commandStatus.exitCode}.`,
2253
+ "SERVER_START_FAILED",
2254
+ {
2255
+ command: devCommand,
2256
+ cmdId: serverCmdId,
2257
+ exitCode: commandStatus.exitCode,
2258
+ stdout: commandOutput?.stdout ?? "",
2259
+ stderr: commandOutput?.stderr ?? ""
2260
+ }
2261
+ );
2262
+ }
2263
+ }
1721
2264
  spinner.succeed("Development environment ready");
1722
2265
  console.log();
1723
2266
  formatSuccess("Live preview started!", false);
1724
2267
  console.log();
2268
+ if (sandboxId) {
2269
+ console.log(` Sandbox ID: ${sandboxId}`);
2270
+ }
1725
2271
  console.log(` Preview URL: ${previewUrl}`);
1726
2272
  console.log();
1727
2273
  console.log(` MCP Inspector:`);
1728
2274
  console.log(
1729
- ` npx @anthropic-ai/mcp-inspector@latest "${previewUrl}/mcp"`
2275
+ ` npx @modelcontextprotocol/inspector@latest --transport http --server-url "${previewUrl}/mcp"`
1730
2276
  );
1731
2277
  console.log();
1732
2278
  if (options.watch !== false) {
1733
2279
  console.log("Watching for file changes... (Ctrl+C to stop)");
1734
2280
  console.log();
1735
- const ig = await loadIgnorePatterns(projectRoot);
2281
+ const ig = await loadIgnorePatterns(projectRoot, {
2282
+ includeEnvFiles: true
2283
+ });
1736
2284
  const watcher = watch(projectRoot, {
1737
2285
  ignored: (path) => {
1738
2286
  const relative2 = path.replace(`${projectRoot}/`, "");
@@ -1767,111 +2315,86 @@ var previewCommand = new Command14("preview").description("Start live developmen
1767
2315
  const relativePath = filePath.replace(`${projectRoot}/`, "");
1768
2316
  console.log(` Deleted: ${relativePath}`);
1769
2317
  });
1770
- process.on("SIGINT", async () => {
2318
+ let shuttingDown = false;
2319
+ let serverMonitorInterval = null;
2320
+ const gracefulShutdown = async (exitCode = 0) => {
2321
+ if (shuttingDown) return;
2322
+ shuttingDown = true;
2323
+ if (serverMonitorInterval) {
2324
+ clearInterval(serverMonitorInterval);
2325
+ serverMonitorInterval = null;
2326
+ }
1771
2327
  console.log();
1772
2328
  console.log("Stopping development environment...");
1773
- await watcher.close();
1774
- process.exit(0);
1775
- });
1776
- await new Promise(() => {
1777
- });
1778
- }
1779
- } catch (error) {
1780
- handleError(error, json);
1781
- process.exit(1);
1782
- }
1783
- });
1784
-
1785
- // src/commands/mcp/publish.ts
1786
- import { execSync as execSync3 } from "child_process";
1787
- import { input } from "@inquirer/prompts";
1788
- import { Command as Command15 } from "commander";
1789
- import ora11 from "ora";
1790
- var publishCommand = new Command15("publish").description("Push local files to GitHub and trigger deployment").option("-m, --message <msg>", "Commit message").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
1791
- const globalOptions = command.optsWithGlobals();
1792
- const json = globalOptions.json ?? false;
1793
- try {
1794
- const mcpId = await requireMcpId(options.mcpId);
1795
- const projectRoot = await findProjectRoot(process.cwd());
1796
- if (!projectRoot) {
1797
- throw new CLIError(
1798
- "Not in a WaniWani project. Run 'waniwani mcp create <name>' first.",
1799
- "NOT_IN_PROJECT"
1800
- );
1801
- }
1802
- try {
1803
- execSync3("git rev-parse --is-inside-work-tree", {
1804
- cwd: projectRoot,
1805
- stdio: "ignore"
1806
- });
1807
- } catch {
1808
- throw new CLIError(
1809
- "Not a git repository. Run 'waniwani mcp create <name>' or 'waniwani mcp clone <name>' to set up properly.",
1810
- "NOT_GIT_REPO"
1811
- );
1812
- }
1813
- const status = execSync3("git status --porcelain", {
1814
- cwd: projectRoot,
1815
- encoding: "utf-8"
1816
- }).trim();
1817
- if (!status) {
1818
- if (json) {
1819
- formatOutput({ success: true, message: "Nothing to publish" }, true);
1820
- } else {
1821
- console.log("Nothing to publish \u2014 no changes detected.");
2329
+ try {
2330
+ await withTimeout(
2331
+ (async () => {
2332
+ await withTimeout(
2333
+ watcher.close().catch(() => void 0),
2334
+ SHUTDOWN_STEP_TIMEOUT_MS
2335
+ );
2336
+ await cleanupPreviewSession(sessionId);
2337
+ })(),
2338
+ SHUTDOWN_MAX_WAIT_MS,
2339
+ {
2340
+ onTimeout: () => {
2341
+ console.log("Shutdown timed out, forcing exit.");
2342
+ }
2343
+ }
2344
+ );
2345
+ } finally {
2346
+ process.exit(exitCode);
2347
+ }
2348
+ };
2349
+ if (serverCmdId) {
2350
+ serverMonitorInterval = setInterval(() => {
2351
+ if (shuttingDown) return;
2352
+ void (async () => {
2353
+ try {
2354
+ const commandStatus = await api.get(
2355
+ `/api/mcp/sessions/${sessionId}/commands/${serverCmdId}?statusOnly=true`
2356
+ );
2357
+ if (commandStatus.exitCode === null) {
2358
+ return;
2359
+ }
2360
+ if (commandStatus.exitCode === 0) {
2361
+ console.log("Dev server exited (code 0).");
2362
+ await gracefulShutdown(0);
2363
+ return;
2364
+ }
2365
+ const commandOutput = await getCommandOutputOrNull(
2366
+ sessionId,
2367
+ serverCmdId
2368
+ );
2369
+ handleError(
2370
+ new CLIError(
2371
+ `Dev server exited with code ${commandStatus.exitCode}.`,
2372
+ "SERVER_EXITED",
2373
+ {
2374
+ command: devCommand,
2375
+ cmdId: serverCmdId,
2376
+ exitCode: commandStatus.exitCode,
2377
+ stdout: commandOutput?.stdout ?? "",
2378
+ stderr: commandOutput?.stderr ?? ""
2379
+ }
2380
+ ),
2381
+ json
2382
+ );
2383
+ await gracefulShutdown(1);
2384
+ } catch {
2385
+ }
2386
+ })();
2387
+ }, statusPollIntervalMs);
1822
2388
  }
1823
- return;
1824
- }
1825
- let message = options.message;
1826
- if (!message) {
1827
- message = await input({
1828
- message: "Commit message:",
1829
- validate: (value) => value.trim() ? true : "Commit message is required"
2389
+ process.once("SIGINT", () => {
2390
+ void gracefulShutdown();
1830
2391
  });
1831
- }
1832
- const spinner = ora11("Publishing...").start();
1833
- const { cloneUrl } = await api.get(
1834
- `/api/mcp/repositories/${mcpId}/clone-url`
1835
- );
1836
- spinner.text = "Committing changes...";
1837
- execSync3("git add -A", { cwd: projectRoot, stdio: "ignore" });
1838
- execSync3(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
1839
- cwd: projectRoot,
1840
- stdio: "ignore"
1841
- });
1842
- spinner.text = "Pushing to GitHub...";
1843
- const originalUrl = execSync3("git remote get-url origin", {
1844
- cwd: projectRoot,
1845
- encoding: "utf-8"
1846
- }).trim();
1847
- try {
1848
- execSync3(`git remote set-url origin "${cloneUrl}"`, {
1849
- cwd: projectRoot,
1850
- stdio: "ignore"
1851
- });
1852
- execSync3("git push origin HEAD", {
1853
- cwd: projectRoot,
1854
- stdio: "ignore"
2392
+ process.once("SIGTERM", () => {
2393
+ void gracefulShutdown();
1855
2394
  });
1856
- } finally {
1857
- execSync3(`git remote set-url origin "${originalUrl}"`, {
1858
- cwd: projectRoot,
1859
- stdio: "ignore"
2395
+ await new Promise(() => {
1860
2396
  });
1861
2397
  }
1862
- const commitSha = execSync3("git rev-parse HEAD", {
1863
- cwd: projectRoot,
1864
- encoding: "utf-8"
1865
- }).trim();
1866
- spinner.succeed(`Pushed to GitHub (${commitSha.slice(0, 7)})`);
1867
- if (json) {
1868
- formatOutput({ commitSha, message }, true);
1869
- } else {
1870
- console.log();
1871
- formatSuccess("Files pushed to GitHub!", false);
1872
- console.log();
1873
- console.log("Deployment will start automatically via webhook.");
1874
- }
1875
2398
  } catch (error) {
1876
2399
  handleError(error, json);
1877
2400
  process.exit(1);
@@ -1881,7 +2404,7 @@ var publishCommand = new Command15("publish").description("Push local files to G
1881
2404
  // src/commands/mcp/run-command.ts
1882
2405
  import chalk8 from "chalk";
1883
2406
  import { Command as Command16 } from "commander";
1884
- import ora12 from "ora";
2407
+ import ora11 from "ora";
1885
2408
  var runCommandCommand = new Command16("run-command").description("Run a command in the MCP sandbox").argument("<command>", "Command to run").argument("[args...]", "Command arguments").option("--mcp-id <id>", "Specific MCP ID").option("--cwd <path>", "Working directory").option(
1886
2409
  "--timeout <ms>",
1887
2410
  "Command timeout in milliseconds (default: 30000, max: 300000)"
@@ -1892,7 +2415,7 @@ var runCommandCommand = new Command16("run-command").description("Run a command
1892
2415
  await requireMcpId(options.mcpId);
1893
2416
  const sessionId = await requireSessionId();
1894
2417
  const timeout = options.timeout ? Number.parseInt(options.timeout, 10) : void 0;
1895
- const spinner = ora12(`Running: ${cmd} ${args.join(" ")}`.trim()).start();
2418
+ const spinner = ora11(`Running: ${cmd} ${args.join(" ")}`.trim()).start();
1896
2419
  const result = await api.post(
1897
2420
  `/api/mcp/sessions/${sessionId}/commands`,
1898
2421
  {
@@ -1940,13 +2463,13 @@ var runCommandCommand = new Command16("run-command").description("Run a command
1940
2463
  // src/commands/mcp/status.ts
1941
2464
  import chalk9 from "chalk";
1942
2465
  import { Command as Command17 } from "commander";
1943
- import ora13 from "ora";
2466
+ import ora12 from "ora";
1944
2467
  var statusCommand = new Command17("status").description("Show current MCP status").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
1945
2468
  const globalOptions = command.optsWithGlobals();
1946
2469
  const json = globalOptions.json ?? false;
1947
2470
  try {
1948
2471
  const mcpId = await requireMcpId(options.mcpId);
1949
- const spinner = ora13("Fetching MCP status...").start();
2472
+ const spinner = ora12("Fetching MCP status...").start();
1950
2473
  const result = await api.get(
1951
2474
  `/api/mcp/repositories/${mcpId}`
1952
2475
  );
@@ -2016,14 +2539,14 @@ var statusCommand = new Command17("status").description("Show current MCP status
2016
2539
 
2017
2540
  // src/commands/mcp/stop.ts
2018
2541
  import { Command as Command18 } from "commander";
2019
- import ora14 from "ora";
2542
+ import ora13 from "ora";
2020
2543
  var stopCommand = new Command18("stop").description("Stop the development environment (sandbox + server)").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
2021
2544
  const globalOptions = command.optsWithGlobals();
2022
2545
  const json = globalOptions.json ?? false;
2023
2546
  try {
2024
2547
  await requireMcpId(options.mcpId);
2025
2548
  const sessionId = await requireSessionId();
2026
- const spinner = ora14("Stopping development environment...").start();
2549
+ const spinner = ora13("Stopping development environment...").start();
2027
2550
  try {
2028
2551
  await api.post(`/api/mcp/sessions/${sessionId}/server`, {
2029
2552
  action: "stop"
@@ -2048,7 +2571,7 @@ var stopCommand = new Command18("stop").description("Stop the development enviro
2048
2571
 
2049
2572
  // src/commands/mcp/sync.ts
2050
2573
  import { Command as Command19 } from "commander";
2051
- import ora15 from "ora";
2574
+ import ora14 from "ora";
2052
2575
  var syncCommand = new Command19("sync").description("Pull template files to local project").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
2053
2576
  const globalOptions = command.optsWithGlobals();
2054
2577
  const json = globalOptions.json ?? false;
@@ -2061,7 +2584,7 @@ var syncCommand = new Command19("sync").description("Pull template files to loca
2061
2584
  "NOT_IN_PROJECT"
2062
2585
  );
2063
2586
  }
2064
- const spinner = ora15("Pulling files...").start();
2587
+ const spinner = ora14("Pulling files...").start();
2065
2588
  const result = await pullFilesFromGithub(mcpId, projectRoot);
2066
2589
  spinner.succeed(`Pulled ${result.count} files`);
2067
2590
  if (json) {
@@ -2084,12 +2607,12 @@ var syncCommand = new Command19("sync").description("Pull template files to loca
2084
2607
 
2085
2608
  // src/commands/mcp/use.ts
2086
2609
  import { Command as Command20 } from "commander";
2087
- import ora16 from "ora";
2610
+ import ora15 from "ora";
2088
2611
  var useCommand = new Command20("use").description("Select an MCP to use for subsequent commands").argument("<name>", "Name of the MCP to use").action(async (name, _options, command) => {
2089
2612
  const globalOptions = command.optsWithGlobals();
2090
2613
  const json = globalOptions.json ?? false;
2091
2614
  try {
2092
- const spinner = ora16("Fetching MCPs...").start();
2615
+ const spinner = ora15("Fetching MCPs...").start();
2093
2616
  const mcps = await api.get(
2094
2617
  "/api/mcp/repositories"
2095
2618
  );
@@ -2120,7 +2643,7 @@ var useCommand = new Command20("use").description("Select an MCP to use for subs
2120
2643
  });
2121
2644
 
2122
2645
  // src/commands/mcp/index.ts
2123
- var mcpCommand = new Command21("mcp").description("MCP management commands").addCommand(createCommand).addCommand(cloneCommand).addCommand(listCommand2).addCommand(useCommand).addCommand(statusCommand).addCommand(previewCommand).addCommand(stopCommand).addCommand(logsCommand).addCommand(syncCommand).addCommand(publishCommand).addCommand(deleteCommand).addCommand(fileCommand).addCommand(runCommandCommand);
2646
+ var mcpCommand = new Command21("mcp").description("MCP management commands").addCommand(createCommand).addCommand(cloneCommand).addCommand(listCommand2).addCommand(useCommand).addCommand(statusCommand).addCommand(previewCommand).addCommand(stopCommand).addCommand(logsCommand).addCommand(syncCommand).addCommand(deleteCommand).addCommand(fileCommand).addCommand(runCommandCommand);
2124
2647
 
2125
2648
  // src/commands/org/index.ts
2126
2649
  import { Command as Command24 } from "commander";
@@ -2128,12 +2651,12 @@ import { Command as Command24 } from "commander";
2128
2651
  // src/commands/org/list.ts
2129
2652
  import chalk10 from "chalk";
2130
2653
  import { Command as Command22 } from "commander";
2131
- import ora17 from "ora";
2132
- var listCommand3 = new Command22("list").description("List your organizations").action(async (_, command) => {
2654
+ import ora16 from "ora";
2655
+ var listCommand3 = new Command22("list").description("List your organizations").action(async (_options, command) => {
2133
2656
  const globalOptions = command.optsWithGlobals();
2134
2657
  const json = globalOptions.json ?? false;
2135
2658
  try {
2136
- const spinner = ora17("Fetching organizations...").start();
2659
+ const spinner = ora16("Fetching organizations...").start();
2137
2660
  const result = await api.get("/api/oauth/orgs");
2138
2661
  spinner.stop();
2139
2662
  const { orgs, activeOrgId } = result;
@@ -2180,12 +2703,12 @@ var listCommand3 = new Command22("list").description("List your organizations").
2180
2703
 
2181
2704
  // src/commands/org/switch.ts
2182
2705
  import { Command as Command23 } from "commander";
2183
- import ora18 from "ora";
2184
- var switchCommand = new Command23("switch").description("Switch to a different organization").argument("<name>", "Name or slug of the organization to switch to").action(async (name, _, command) => {
2706
+ import ora17 from "ora";
2707
+ var switchCommand = new Command23("switch").description("Switch to a different organization").argument("<name>", "Name or slug of the organization to switch to").action(async (name, _options, command) => {
2185
2708
  const globalOptions = command.optsWithGlobals();
2186
2709
  const json = globalOptions.json ?? false;
2187
2710
  try {
2188
- const spinner = ora18("Fetching organizations...").start();
2711
+ const spinner = ora17("Fetching organizations...").start();
2189
2712
  const { orgs } = await api.get("/api/oauth/orgs");
2190
2713
  const org = orgs.find((o) => o.name === name || o.slug === name);
2191
2714
  if (!org) {
@@ -2229,6 +2752,7 @@ program.addCommand(logoutCommand);
2229
2752
  program.addCommand(mcpCommand);
2230
2753
  program.addCommand(orgCommand);
2231
2754
  program.addCommand(configCommand);
2755
+ program.addCommand(gitCredentialHelperCommand);
2232
2756
 
2233
2757
  // src/index.ts
2234
2758
  program.parse(process.argv);