@waniwani/cli 0.0.33 → 0.0.35

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command as Command25 } from "commander";
4
+ import { Command as Command24 } from "commander";
5
5
 
6
6
  // src/commands/config/index.ts
7
7
  import { Command as Command2 } from "commander";
@@ -14,30 +14,31 @@ import { Command } from "commander";
14
14
  // src/lib/config.ts
15
15
  import { existsSync } from "fs";
16
16
  import { mkdir, readFile, writeFile } from "fs/promises";
17
- import { homedir } from "os";
18
17
  import { join } from "path";
19
18
  import { z } from "zod";
20
19
  var LOCAL_CONFIG_DIR = ".waniwani";
21
20
  var CONFIG_FILE_NAME = "settings.json";
22
21
  var LOCAL_DIR = join(process.cwd(), LOCAL_CONFIG_DIR);
23
22
  var LOCAL_FILE = join(LOCAL_DIR, CONFIG_FILE_NAME);
24
- var GLOBAL_DIR = join(homedir(), LOCAL_CONFIG_DIR);
25
- var GLOBAL_FILE = join(GLOBAL_DIR, CONFIG_FILE_NAME);
26
23
  var DEFAULT_API_URL = "https://app.waniwani.ai";
27
24
  var ConfigSchema = z.object({
25
+ // Settings
26
+ sessionId: z.string().nullable().default(null),
28
27
  mcpId: z.string().nullable().default(null),
29
- apiUrl: z.string().nullable().default(null)
28
+ apiUrl: z.string().default(DEFAULT_API_URL),
29
+ // Auth (merged from auth.json)
30
+ accessToken: z.string().nullable().default(null),
31
+ refreshToken: z.string().nullable().default(null),
32
+ expiresAt: z.string().nullable().default(null),
33
+ clientId: z.string().nullable().default(null)
30
34
  });
31
35
  var Config = class {
32
36
  dir;
33
37
  file;
34
38
  cache = null;
35
- scope;
36
- constructor(forceGlobal = false) {
37
- const useLocal = !forceGlobal && existsSync(LOCAL_DIR);
38
- this.dir = useLocal ? LOCAL_DIR : GLOBAL_DIR;
39
- this.file = useLocal ? LOCAL_FILE : GLOBAL_FILE;
40
- this.scope = useLocal ? "local" : "global";
39
+ constructor() {
40
+ this.dir = LOCAL_DIR;
41
+ this.file = LOCAL_FILE;
41
42
  }
42
43
  async load() {
43
44
  if (!this.cache) {
@@ -56,6 +57,20 @@ var Config = class {
56
57
  await mkdir(this.dir, { recursive: true });
57
58
  await writeFile(this.file, JSON.stringify(data, null, " "));
58
59
  }
60
+ /**
61
+ * Ensure the .waniwani directory exists in cwd.
62
+ * Used by login to create config before saving tokens.
63
+ */
64
+ async ensureConfigDir() {
65
+ await mkdir(this.dir, { recursive: true });
66
+ }
67
+ /**
68
+ * Check if a .waniwani config directory exists in cwd.
69
+ */
70
+ hasConfig() {
71
+ return existsSync(this.dir);
72
+ }
73
+ // --- Settings methods ---
59
74
  async getMcpId() {
60
75
  return (await this.load()).mcpId;
61
76
  }
@@ -64,21 +79,57 @@ var Config = class {
64
79
  data.mcpId = id;
65
80
  await this.save(data);
66
81
  }
67
- async getApiUrl() {
68
- if (process.env.WANIWANI_API_URL) return process.env.WANIWANI_API_URL;
69
- return (await this.load()).apiUrl || DEFAULT_API_URL;
82
+ async getSessionId() {
83
+ return (await this.load()).sessionId;
70
84
  }
71
- async setApiUrl(url) {
85
+ async setSessionId(id) {
72
86
  const data = await this.load();
73
- data.apiUrl = url;
87
+ data.sessionId = id;
74
88
  await this.save(data);
75
89
  }
90
+ async getApiUrl() {
91
+ if (process.env.WANIWANI_API_URL) return process.env.WANIWANI_API_URL;
92
+ return (await this.load()).apiUrl;
93
+ }
76
94
  async clear() {
77
95
  await this.save(ConfigSchema.parse({}));
78
96
  }
97
+ // --- Auth methods ---
98
+ async getAccessToken() {
99
+ return (await this.load()).accessToken;
100
+ }
101
+ async getRefreshToken() {
102
+ return (await this.load()).refreshToken;
103
+ }
104
+ async getClientId() {
105
+ return (await this.load()).clientId;
106
+ }
107
+ async setTokens(accessToken, refreshToken, expiresIn, clientId) {
108
+ const expiresAt = new Date(Date.now() + expiresIn * 1e3).toISOString();
109
+ const data = await this.load();
110
+ data.accessToken = accessToken;
111
+ data.refreshToken = refreshToken;
112
+ data.expiresAt = expiresAt;
113
+ if (clientId) {
114
+ data.clientId = clientId;
115
+ }
116
+ await this.save(data);
117
+ }
118
+ async clearAuth() {
119
+ const data = await this.load();
120
+ data.accessToken = null;
121
+ data.refreshToken = null;
122
+ data.expiresAt = null;
123
+ data.clientId = null;
124
+ await this.save(data);
125
+ }
126
+ async isTokenExpired() {
127
+ const data = await this.load();
128
+ if (!data.expiresAt) return true;
129
+ return new Date(data.expiresAt).getTime() - 5 * 60 * 1e3 < Date.now();
130
+ }
79
131
  };
80
132
  var config = new Config();
81
- var globalConfig = new Config(true);
82
133
  async function initConfigAt(dir, overrides = {}) {
83
134
  const configDir = join(dir, LOCAL_CONFIG_DIR);
84
135
  const configPath = join(configDir, CONFIG_FILE_NAME);
@@ -235,90 +286,37 @@ var configInitCommand = new Command("init").description("Initialize .waniwani co
235
286
  // src/commands/config/index.ts
236
287
  var configCommand = new Command2("config").description("Manage WaniWani configuration").addCommand(configInitCommand);
237
288
 
238
- // src/commands/dev.ts
239
- import { relative as relative2 } from "path";
289
+ // src/commands/login.ts
290
+ import { spawn } from "child_process";
291
+ import { createServer } from "http";
240
292
  import chalk3 from "chalk";
241
- import chokidar from "chokidar";
242
293
  import { Command as Command3 } from "commander";
243
294
  import ora from "ora";
244
295
 
245
296
  // src/lib/auth.ts
246
- import { access, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
247
- import { homedir as homedir2 } from "os";
248
- import { join as join3 } from "path";
249
- import { z as z2 } from "zod";
250
- var CONFIG_DIR = join3(homedir2(), ".waniwani");
251
- var AUTH_FILE = join3(CONFIG_DIR, "auth.json");
252
- var AuthStoreSchema = z2.object({
253
- accessToken: z2.string().nullable().default(null),
254
- refreshToken: z2.string().nullable().default(null),
255
- expiresAt: z2.string().nullable().default(null),
256
- clientId: z2.string().nullable().default(null)
257
- });
258
- async function ensureConfigDir() {
259
- await mkdir2(CONFIG_DIR, { recursive: true });
260
- }
261
- async function readAuthStore() {
262
- await ensureConfigDir();
263
- try {
264
- await access(AUTH_FILE);
265
- const content = await readFile2(AUTH_FILE, "utf-8");
266
- return AuthStoreSchema.parse(JSON.parse(content));
267
- } catch {
268
- return AuthStoreSchema.parse({});
269
- }
270
- }
271
- async function writeAuthStore(store) {
272
- await ensureConfigDir();
273
- await writeFile2(AUTH_FILE, JSON.stringify(store, null, 2), "utf-8");
274
- }
275
297
  var AuthManager = class {
276
- storeCache = null;
277
- async getStore() {
278
- if (!this.storeCache) {
279
- this.storeCache = await readAuthStore();
280
- }
281
- return this.storeCache;
282
- }
283
- async saveStore(store) {
284
- this.storeCache = store;
285
- await writeAuthStore(store);
286
- }
287
298
  async isLoggedIn() {
288
- const store = await this.getStore();
289
- return !!store.accessToken;
299
+ const token = await config.getAccessToken();
300
+ return !!token;
290
301
  }
291
302
  async getAccessToken() {
292
- const store = await this.getStore();
293
- return store.accessToken;
303
+ return config.getAccessToken();
294
304
  }
295
305
  async getRefreshToken() {
296
- const store = await this.getStore();
297
- return store.refreshToken;
306
+ return config.getRefreshToken();
298
307
  }
299
308
  async setTokens(accessToken, refreshToken, expiresIn, clientId) {
300
- const expiresAt = new Date(Date.now() + expiresIn * 1e3).toISOString();
301
- const store = await this.getStore();
302
- store.accessToken = accessToken;
303
- store.refreshToken = refreshToken;
304
- store.expiresAt = expiresAt;
305
- if (clientId) {
306
- store.clientId = clientId;
307
- }
308
- await this.saveStore(store);
309
+ return config.setTokens(accessToken, refreshToken, expiresIn, clientId);
309
310
  }
310
311
  async clear() {
311
- const emptyStore = AuthStoreSchema.parse({});
312
- await this.saveStore(emptyStore);
312
+ return config.clearAuth();
313
313
  }
314
314
  async isTokenExpired() {
315
- const store = await this.getStore();
316
- if (!store.expiresAt) return true;
317
- return new Date(store.expiresAt).getTime() - 5 * 60 * 1e3 < Date.now();
315
+ return config.isTokenExpired();
318
316
  }
319
317
  async tryRefreshToken() {
320
- const store = await this.getStore();
321
- const { refreshToken, clientId } = store;
318
+ const refreshToken = await config.getRefreshToken();
319
+ const clientId = await config.getClientId();
322
320
  if (!refreshToken || !clientId) return false;
323
321
  try {
324
322
  const apiUrl = await config.getApiUrl();
@@ -350,532 +348,53 @@ var AuthManager = class {
350
348
  };
351
349
  var auth = new AuthManager();
352
350
 
353
- // src/lib/api.ts
354
- var ApiError = class extends CLIError {
355
- constructor(message, code, statusCode, details) {
356
- super(message, code, details);
357
- this.statusCode = statusCode;
358
- this.name = "ApiError";
359
- }
360
- };
361
- async function request(method, path, options) {
362
- const {
363
- body,
364
- requireAuth = true,
365
- headers: extraHeaders = {}
366
- } = options || {};
367
- const headers = {
368
- "Content-Type": "application/json",
369
- ...extraHeaders
370
- };
371
- if (requireAuth) {
372
- const token = await auth.getAccessToken();
373
- if (!token) {
374
- throw new AuthError(
375
- "Not logged in. Run 'waniwani login' to authenticate."
376
- );
377
- }
378
- headers.Authorization = `Bearer ${token}`;
379
- }
380
- const baseUrl = await config.getApiUrl();
381
- const url = `${baseUrl}${path}`;
382
- const response = await fetch(url, {
383
- method,
384
- headers,
385
- body: body ? JSON.stringify(body) : void 0
386
- });
387
- if (response.status === 204) {
388
- return void 0;
389
- }
390
- let data;
391
- let rawBody;
392
- try {
393
- rawBody = await response.text();
394
- data = JSON.parse(rawBody);
395
- } catch {
396
- throw new ApiError(
397
- rawBody || `Request failed with status ${response.status}`,
398
- "API_ERROR",
399
- response.status,
400
- { statusText: response.statusText }
401
- );
402
- }
403
- if (!response.ok || data.error) {
404
- const errorMessage = data.error?.message || data.message || data.error || rawBody || `Request failed with status ${response.status}`;
405
- const errorCode = data.error?.code || data.code || "API_ERROR";
406
- const errorDetails = {
407
- ...data.error?.details,
408
- statusText: response.statusText,
409
- ...data.error ? {} : { rawResponse: data }
410
- };
411
- const error = {
412
- code: errorCode,
413
- message: errorMessage,
414
- details: errorDetails
415
- };
416
- if (response.status === 401) {
417
- const refreshed = await auth.tryRefreshToken();
418
- if (refreshed) {
419
- return request(method, path, options);
420
- }
421
- throw new AuthError(
422
- "Session expired. Run 'waniwani login' to re-authenticate."
423
- );
424
- }
425
- throw new ApiError(
426
- error.message,
427
- error.code,
428
- response.status,
429
- error.details
430
- );
431
- }
432
- return data.data;
433
- }
434
- var api = {
435
- get: (path, options) => request("GET", path, options),
436
- post: (path, body, options) => request("POST", path, { body, ...options }),
437
- delete: (path, options) => request("DELETE", path, options),
438
- getBaseUrl: () => config.getApiUrl()
439
- };
440
-
441
- // src/lib/sync.ts
442
- import { existsSync as existsSync3 } from "fs";
443
- import { mkdir as mkdir3, readdir, readFile as readFile3, stat, writeFile as writeFile3 } from "fs/promises";
444
- import { dirname, join as join4, relative } from "path";
445
- import ignore from "ignore";
446
-
447
- // src/lib/utils.ts
448
- function debounce(fn, delay) {
449
- let timeoutId;
450
- return (...args) => {
451
- clearTimeout(timeoutId);
452
- timeoutId = setTimeout(() => fn(...args), delay);
453
- };
351
+ // src/commands/login.ts
352
+ var CALLBACK_PORT = 54321;
353
+ var CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
354
+ var CLIENT_NAME = "waniwani-cli";
355
+ function generateCodeVerifier() {
356
+ const array = new Uint8Array(32);
357
+ crypto.getRandomValues(array);
358
+ return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
454
359
  }
455
- var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
456
- ".png",
457
- ".jpg",
458
- ".jpeg",
459
- ".gif",
460
- ".ico",
461
- ".webp",
462
- ".svg",
463
- ".woff",
464
- ".woff2",
465
- ".ttf",
466
- ".eot",
467
- ".otf",
468
- ".zip",
469
- ".tar",
470
- ".gz",
471
- ".pdf",
472
- ".exe",
473
- ".dll",
474
- ".so",
475
- ".dylib",
476
- ".bin",
477
- ".mp3",
478
- ".mp4",
479
- ".wav",
480
- ".ogg",
481
- ".webm"
482
- ]);
483
- function isBinaryPath(filePath) {
484
- const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
485
- return BINARY_EXTENSIONS.has(ext);
360
+ async function generateCodeChallenge(verifier) {
361
+ const encoder = new TextEncoder();
362
+ const data = encoder.encode(verifier);
363
+ const hash = await crypto.subtle.digest("SHA-256", data);
364
+ return btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
486
365
  }
487
- function detectBinary(buffer) {
488
- const sample = buffer.subarray(0, 8192);
489
- return sample.includes(0);
366
+ function generateState() {
367
+ const array = new Uint8Array(16);
368
+ crypto.getRandomValues(array);
369
+ return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
490
370
  }
491
-
492
- // src/lib/sync.ts
493
- var PROJECT_DIR = ".waniwani";
494
- var SETTINGS_FILE = "settings.json";
495
- async function findProjectRoot(startDir) {
496
- let current = startDir;
497
- const root = dirname(current);
498
- while (current !== root) {
499
- if (existsSync3(join4(current, PROJECT_DIR))) {
500
- return current;
501
- }
502
- const parent = dirname(current);
503
- if (parent === current) break;
504
- current = parent;
505
- }
506
- if (existsSync3(join4(current, PROJECT_DIR))) {
507
- return current;
371
+ async function registerClient() {
372
+ const apiUrl = await config.getApiUrl();
373
+ const response = await fetch(`${apiUrl}/api/auth/oauth2/register`, {
374
+ method: "POST",
375
+ headers: {
376
+ "Content-Type": "application/json"
377
+ },
378
+ body: JSON.stringify({
379
+ client_name: CLIENT_NAME,
380
+ redirect_uris: [CALLBACK_URL],
381
+ grant_types: ["authorization_code", "refresh_token"],
382
+ response_types: ["code"],
383
+ token_endpoint_auth_method: "none"
384
+ })
385
+ });
386
+ if (!response.ok) {
387
+ const error = await response.json().catch(() => ({}));
388
+ throw new CLIError(
389
+ error.error_description || "Failed to register OAuth client",
390
+ "CLIENT_REGISTRATION_FAILED"
391
+ );
508
392
  }
509
- return null;
393
+ return response.json();
510
394
  }
511
- async function loadProjectMcpId(projectRoot) {
512
- const settingsPath = join4(projectRoot, PROJECT_DIR, SETTINGS_FILE);
513
- try {
514
- const content = await readFile3(settingsPath, "utf-8");
515
- const settings = JSON.parse(content);
516
- return settings.mcpId ?? null;
517
- } catch {
518
- return null;
519
- }
520
- }
521
- var DEFAULT_IGNORE_PATTERNS = [
522
- ".waniwani",
523
- ".git",
524
- "node_modules",
525
- ".env",
526
- ".env.*",
527
- ".DS_Store",
528
- "*.log",
529
- ".cache",
530
- "dist",
531
- "coverage",
532
- ".turbo",
533
- ".next",
534
- ".nuxt",
535
- ".vercel"
536
- ];
537
- async function loadIgnorePatterns(projectRoot) {
538
- const ig = ignore();
539
- ig.add(DEFAULT_IGNORE_PATTERNS);
540
- const gitignorePath = join4(projectRoot, ".gitignore");
541
- if (existsSync3(gitignorePath)) {
542
- try {
543
- const content = await readFile3(gitignorePath, "utf-8");
544
- ig.add(content);
545
- } catch {
546
- }
547
- }
548
- return ig;
549
- }
550
- async function collectFiles(projectRoot) {
551
- const ig = await loadIgnorePatterns(projectRoot);
552
- const files = [];
553
- async function walk(dir) {
554
- const entries = await readdir(dir, { withFileTypes: true });
555
- for (const entry of entries) {
556
- const fullPath = join4(dir, entry.name);
557
- const relativePath = relative(projectRoot, fullPath);
558
- if (ig.ignores(relativePath)) {
559
- continue;
560
- }
561
- if (entry.isDirectory()) {
562
- await walk(fullPath);
563
- } else if (entry.isFile()) {
564
- try {
565
- const content = await readFile3(fullPath);
566
- const isBinary = isBinaryPath(fullPath) || detectBinary(content);
567
- files.push({
568
- path: relativePath,
569
- content: isBinary ? content.toString("base64") : content.toString("utf8"),
570
- encoding: isBinary ? "base64" : "utf8"
571
- });
572
- } catch {
573
- }
574
- }
575
- }
576
- }
577
- await walk(projectRoot);
578
- return files;
579
- }
580
- async function pullFilesFromSandbox(mcpId, targetDir) {
581
- const result = await api.get(
582
- `/api/mcp/sandboxes/${mcpId}/files/pull`
583
- );
584
- const writtenFiles = [];
585
- for (const file of result.files) {
586
- const localPath = join4(targetDir, file.path);
587
- const dir = dirname(localPath);
588
- await mkdir3(dir, { recursive: true });
589
- if (file.encoding === "base64") {
590
- await writeFile3(localPath, Buffer.from(file.content, "base64"));
591
- } else {
592
- await writeFile3(localPath, file.content, "utf8");
593
- }
594
- writtenFiles.push(file.path);
595
- }
596
- return { count: writtenFiles.length, files: writtenFiles };
597
- }
598
- async function collectSingleFile(projectRoot, filePath) {
599
- const fullPath = join4(projectRoot, filePath);
600
- const relativePath = relative(projectRoot, fullPath);
601
- if (!existsSync3(fullPath)) {
602
- return null;
603
- }
604
- try {
605
- const fileStat = await stat(fullPath);
606
- if (!fileStat.isFile()) {
607
- return null;
608
- }
609
- const content = await readFile3(fullPath);
610
- const isBinary = isBinaryPath(fullPath) || detectBinary(content);
611
- return {
612
- path: relativePath,
613
- content: isBinary ? content.toString("base64") : content.toString("utf8"),
614
- encoding: isBinary ? "base64" : "utf8"
615
- };
616
- } catch {
617
- return null;
618
- }
619
- }
620
-
621
- // src/commands/dev.ts
622
- var BATCH_SIZE = 50;
623
- var DEFAULT_DEBOUNCE_MS = 300;
624
- var devCommand = new Command3("dev").description("Watch and sync files to MCP sandbox").option("--no-initial-sync", "Skip initial sync of all files").option(
625
- "--debounce <ms>",
626
- "Debounce delay in milliseconds",
627
- String(DEFAULT_DEBOUNCE_MS)
628
- ).action(async (options, command) => {
629
- const globalOptions = command.optsWithGlobals();
630
- const json = globalOptions.json ?? false;
631
- try {
632
- const cwd = process.cwd();
633
- const projectRoot = await findProjectRoot(cwd);
634
- if (!projectRoot) {
635
- throw new CLIError(
636
- "Not in a WaniWani project. Run 'waniwani init <name>' first.",
637
- "NOT_IN_PROJECT"
638
- );
639
- }
640
- const mcpId = await loadProjectMcpId(projectRoot);
641
- if (!mcpId) {
642
- throw new CLIError(
643
- "No MCP ID found in project config. Run 'waniwani init <name>' first.",
644
- "NO_MCP_ID"
645
- );
646
- }
647
- if (options.initialSync !== false) {
648
- const spinner = ora("Initial sync...").start();
649
- const files = await collectFiles(projectRoot);
650
- if (files.length > 0) {
651
- const totalBatches = Math.ceil(files.length / BATCH_SIZE);
652
- let synced = 0;
653
- for (let i = 0; i < totalBatches; i++) {
654
- const batch = files.slice(i * BATCH_SIZE, (i + 1) * BATCH_SIZE);
655
- spinner.text = `Syncing (${i + 1}/${totalBatches})...`;
656
- const result = await api.post(
657
- `/api/mcp/sandboxes/${mcpId}/files`,
658
- {
659
- files: batch.map((f) => ({
660
- path: f.path,
661
- content: f.content,
662
- encoding: f.encoding
663
- }))
664
- }
665
- );
666
- synced += result.written.length;
667
- }
668
- spinner.succeed(`Initial sync complete (${synced} files)`);
669
- } else {
670
- spinner.info("No files to sync");
671
- }
672
- }
673
- const ig = await loadIgnorePatterns(projectRoot);
674
- const debounceMs = Number.parseInt(options.debounce, 10) || DEFAULT_DEBOUNCE_MS;
675
- const syncFile = debounce(async (filePath) => {
676
- const relativePath = relative2(projectRoot, filePath);
677
- if (ig.ignores(relativePath)) {
678
- return;
679
- }
680
- const file = await collectSingleFile(projectRoot, relativePath);
681
- if (!file) {
682
- console.log(chalk3.yellow("Skipped:"), relativePath);
683
- return;
684
- }
685
- try {
686
- await api.post(
687
- `/api/mcp/sandboxes/${mcpId}/files`,
688
- {
689
- files: [
690
- {
691
- path: file.path,
692
- content: file.content,
693
- encoding: file.encoding
694
- }
695
- ]
696
- }
697
- );
698
- console.log(chalk3.green("Synced:"), relativePath);
699
- } catch (error) {
700
- console.log(chalk3.red("Failed:"), relativePath);
701
- if (globalOptions.verbose) {
702
- console.error(error);
703
- }
704
- }
705
- }, debounceMs);
706
- console.log();
707
- console.log(chalk3.bold("Watching for changes..."));
708
- console.log(chalk3.dim("Press Ctrl+C to stop"));
709
- console.log();
710
- const watcher = chokidar.watch(projectRoot, {
711
- ignored: (path) => {
712
- const relativePath = relative2(projectRoot, path);
713
- return ig.ignores(relativePath);
714
- },
715
- persistent: true,
716
- ignoreInitial: true,
717
- awaitWriteFinish: {
718
- stabilityThreshold: 100,
719
- pollInterval: 100
720
- }
721
- });
722
- watcher.on("add", (path) => syncFile(path)).on("change", (path) => syncFile(path)).on("unlink", (path) => {
723
- const relativePath = relative2(projectRoot, path);
724
- console.log(chalk3.yellow("Deleted (local only):"), relativePath);
725
- });
726
- const cleanup = () => {
727
- console.log();
728
- console.log(chalk3.dim("Stopping watcher..."));
729
- watcher.close().then(() => {
730
- process.exit(0);
731
- });
732
- };
733
- process.on("SIGINT", cleanup);
734
- process.on("SIGTERM", cleanup);
735
- } catch (error) {
736
- handleError(error, json);
737
- process.exit(1);
738
- }
739
- });
740
-
741
- // src/commands/init.ts
742
- import { existsSync as existsSync4 } from "fs";
743
- import { mkdir as mkdir4, readFile as readFile4 } from "fs/promises";
744
- import { join as join5 } from "path";
745
- import { Command as Command4 } from "commander";
746
- import ora2 from "ora";
747
- async function loadParentConfig(cwd) {
748
- const parentConfigPath = join5(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE_NAME);
749
- if (!existsSync4(parentConfigPath)) {
750
- return null;
751
- }
752
- try {
753
- const content = await readFile4(parentConfigPath, "utf-8");
754
- const config2 = JSON.parse(content);
755
- const { mcpId: _, ...rest } = config2;
756
- return rest;
757
- } catch {
758
- return null;
759
- }
760
- }
761
- var initCommand = new Command4("init").description("Create a new MCP project from template").argument("<name>", "Name for the MCP project").action(async (name, _, command) => {
762
- const globalOptions = command.optsWithGlobals();
763
- const json = globalOptions.json ?? false;
764
- try {
765
- const cwd = process.cwd();
766
- const projectDir = join5(cwd, name);
767
- if (existsSync4(projectDir)) {
768
- if (json) {
769
- formatOutput(
770
- {
771
- success: false,
772
- error: `Directory "${name}" already exists`
773
- },
774
- true
775
- );
776
- } else {
777
- console.error(`Error: Directory "${name}" already exists`);
778
- }
779
- process.exit(1);
780
- }
781
- const spinner = ora2("Creating MCP sandbox...").start();
782
- const result = await api.post("/api/mcp/sandboxes", {
783
- name
784
- });
785
- spinner.text = "Downloading template files...";
786
- await mkdir4(projectDir, { recursive: true });
787
- await pullFilesFromSandbox(result.id, projectDir);
788
- spinner.text = "Setting up project config...";
789
- const parentConfig = await loadParentConfig(cwd);
790
- await initConfigAt(projectDir, {
791
- ...parentConfig,
792
- mcpId: result.id
793
- // Always use the new sandbox's mcpId
794
- });
795
- spinner.succeed("MCP project created");
796
- if (json) {
797
- formatOutput(
798
- {
799
- success: true,
800
- projectDir,
801
- mcpId: result.id,
802
- sandboxId: result.sandboxId,
803
- previewUrl: result.previewUrl
804
- },
805
- true
806
- );
807
- } else {
808
- console.log();
809
- formatSuccess(`MCP project "${name}" created!`, false);
810
- console.log();
811
- console.log(` Project: ${projectDir}`);
812
- console.log(` MCP ID: ${result.id}`);
813
- console.log(` Preview URL: ${result.previewUrl}`);
814
- console.log();
815
- console.log("Next steps:");
816
- console.log(` cd ${name}`);
817
- console.log(" waniwani push # Sync files to sandbox");
818
- console.log(" waniwani dev # Watch mode with auto-sync");
819
- console.log(' waniwani task "..." # Send tasks to Claude');
820
- }
821
- } catch (error) {
822
- handleError(error, json);
823
- process.exit(1);
824
- }
825
- });
826
-
827
- // src/commands/login.ts
828
- import { spawn } from "child_process";
829
- import { createServer } from "http";
830
- import chalk4 from "chalk";
831
- import { Command as Command5 } from "commander";
832
- import ora3 from "ora";
833
- var CALLBACK_PORT = 54321;
834
- var CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
835
- var CLIENT_NAME = "waniwani-cli";
836
- function generateCodeVerifier() {
837
- const array = new Uint8Array(32);
838
- crypto.getRandomValues(array);
839
- return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
840
- }
841
- async function generateCodeChallenge(verifier) {
842
- const encoder = new TextEncoder();
843
- const data = encoder.encode(verifier);
844
- const hash = await crypto.subtle.digest("SHA-256", data);
845
- return btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
846
- }
847
- function generateState() {
848
- const array = new Uint8Array(16);
849
- crypto.getRandomValues(array);
850
- return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
851
- }
852
- async function registerClient() {
853
- const apiUrl = await config.getApiUrl();
854
- const response = await fetch(`${apiUrl}/api/auth/oauth2/register`, {
855
- method: "POST",
856
- headers: {
857
- "Content-Type": "application/json"
858
- },
859
- body: JSON.stringify({
860
- client_name: CLIENT_NAME,
861
- redirect_uris: [CALLBACK_URL],
862
- grant_types: ["authorization_code", "refresh_token"],
863
- response_types: ["code"],
864
- token_endpoint_auth_method: "none"
865
- })
866
- });
867
- if (!response.ok) {
868
- const error = await response.json().catch(() => ({}));
869
- throw new CLIError(
870
- error.error_description || "Failed to register OAuth client",
871
- "CLIENT_REGISTRATION_FAILED"
872
- );
873
- }
874
- return response.json();
875
- }
876
- async function openBrowser(url) {
877
- const [cmd, ...args] = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", url] : ["xdg-open", url];
878
- spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
395
+ async function openBrowser(url) {
396
+ const [cmd, ...args] = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", url] : ["xdg-open", url];
397
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
879
398
  }
880
399
  async function waitForCallback(expectedState, timeoutMs = 3e5) {
881
400
  return new Promise((resolve, reject) => {
@@ -949,7 +468,10 @@ async function waitForCallback(expectedState, timeoutMs = 3e5) {
949
468
  text-align: center;
950
469
  }
951
470
  .logo {
952
- height: 40px;
471
+ font-size: 1.75rem;
472
+ font-weight: 700;
473
+ color: #1e293b;
474
+ letter-spacing: -0.025em;
953
475
  }
954
476
  .icon-circle {
955
477
  width: 80px;
@@ -982,9 +504,7 @@ async function waitForCallback(expectedState, timeoutMs = 3e5) {
982
504
  <div class="blob blob-2"></div>
983
505
  <div class="blob blob-3"></div>
984
506
  <div class="container">
985
- <svg class="logo" viewBox="0 0 248 40" fill="none" xmlns="http://www.w3.org/2000/svg">
986
- <text x="0" y="32" font-family="system-ui" font-size="28" font-weight="bold" fill="#1e293b">WaniWani</text>
987
- </svg>
507
+ <span class="logo">WaniWani</span>
988
508
  <div class="icon-circle">
989
509
  ${isSuccess ? '<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg>' : '<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>'}
990
510
  </div>
@@ -1104,7 +624,7 @@ async function exchangeCodeForToken(code, codeVerifier, clientId, resource) {
1104
624
  }
1105
625
  return response.json();
1106
626
  }
1107
- var loginCommand = new Command5("login").description("Log in to WaniWani").option("--no-browser", "Don't open the browser automatically").action(async (options, command) => {
627
+ var loginCommand = new Command3("login").description("Log in to WaniWani").option("--no-browser", "Don't open the browser automatically").action(async (options, command) => {
1108
628
  const globalOptions = command.optsWithGlobals();
1109
629
  const json = globalOptions.json ?? false;
1110
630
  try {
@@ -1116,14 +636,14 @@ var loginCommand = new Command5("login").description("Log in to WaniWani").optio
1116
636
  formatOutput({ alreadyLoggedIn: true, refreshed: true }, true);
1117
637
  } else {
1118
638
  console.log(
1119
- chalk4.green("Session refreshed. You're still logged in.")
639
+ chalk3.green("Session refreshed. You're still logged in.")
1120
640
  );
1121
641
  }
1122
642
  return;
1123
643
  }
1124
644
  if (!json) {
1125
645
  console.log(
1126
- chalk4.yellow("Session expired. Starting new login flow...")
646
+ chalk3.yellow("Session expired. Starting new login flow...")
1127
647
  );
1128
648
  }
1129
649
  await auth.clear();
@@ -1132,7 +652,7 @@ var loginCommand = new Command5("login").description("Log in to WaniWani").optio
1132
652
  formatOutput({ alreadyLoggedIn: true }, true);
1133
653
  } else {
1134
654
  console.log(
1135
- chalk4.yellow(
655
+ chalk3.yellow(
1136
656
  "Already logged in. Use 'waniwani logout' to log out first."
1137
657
  )
1138
658
  );
@@ -1141,9 +661,9 @@ var loginCommand = new Command5("login").description("Log in to WaniWani").optio
1141
661
  }
1142
662
  }
1143
663
  if (!json) {
1144
- console.log(chalk4.bold("\nWaniWani CLI Login\n"));
664
+ console.log(chalk3.bold("\nWaniWani CLI Login\n"));
1145
665
  }
1146
- const spinner = ora3("Registering client...").start();
666
+ const spinner = ora("Registering client...").start();
1147
667
  const { client_id: clientId } = await registerClient();
1148
668
  spinner.text = "Preparing authentication...";
1149
669
  const codeVerifier = generateCodeVerifier();
@@ -1163,7 +683,7 @@ var loginCommand = new Command5("login").description("Log in to WaniWani").optio
1163
683
  console.log("Opening browser for authentication...\n");
1164
684
  console.log(`If the browser doesn't open, visit:
1165
685
  `);
1166
- console.log(chalk4.cyan(` ${authUrl.toString()}`));
686
+ console.log(chalk3.cyan(` ${authUrl.toString()}`));
1167
687
  console.log();
1168
688
  }
1169
689
  const callbackPromise = waitForCallback(state);
@@ -1180,6 +700,7 @@ var loginCommand = new Command5("login").description("Log in to WaniWani").optio
1180
700
  apiUrl
1181
701
  // RFC 8707 resource parameter
1182
702
  );
703
+ await config.ensureConfigDir();
1183
704
  await auth.setTokens(
1184
705
  tokenResponse.access_token,
1185
706
  tokenResponse.refresh_token,
@@ -1209,8 +730,8 @@ var loginCommand = new Command5("login").description("Log in to WaniWani").optio
1209
730
  });
1210
731
 
1211
732
  // src/commands/logout.ts
1212
- import { Command as Command6 } from "commander";
1213
- var logoutCommand = new Command6("logout").description("Log out from WaniWani").action(async (_, command) => {
733
+ import { Command as Command4 } from "commander";
734
+ var logoutCommand = new Command4("logout").description("Log out from WaniWani").action(async (_, command) => {
1214
735
  const globalOptions = command.optsWithGlobals();
1215
736
  const json = globalOptions.json ?? false;
1216
737
  try {
@@ -1238,76 +759,487 @@ var logoutCommand = new Command6("logout").description("Log out from WaniWani").
1238
759
  import { Command as Command20 } from "commander";
1239
760
 
1240
761
  // src/commands/mcp/delete.ts
1241
- import { Command as Command7 } from "commander";
1242
- import ora4 from "ora";
1243
- var deleteCommand = new Command7("delete").description("Delete the MCP sandbox").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
1244
- const globalOptions = command.optsWithGlobals();
1245
- const json = globalOptions.json ?? false;
1246
- try {
1247
- let mcpId = options.mcpId;
1248
- if (!mcpId) {
1249
- mcpId = await config.getMcpId();
1250
- if (!mcpId) {
1251
- throw new McpError("No active MCP. Use --mcp-id to specify one.");
1252
- }
1253
- }
1254
- const spinner = ora4("Deleting MCP sandbox...").start();
1255
- await api.delete(`/api/mcp/sandboxes/${mcpId}`);
1256
- spinner.succeed("MCP sandbox deleted");
1257
- if (await config.getMcpId() === mcpId) {
1258
- await config.setMcpId(null);
1259
- }
1260
- if (json) {
1261
- formatOutput({ deleted: mcpId }, true);
1262
- } else {
1263
- formatSuccess("MCP sandbox deleted and cleaned up.", false);
1264
- }
1265
- } catch (error) {
1266
- handleError(error, json);
1267
- process.exit(1);
1268
- }
1269
- });
762
+ import { confirm } from "@inquirer/prompts";
763
+ import chalk4 from "chalk";
764
+ import { Command as Command5 } from "commander";
765
+ import ora2 from "ora";
1270
766
 
1271
- // src/commands/mcp/deploy.ts
1272
- import { Command as Command8 } from "commander";
1273
- import ora5 from "ora";
1274
- var deployCommand = new Command8("deploy").description("Deploy MCP server to GitHub + Vercel from sandbox").option("--repo <name>", "GitHub repository name").option("--org <name>", "GitHub organization").option("--private", "Create private repository").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
1275
- const globalOptions = command.optsWithGlobals();
1276
- const json = globalOptions.json ?? false;
1277
- try {
1278
- let mcpId = options.mcpId;
1279
- if (!mcpId) {
1280
- mcpId = await config.getMcpId();
1281
- if (!mcpId) {
1282
- throw new McpError(
1283
- "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
1284
- );
767
+ // src/lib/api.ts
768
+ var ApiError = class extends CLIError {
769
+ constructor(message, code, statusCode, details) {
770
+ super(message, code, details);
771
+ this.statusCode = statusCode;
772
+ this.name = "ApiError";
773
+ }
774
+ };
775
+ async function request(method, path, options) {
776
+ const {
777
+ body,
778
+ requireAuth = true,
779
+ headers: extraHeaders = {}
780
+ } = options || {};
781
+ const headers = {
782
+ "Content-Type": "application/json",
783
+ ...extraHeaders
784
+ };
785
+ if (requireAuth) {
786
+ const token = await auth.getAccessToken();
787
+ if (!token) {
788
+ throw new AuthError(
789
+ "Not logged in. Run 'waniwani login' to authenticate."
790
+ );
791
+ }
792
+ headers.Authorization = `Bearer ${token}`;
793
+ }
794
+ const baseUrl = await config.getApiUrl();
795
+ const url = `${baseUrl}${path}`;
796
+ const response = await fetch(url, {
797
+ method,
798
+ headers,
799
+ body: body ? JSON.stringify(body) : void 0
800
+ });
801
+ if (response.status === 204) {
802
+ return void 0;
803
+ }
804
+ let data;
805
+ let rawBody;
806
+ try {
807
+ rawBody = await response.text();
808
+ data = JSON.parse(rawBody);
809
+ } catch {
810
+ throw new ApiError(
811
+ rawBody || `Request failed with status ${response.status}`,
812
+ "API_ERROR",
813
+ response.status,
814
+ { statusText: response.statusText }
815
+ );
816
+ }
817
+ if (!response.ok || data.error) {
818
+ const errorMessage = data.error?.message || data.message || data.error || rawBody || `Request failed with status ${response.status}`;
819
+ const errorCode = data.error?.code || data.code || "API_ERROR";
820
+ const errorDetails = {
821
+ ...data.error?.details,
822
+ statusText: response.statusText,
823
+ ...data.error ? {} : { rawResponse: data }
824
+ };
825
+ const error = {
826
+ code: errorCode,
827
+ message: errorMessage,
828
+ details: errorDetails
829
+ };
830
+ if (response.status === 401) {
831
+ const refreshed = await auth.tryRefreshToken();
832
+ if (refreshed) {
833
+ return request(method, path, options);
1285
834
  }
835
+ throw new AuthError(
836
+ "Session expired. Run 'waniwani login' to re-authenticate."
837
+ );
1286
838
  }
1287
- const spinner = ora5("Deploying to GitHub...").start();
1288
- const result = await api.post(
1289
- `/api/mcp/sandboxes/${mcpId}/deploy`,
1290
- {
1291
- repoName: options.repo,
1292
- org: options.org,
1293
- private: options.private ?? false
839
+ throw new ApiError(
840
+ error.message,
841
+ error.code,
842
+ response.status,
843
+ error.details
844
+ );
845
+ }
846
+ return data.data;
847
+ }
848
+ var api = {
849
+ get: (path, options) => request("GET", path, options),
850
+ post: (path, body, options) => request("POST", path, { body, ...options }),
851
+ delete: (path, options) => request("DELETE", path, options),
852
+ getBaseUrl: () => config.getApiUrl()
853
+ };
854
+
855
+ // src/lib/utils.ts
856
+ async function requireMcpId(mcpId) {
857
+ if (mcpId) return mcpId;
858
+ const configMcpId = await config.getMcpId();
859
+ if (!configMcpId) {
860
+ throw new McpError(
861
+ "No active MCP. Run 'waniwani mcp init <name>' or 'waniwani mcp use <name>'."
862
+ );
863
+ }
864
+ return configMcpId;
865
+ }
866
+ async function requireSessionId() {
867
+ const sessionId = await config.getSessionId();
868
+ if (!sessionId) {
869
+ throw new McpError(
870
+ "No active session. Run 'waniwani mcp dev' to start development."
871
+ );
872
+ }
873
+ return sessionId;
874
+ }
875
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
876
+ ".png",
877
+ ".jpg",
878
+ ".jpeg",
879
+ ".gif",
880
+ ".ico",
881
+ ".webp",
882
+ ".svg",
883
+ ".woff",
884
+ ".woff2",
885
+ ".ttf",
886
+ ".eot",
887
+ ".otf",
888
+ ".zip",
889
+ ".tar",
890
+ ".gz",
891
+ ".pdf",
892
+ ".exe",
893
+ ".dll",
894
+ ".so",
895
+ ".dylib",
896
+ ".bin",
897
+ ".mp3",
898
+ ".mp4",
899
+ ".wav",
900
+ ".ogg",
901
+ ".webm"
902
+ ]);
903
+ function isBinaryPath(filePath) {
904
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
905
+ return BINARY_EXTENSIONS.has(ext);
906
+ }
907
+ function detectBinary(buffer) {
908
+ const sample = buffer.subarray(0, 8192);
909
+ return sample.includes(0);
910
+ }
911
+
912
+ // src/commands/mcp/delete.ts
913
+ var deleteCommand = new Command5("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) => {
914
+ const globalOptions = command.optsWithGlobals();
915
+ const json = globalOptions.json ?? false;
916
+ try {
917
+ const mcpId = await requireMcpId(options.mcpId);
918
+ const mcp = await api.get(
919
+ `/api/mcp/repositories/${mcpId}`
920
+ );
921
+ if (!options.force && !json) {
922
+ console.log();
923
+ console.log(chalk4.yellow("This will permanently delete:"));
924
+ console.log(` - MCP: ${mcp.name}`);
925
+ if (mcp.activeSandbox) {
926
+ console.log(" - Active sandbox");
927
+ }
928
+ console.log();
929
+ const confirmed = await confirm({
930
+ message: `Delete "${mcp.name}"?`,
931
+ default: false
932
+ });
933
+ if (!confirmed) {
934
+ console.log("Cancelled.");
935
+ return;
936
+ }
937
+ }
938
+ const spinner = ora2("Deleting MCP...").start();
939
+ await api.delete(`/api/mcp/repositories/${mcpId}`);
940
+ spinner.succeed("MCP deleted");
941
+ if (await config.getMcpId() === mcpId) {
942
+ await config.setMcpId(null);
943
+ await config.setSessionId(null);
944
+ }
945
+ if (json) {
946
+ formatOutput({ deleted: mcpId }, true);
947
+ } else {
948
+ formatSuccess("MCP deleted.", false);
949
+ }
950
+ } catch (error) {
951
+ handleError(error, json);
952
+ process.exit(1);
953
+ }
954
+ });
955
+
956
+ // src/commands/mcp/deploy.ts
957
+ import { input } from "@inquirer/prompts";
958
+ import { Command as Command6 } from "commander";
959
+ import ora3 from "ora";
960
+
961
+ // src/lib/sync.ts
962
+ import { existsSync as existsSync3 } from "fs";
963
+ import { mkdir as mkdir2, readdir, readFile as readFile2, stat, writeFile as writeFile2 } from "fs/promises";
964
+ import { dirname, join as join3, relative } from "path";
965
+ import ignore from "ignore";
966
+ var PROJECT_DIR = ".waniwani";
967
+ async function findProjectRoot(startDir) {
968
+ let current = startDir;
969
+ const root = dirname(current);
970
+ while (current !== root) {
971
+ if (existsSync3(join3(current, PROJECT_DIR))) {
972
+ return current;
973
+ }
974
+ const parent = dirname(current);
975
+ if (parent === current) break;
976
+ current = parent;
977
+ }
978
+ if (existsSync3(join3(current, PROJECT_DIR))) {
979
+ return current;
980
+ }
981
+ return null;
982
+ }
983
+ var DEFAULT_IGNORE_PATTERNS = [
984
+ ".waniwani",
985
+ ".git",
986
+ "node_modules",
987
+ ".env",
988
+ ".env.*",
989
+ ".DS_Store",
990
+ "*.log",
991
+ ".cache",
992
+ "dist",
993
+ "coverage",
994
+ ".turbo",
995
+ ".next",
996
+ ".nuxt",
997
+ ".vercel"
998
+ ];
999
+ async function loadIgnorePatterns(projectRoot) {
1000
+ const ig = ignore();
1001
+ ig.add(DEFAULT_IGNORE_PATTERNS);
1002
+ const gitignorePath = join3(projectRoot, ".gitignore");
1003
+ if (existsSync3(gitignorePath)) {
1004
+ try {
1005
+ const content = await readFile2(gitignorePath, "utf-8");
1006
+ ig.add(content);
1007
+ } catch {
1008
+ }
1009
+ }
1010
+ return ig;
1011
+ }
1012
+ async function collectFiles(projectRoot) {
1013
+ const ig = await loadIgnorePatterns(projectRoot);
1014
+ const files = [];
1015
+ async function walk(dir) {
1016
+ const entries = await readdir(dir, { withFileTypes: true });
1017
+ for (const entry of entries) {
1018
+ const fullPath = join3(dir, entry.name);
1019
+ const relativePath = relative(projectRoot, fullPath);
1020
+ if (ig.ignores(relativePath)) {
1021
+ continue;
1022
+ }
1023
+ if (entry.isDirectory()) {
1024
+ await walk(fullPath);
1025
+ } else if (entry.isFile()) {
1026
+ try {
1027
+ const content = await readFile2(fullPath);
1028
+ const isBinary = isBinaryPath(fullPath) || detectBinary(content);
1029
+ files.push({
1030
+ path: relativePath,
1031
+ content: isBinary ? content.toString("base64") : content.toString("utf8"),
1032
+ encoding: isBinary ? "base64" : "utf8"
1033
+ });
1034
+ } catch {
1035
+ }
1294
1036
  }
1037
+ }
1038
+ }
1039
+ await walk(projectRoot);
1040
+ return files;
1041
+ }
1042
+ async function pullFilesFromGithub(mcpId, targetDir) {
1043
+ const result = await api.get(
1044
+ `/api/mcp/repositories/${mcpId}/files`
1045
+ );
1046
+ const writtenFiles = [];
1047
+ for (const file of result.files) {
1048
+ const localPath = join3(targetDir, file.path);
1049
+ const dir = dirname(localPath);
1050
+ await mkdir2(dir, { recursive: true });
1051
+ if (file.encoding === "base64") {
1052
+ await writeFile2(localPath, Buffer.from(file.content, "base64"));
1053
+ } else {
1054
+ await writeFile2(localPath, file.content, "utf8");
1055
+ }
1056
+ writtenFiles.push(file.path);
1057
+ }
1058
+ return { count: writtenFiles.length, files: writtenFiles };
1059
+ }
1060
+ async function collectSingleFile(projectRoot, filePath) {
1061
+ const fullPath = join3(projectRoot, filePath);
1062
+ const relativePath = relative(projectRoot, fullPath);
1063
+ if (!existsSync3(fullPath)) {
1064
+ return null;
1065
+ }
1066
+ try {
1067
+ const fileStat = await stat(fullPath);
1068
+ if (!fileStat.isFile()) {
1069
+ return null;
1070
+ }
1071
+ const content = await readFile2(fullPath);
1072
+ const isBinary = isBinaryPath(fullPath) || detectBinary(content);
1073
+ return {
1074
+ path: relativePath,
1075
+ content: isBinary ? content.toString("base64") : content.toString("utf8"),
1076
+ encoding: isBinary ? "base64" : "utf8"
1077
+ };
1078
+ } catch {
1079
+ return null;
1080
+ }
1081
+ }
1082
+
1083
+ // src/commands/mcp/deploy.ts
1084
+ var deployCommand = new Command6("deploy").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) => {
1085
+ const globalOptions = command.optsWithGlobals();
1086
+ const json = globalOptions.json ?? false;
1087
+ try {
1088
+ const mcpId = await requireMcpId(options.mcpId);
1089
+ const projectRoot = await findProjectRoot(process.cwd());
1090
+ if (!projectRoot) {
1091
+ throw new CLIError(
1092
+ "Not in a WaniWani project. Run 'waniwani mcp init <name>' first.",
1093
+ "NOT_IN_PROJECT"
1094
+ );
1095
+ }
1096
+ let message = options.message;
1097
+ if (!message) {
1098
+ message = await input({
1099
+ message: "Commit message:",
1100
+ validate: (value) => value.trim() ? true : "Commit message is required"
1101
+ });
1102
+ }
1103
+ const spinner = ora3("Collecting files...").start();
1104
+ const files = await collectFiles(projectRoot);
1105
+ if (files.length === 0) {
1106
+ spinner.fail("No files to deploy");
1107
+ return;
1108
+ }
1109
+ spinner.text = `Pushing ${files.length} files to GitHub...`;
1110
+ const result = await api.post(
1111
+ `/api/mcp/repositories/${mcpId}/deploy`,
1112
+ { files, message }
1295
1113
  );
1296
- spinner.succeed("Deployment complete!");
1114
+ spinner.succeed(`Pushed to GitHub (${result.commitSha.slice(0, 7)})`);
1297
1115
  if (json) {
1298
1116
  formatOutput(result, true);
1299
1117
  } else {
1300
1118
  console.log();
1301
- formatSuccess("MCP server deployed!", false);
1119
+ formatSuccess("Files pushed to GitHub!", false);
1302
1120
  console.log();
1303
- console.log(` Repository: ${result.repository.url}`);
1304
- if (result.deployment.url) {
1305
- console.log(` Deployment: ${result.deployment.url}`);
1121
+ console.log("Deployment will start automatically via webhook.");
1122
+ }
1123
+ } catch (error) {
1124
+ handleError(error, json);
1125
+ process.exit(1);
1126
+ }
1127
+ });
1128
+
1129
+ // src/commands/mcp/dev.ts
1130
+ import { watch } from "chokidar";
1131
+ import { Command as Command7 } from "commander";
1132
+ import ora4 from "ora";
1133
+ var devCommand = new Command7("dev").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) => {
1134
+ const globalOptions = command.optsWithGlobals();
1135
+ const json = globalOptions.json ?? false;
1136
+ try {
1137
+ const projectRoot = await findProjectRoot(process.cwd());
1138
+ if (!projectRoot) {
1139
+ throw new CLIError(
1140
+ "Not in a WaniWani project. Run 'waniwani mcp init <name>' first.",
1141
+ "NOT_IN_PROJECT"
1142
+ );
1143
+ }
1144
+ let mcpId = options.mcpId;
1145
+ if (!mcpId) {
1146
+ mcpId = await config.getMcpId();
1147
+ }
1148
+ if (!mcpId) {
1149
+ throw new CLIError(
1150
+ "No MCP found. Run 'waniwani mcp init <name>' or use --mcp-id.",
1151
+ "NO_MCP"
1152
+ );
1153
+ }
1154
+ const spinner = ora4("Starting development environment...").start();
1155
+ spinner.text = "Starting session...";
1156
+ let sessionId;
1157
+ try {
1158
+ const sessionResponse = await api.post(
1159
+ `/api/mcp/repositories/${mcpId}/session`,
1160
+ {}
1161
+ );
1162
+ sessionId = sessionResponse.sandbox.id;
1163
+ } catch {
1164
+ const existing = await api.get(
1165
+ `/api/mcp/repositories/${mcpId}/session`
1166
+ );
1167
+ if (!existing) {
1168
+ throw new CLIError("Failed to start session", "SESSION_ERROR");
1306
1169
  }
1170
+ sessionId = existing.id;
1171
+ }
1172
+ await config.setSessionId(sessionId);
1173
+ spinner.text = "Syncing files to sandbox...";
1174
+ const files = await collectFiles(projectRoot);
1175
+ if (files.length > 0) {
1176
+ await api.post(
1177
+ `/api/mcp/sessions/${sessionId}/files`,
1178
+ { files }
1179
+ );
1180
+ }
1181
+ spinner.text = "Starting MCP server...";
1182
+ const serverResult = await api.post(
1183
+ `/api/mcp/sessions/${sessionId}/server`,
1184
+ { action: "start" }
1185
+ );
1186
+ spinner.succeed("Development environment ready");
1187
+ console.log();
1188
+ formatSuccess("Live preview started!", false);
1189
+ console.log();
1190
+ console.log(` Preview URL: ${serverResult.previewUrl}`);
1191
+ console.log();
1192
+ console.log(` MCP Inspector:`);
1193
+ console.log(
1194
+ ` npx @anthropic-ai/mcp-inspector@latest "${serverResult.previewUrl}/mcp"`
1195
+ );
1196
+ console.log();
1197
+ if (options.watch !== false) {
1198
+ console.log("Watching for file changes... (Ctrl+C to stop)");
1307
1199
  console.log();
1308
- if (result.deployment.note) {
1309
- console.log(`Note: ${result.deployment.note}`);
1310
- }
1200
+ const ig = await loadIgnorePatterns(projectRoot);
1201
+ const watcher = watch(projectRoot, {
1202
+ ignored: (path) => {
1203
+ const relative2 = path.replace(`${projectRoot}/`, "");
1204
+ if (relative2 === path) return false;
1205
+ return ig.ignores(relative2);
1206
+ },
1207
+ persistent: true,
1208
+ ignoreInitial: true,
1209
+ awaitWriteFinish: {
1210
+ stabilityThreshold: 100,
1211
+ pollInterval: 50
1212
+ }
1213
+ });
1214
+ const syncFile = async (filePath) => {
1215
+ const relativePath = filePath.replace(`${projectRoot}/`, "");
1216
+ const file = await collectSingleFile(projectRoot, relativePath);
1217
+ if (file) {
1218
+ try {
1219
+ await api.post(
1220
+ `/api/mcp/sessions/${sessionId}/files`,
1221
+ { files: [file] }
1222
+ );
1223
+ console.log(` Synced: ${relativePath}`);
1224
+ } catch {
1225
+ console.error(` Failed to sync: ${relativePath}`);
1226
+ }
1227
+ }
1228
+ };
1229
+ watcher.on("add", syncFile);
1230
+ watcher.on("change", syncFile);
1231
+ watcher.on("unlink", (filePath) => {
1232
+ const relativePath = filePath.replace(`${projectRoot}/`, "");
1233
+ console.log(` Deleted: ${relativePath}`);
1234
+ });
1235
+ process.on("SIGINT", async () => {
1236
+ console.log();
1237
+ console.log("Stopping development environment...");
1238
+ await watcher.close();
1239
+ process.exit(0);
1240
+ });
1241
+ await new Promise(() => {
1242
+ });
1311
1243
  }
1312
1244
  } catch (error) {
1313
1245
  handleError(error, json);
@@ -1316,28 +1248,21 @@ var deployCommand = new Command8("deploy").description("Deploy MCP server to Git
1316
1248
  });
1317
1249
 
1318
1250
  // src/commands/mcp/file/index.ts
1319
- import { Command as Command12 } from "commander";
1251
+ import { Command as Command11 } from "commander";
1320
1252
 
1321
1253
  // src/commands/mcp/file/list.ts
1322
1254
  import chalk5 from "chalk";
1323
- import { Command as Command9 } from "commander";
1324
- import ora6 from "ora";
1325
- 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) => {
1255
+ import { Command as Command8 } from "commander";
1256
+ import ora5 from "ora";
1257
+ 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) => {
1326
1258
  const globalOptions = command.optsWithGlobals();
1327
1259
  const json = globalOptions.json ?? false;
1328
1260
  try {
1329
- let mcpId = options.mcpId;
1330
- if (!mcpId) {
1331
- mcpId = await config.getMcpId();
1332
- if (!mcpId) {
1333
- throw new McpError(
1334
- "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
1335
- );
1336
- }
1337
- }
1338
- const spinner = ora6(`Listing ${path}...`).start();
1261
+ await requireMcpId(options.mcpId);
1262
+ const sessionId = await requireSessionId();
1263
+ const spinner = ora5(`Listing ${path}...`).start();
1339
1264
  const result = await api.get(
1340
- `/api/mcp/sandboxes/${mcpId}/files/list?path=${encodeURIComponent(path)}`
1265
+ `/api/mcp/sessions/${sessionId}/files/list?path=${encodeURIComponent(path)}`
1341
1266
  );
1342
1267
  spinner.stop();
1343
1268
  if (json) {
@@ -1371,26 +1296,19 @@ function formatSize(bytes) {
1371
1296
  }
1372
1297
 
1373
1298
  // src/commands/mcp/file/read.ts
1374
- import { writeFile as writeFile4 } from "fs/promises";
1375
- import { Command as Command10 } from "commander";
1376
- import ora7 from "ora";
1377
- 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) => {
1299
+ import { writeFile as writeFile3 } from "fs/promises";
1300
+ import { Command as Command9 } from "commander";
1301
+ import ora6 from "ora";
1302
+ 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) => {
1378
1303
  const globalOptions = command.optsWithGlobals();
1379
1304
  const json = globalOptions.json ?? false;
1380
1305
  try {
1381
- let mcpId = options.mcpId;
1382
- if (!mcpId) {
1383
- mcpId = await config.getMcpId();
1384
- if (!mcpId) {
1385
- throw new McpError(
1386
- "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
1387
- );
1388
- }
1389
- }
1306
+ await requireMcpId(options.mcpId);
1307
+ const sessionId = await requireSessionId();
1390
1308
  const encoding = options.base64 ? "base64" : "utf8";
1391
- const spinner = ora7(`Reading ${path}...`).start();
1309
+ const spinner = ora6(`Reading ${path}...`).start();
1392
1310
  const result = await api.get(
1393
- `/api/mcp/sandboxes/${mcpId}/files?path=${encodeURIComponent(path)}&encoding=${encoding}`
1311
+ `/api/mcp/sessions/${sessionId}/files?path=${encodeURIComponent(path)}&encoding=${encoding}`
1394
1312
  );
1395
1313
  spinner.stop();
1396
1314
  if (!result.exists) {
@@ -1398,7 +1316,7 @@ var readCommand = new Command10("read").description("Read a file from the MCP sa
1398
1316
  }
1399
1317
  if (options.output) {
1400
1318
  const buffer = result.encoding === "base64" ? Buffer.from(result.content, "base64") : Buffer.from(result.content, "utf8");
1401
- await writeFile4(options.output, buffer);
1319
+ await writeFile3(options.output, buffer);
1402
1320
  if (json) {
1403
1321
  formatOutput({ path, savedTo: options.output }, true);
1404
1322
  } else {
@@ -1419,22 +1337,15 @@ var readCommand = new Command10("read").description("Read a file from the MCP sa
1419
1337
  });
1420
1338
 
1421
1339
  // src/commands/mcp/file/write.ts
1422
- import { readFile as readFile5 } from "fs/promises";
1423
- import { Command as Command11 } from "commander";
1424
- import ora8 from "ora";
1425
- 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) => {
1340
+ import { readFile as readFile3 } from "fs/promises";
1341
+ import { Command as Command10 } from "commander";
1342
+ import ora7 from "ora";
1343
+ 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) => {
1426
1344
  const globalOptions = command.optsWithGlobals();
1427
1345
  const json = globalOptions.json ?? false;
1428
1346
  try {
1429
- let mcpId = options.mcpId;
1430
- if (!mcpId) {
1431
- mcpId = await config.getMcpId();
1432
- if (!mcpId) {
1433
- throw new McpError(
1434
- "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
1435
- );
1436
- }
1437
- }
1347
+ await requireMcpId(options.mcpId);
1348
+ const sessionId = await requireSessionId();
1438
1349
  let content;
1439
1350
  let encoding = "utf8";
1440
1351
  if (options.content) {
@@ -1443,7 +1354,7 @@ var writeCommand = new Command11("write").description("Write a file to the MCP s
1443
1354
  encoding = "base64";
1444
1355
  }
1445
1356
  } else if (options.file) {
1446
- const fileBuffer = await readFile5(options.file);
1357
+ const fileBuffer = await readFile3(options.file);
1447
1358
  if (options.base64) {
1448
1359
  content = fileBuffer.toString("base64");
1449
1360
  encoding = "base64";
@@ -1456,9 +1367,9 @@ var writeCommand = new Command11("write").description("Write a file to the MCP s
1456
1367
  "MISSING_CONTENT"
1457
1368
  );
1458
1369
  }
1459
- const spinner = ora8(`Writing ${path}...`).start();
1370
+ const spinner = ora7(`Writing ${path}...`).start();
1460
1371
  const result = await api.post(
1461
- `/api/mcp/sandboxes/${mcpId}/files`,
1372
+ `/api/mcp/sessions/${sessionId}/files`,
1462
1373
  {
1463
1374
  files: [{ path, content, encoding }]
1464
1375
  }
@@ -1476,19 +1387,116 @@ var writeCommand = new Command11("write").description("Write a file to the MCP s
1476
1387
  });
1477
1388
 
1478
1389
  // src/commands/mcp/file/index.ts
1479
- var fileCommand = new Command12("file").description("File operations in MCP sandbox").addCommand(readCommand).addCommand(writeCommand).addCommand(listCommand);
1390
+ var fileCommand = new Command11("file").description("File operations in MCP sandbox").addCommand(readCommand).addCommand(writeCommand).addCommand(listCommand);
1391
+
1392
+ // src/commands/mcp/init.ts
1393
+ import { existsSync as existsSync4 } from "fs";
1394
+ import { readFile as readFile4 } from "fs/promises";
1395
+ import { join as join4 } from "path";
1396
+ import { Command as Command12 } from "commander";
1397
+ import { execa } from "execa";
1398
+ import ora8 from "ora";
1399
+ async function loadParentConfig(cwd) {
1400
+ const parentConfigPath = join4(cwd, LOCAL_CONFIG_DIR, CONFIG_FILE_NAME);
1401
+ if (!existsSync4(parentConfigPath)) {
1402
+ return null;
1403
+ }
1404
+ try {
1405
+ const content = await readFile4(parentConfigPath, "utf-8");
1406
+ const config2 = JSON.parse(content);
1407
+ const { mcpId: _, sessionId: __, ...rest } = config2;
1408
+ return rest;
1409
+ } catch {
1410
+ return null;
1411
+ }
1412
+ }
1413
+ var initCommand = new Command12("init").description("Create a new MCP project").argument("<name>", "Name for the MCP project").option("--no-clone", "Skip automatic git clone (just output the command)").action(async (name, options, command) => {
1414
+ const globalOptions = command.optsWithGlobals();
1415
+ const json = globalOptions.json ?? false;
1416
+ try {
1417
+ const cwd = process.cwd();
1418
+ const projectDir = join4(cwd, name);
1419
+ if (existsSync4(projectDir)) {
1420
+ if (json) {
1421
+ formatOutput(
1422
+ {
1423
+ success: false,
1424
+ error: `Directory "${name}" already exists`
1425
+ },
1426
+ true
1427
+ );
1428
+ } else {
1429
+ console.error(`Error: Directory "${name}" already exists`);
1430
+ }
1431
+ process.exit(1);
1432
+ }
1433
+ const spinner = ora8("Creating MCP...").start();
1434
+ const result = await api.post(
1435
+ "/api/mcp/repositories",
1436
+ { name }
1437
+ );
1438
+ if (options.clone !== false) {
1439
+ spinner.text = "Cloning repository...";
1440
+ await execa("git", ["clone", result.cloneUrl, name], { cwd });
1441
+ spinner.text = "Setting up project config...";
1442
+ const parentConfig = await loadParentConfig(cwd);
1443
+ await initConfigAt(projectDir, {
1444
+ ...parentConfig,
1445
+ mcpId: result.repository.id
1446
+ });
1447
+ spinner.succeed("MCP project created");
1448
+ } else {
1449
+ spinner.succeed("MCP created");
1450
+ }
1451
+ if (json) {
1452
+ formatOutput(
1453
+ {
1454
+ success: true,
1455
+ projectDir: options.clone !== false ? projectDir : null,
1456
+ mcpId: result.repository.id
1457
+ },
1458
+ true
1459
+ );
1460
+ } else {
1461
+ console.log();
1462
+ formatSuccess(`MCP "${name}" created!`, false);
1463
+ console.log();
1464
+ if (options.clone !== false) {
1465
+ console.log("Next steps:");
1466
+ console.log(` cd ${name}`);
1467
+ console.log(
1468
+ " waniwani mcp dev # Start live preview with file watching"
1469
+ );
1470
+ console.log(" waniwani mcp push # Deploy to production");
1471
+ } else {
1472
+ console.log("Clone your repository:");
1473
+ console.log(` ${result.cloneCommand}`);
1474
+ console.log(` cd ${name}`);
1475
+ console.log();
1476
+ console.log("Then start developing:");
1477
+ console.log(
1478
+ " waniwani mcp dev # Start live preview with file watching"
1479
+ );
1480
+ console.log(" waniwani mcp push # Deploy to production");
1481
+ }
1482
+ }
1483
+ } catch (error) {
1484
+ handleError(error, json);
1485
+ process.exit(1);
1486
+ }
1487
+ });
1480
1488
 
1481
1489
  // src/commands/mcp/list.ts
1482
1490
  import chalk6 from "chalk";
1483
1491
  import { Command as Command13 } from "commander";
1484
1492
  import ora9 from "ora";
1485
- var listCommand2 = new Command13("list").description("List all MCPs in your organization").option("--all", "Include stopped/expired MCPs").action(async (options, command) => {
1493
+ var listCommand2 = new Command13("list").description("List all MCPs in your organization").action(async (_, command) => {
1486
1494
  const globalOptions = command.optsWithGlobals();
1487
1495
  const json = globalOptions.json ?? false;
1488
1496
  try {
1489
1497
  const spinner = ora9("Fetching MCPs...").start();
1490
1498
  const mcps = await api.get(
1491
- `/api/mcp/sandboxes${options.all ? "?all=true" : ""}`
1499
+ "/api/mcp/repositories"
1492
1500
  );
1493
1501
  spinner.stop();
1494
1502
  const activeMcpId = await config.getMcpId();
@@ -1506,29 +1514,29 @@ var listCommand2 = new Command13("list").description("List all MCPs in your orga
1506
1514
  } else {
1507
1515
  if (mcps.length === 0) {
1508
1516
  console.log("No MCPs found.");
1509
- console.log("\nCreate a new MCP sandbox: waniwani mcp create <name>");
1517
+ console.log("\nCreate a new MCP: waniwani mcp init <name>");
1510
1518
  return;
1511
1519
  }
1512
1520
  console.log(chalk6.bold("\nMCPs:\n"));
1513
1521
  const rows = mcps.map((m) => {
1514
1522
  const isActive = m.id === activeMcpId;
1515
- const statusColor = m.status === "active" ? chalk6.green : m.status === "stopped" ? chalk6.red : chalk6.yellow;
1523
+ const deployStatus = m.deployedAt ? chalk6.green("Deployed") : chalk6.yellow("Pending");
1524
+ const sandboxStatus = m.activeSandbox ? chalk6.green("Active") : chalk6.gray("None");
1525
+ const lastDeploy = m.deployedAt ? new Date(m.deployedAt).toLocaleDateString() : chalk6.gray("Never");
1516
1526
  return [
1517
- isActive ? chalk6.cyan(`* ${m.id.slice(0, 8)}`) : ` ${m.id.slice(0, 8)}`,
1518
- m.name,
1519
- statusColor(m.status),
1520
- m.previewUrl,
1521
- m.createdAt ? new Date(m.createdAt).toLocaleString() : "N/A"
1527
+ isActive ? chalk6.cyan(`* ${m.name}`) : ` ${m.name}`,
1528
+ deployStatus,
1529
+ sandboxStatus,
1530
+ lastDeploy
1522
1531
  ];
1523
1532
  });
1524
- formatTable(
1525
- ["ID", "Name", "Status", "Preview URL", "Created"],
1526
- rows,
1527
- false
1528
- );
1533
+ formatTable(["Name", "Status", "Sandbox", "Last Deploy"], rows, false);
1529
1534
  console.log();
1530
1535
  if (activeMcpId) {
1531
- console.log(`Active MCP: ${chalk6.cyan(activeMcpId.slice(0, 8))}`);
1536
+ const activeMcp = mcps.find((m) => m.id === activeMcpId);
1537
+ if (activeMcp) {
1538
+ console.log(`Active MCP: ${chalk6.cyan(activeMcp.name)}`);
1539
+ }
1532
1540
  }
1533
1541
  console.log("\nSelect an MCP: waniwani mcp use <name>");
1534
1542
  }
@@ -1556,15 +1564,8 @@ var logsCommand = new Command14("logs").description("Stream logs from the MCP se
1556
1564
  process.on("SIGINT", cleanup);
1557
1565
  process.on("SIGTERM", cleanup);
1558
1566
  try {
1559
- let mcpId = options.mcpId;
1560
- if (!mcpId) {
1561
- mcpId = await config.getMcpId();
1562
- if (!mcpId) {
1563
- throw new McpError(
1564
- "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
1565
- );
1566
- }
1567
- }
1567
+ await requireMcpId(options.mcpId);
1568
+ const sessionId = await requireSessionId();
1568
1569
  const token = await auth.getAccessToken();
1569
1570
  if (!token) {
1570
1571
  throw new AuthError(
@@ -1575,20 +1576,20 @@ var logsCommand = new Command14("logs").description("Stream logs from the MCP se
1575
1576
  if (!cmdId) {
1576
1577
  const spinner = ora10("Getting server status...").start();
1577
1578
  const status = await api.post(
1578
- `/api/mcp/sandboxes/${mcpId}/server`,
1579
+ `/api/mcp/sessions/${sessionId}/server`,
1579
1580
  { action: "status" }
1580
1581
  );
1581
1582
  spinner.stop();
1582
1583
  if (!status.running || !status.cmdId) {
1583
1584
  throw new McpError(
1584
- "No server is running. Run 'waniwani mcp start' first."
1585
+ "No server is running. Run 'waniwani mcp dev' first."
1585
1586
  );
1586
1587
  }
1587
1588
  cmdId = status.cmdId;
1588
1589
  }
1589
1590
  const baseUrl = await api.getBaseUrl();
1590
1591
  const streamParam = options.follow ? "?stream=true" : "";
1591
- const url = `${baseUrl}/api/mcp/sandboxes/${mcpId}/commands/${cmdId}${streamParam}`;
1592
+ const url = `${baseUrl}/api/mcp/sessions/${sessionId}/commands/${cmdId}${streamParam}`;
1592
1593
  if (!json) {
1593
1594
  console.log(chalk7.gray(`Streaming logs for command ${cmdId}...`));
1594
1595
  console.log(chalk7.gray("Press Ctrl+C to stop\n"));
@@ -1699,19 +1700,12 @@ var runCommandCommand = new Command15("run-command").description("Run a command
1699
1700
  const globalOptions = command.optsWithGlobals();
1700
1701
  const json = globalOptions.json ?? false;
1701
1702
  try {
1702
- let mcpId = options.mcpId;
1703
- if (!mcpId) {
1704
- mcpId = await config.getMcpId();
1705
- if (!mcpId) {
1706
- throw new McpError(
1707
- "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
1708
- );
1709
- }
1710
- }
1703
+ await requireMcpId(options.mcpId);
1704
+ const sessionId = await requireSessionId();
1711
1705
  const timeout = options.timeout ? Number.parseInt(options.timeout, 10) : void 0;
1712
1706
  const spinner = ora11(`Running: ${cmd} ${args.join(" ")}`.trim()).start();
1713
1707
  const result = await api.post(
1714
- `/api/mcp/sandboxes/${mcpId}/commands`,
1708
+ `/api/mcp/sessions/${sessionId}/commands`,
1715
1709
  {
1716
1710
  command: cmd,
1717
1711
  args: args.length > 0 ? args : void 0,
@@ -1754,49 +1748,76 @@ var runCommandCommand = new Command15("run-command").description("Run a command
1754
1748
  }
1755
1749
  });
1756
1750
 
1757
- // src/commands/mcp/start.ts
1751
+ // src/commands/mcp/status.ts
1758
1752
  import chalk9 from "chalk";
1759
1753
  import { Command as Command16 } from "commander";
1760
1754
  import ora12 from "ora";
1761
- var startCommand = new Command16("start").description("Start the MCP server (npm run dev)").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
1755
+ var statusCommand = new Command16("status").description("Show current MCP status").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
1762
1756
  const globalOptions = command.optsWithGlobals();
1763
1757
  const json = globalOptions.json ?? false;
1764
1758
  try {
1765
- let mcpId = options.mcpId;
1766
- if (!mcpId) {
1767
- mcpId = await config.getMcpId();
1768
- if (!mcpId) {
1769
- throw new McpError(
1770
- "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
1771
- );
1772
- }
1773
- }
1774
- const spinner = ora12("Starting MCP server...").start();
1775
- const result = await api.post(
1776
- `/api/mcp/sandboxes/${mcpId}/server`,
1777
- { action: "start" }
1759
+ const mcpId = await requireMcpId(options.mcpId);
1760
+ const spinner = ora12("Fetching MCP status...").start();
1761
+ const result = await api.get(
1762
+ `/api/mcp/repositories/${mcpId}`
1778
1763
  );
1779
- spinner.succeed("MCP server started");
1764
+ let serverStatus = null;
1765
+ if (result.activeSandbox) {
1766
+ serverStatus = await api.post(
1767
+ `/api/mcp/sessions/${result.activeSandbox.id}/server`,
1768
+ { action: "status" }
1769
+ ).catch(() => null);
1770
+ }
1771
+ spinner.stop();
1780
1772
  if (json) {
1781
- formatOutput(result, true);
1773
+ formatOutput({ ...result, server: serverStatus }, true);
1782
1774
  } else {
1775
+ const deployStatus = result.deployedAt ? chalk9.green("Deployed") : chalk9.yellow("Pending");
1776
+ const items = [
1777
+ { label: "Name", value: result.name },
1778
+ { label: "MCP ID", value: result.id },
1779
+ { label: "Status", value: deployStatus },
1780
+ {
1781
+ label: "Last Deploy",
1782
+ value: result.deployedAt ? new Date(result.deployedAt).toLocaleString() : chalk9.gray("Never")
1783
+ },
1784
+ {
1785
+ label: "Created",
1786
+ value: new Date(result.createdAt).toLocaleString()
1787
+ }
1788
+ ];
1789
+ if (result.activeSandbox) {
1790
+ const sandbox = result.activeSandbox;
1791
+ const serverRunning = serverStatus?.running ?? sandbox.serverRunning;
1792
+ const serverStatusColor = serverRunning ? chalk9.green : chalk9.yellow;
1793
+ items.push(
1794
+ { label: "", value: "" },
1795
+ // Separator
1796
+ { label: "Sandbox", value: chalk9.green("Active") },
1797
+ { label: "Preview URL", value: sandbox.previewUrl },
1798
+ {
1799
+ label: "Server",
1800
+ value: serverStatusColor(serverRunning ? "Running" : "Stopped")
1801
+ },
1802
+ {
1803
+ label: "Expires",
1804
+ value: sandbox.expiresAt ? new Date(sandbox.expiresAt).toLocaleString() : chalk9.gray("N/A")
1805
+ }
1806
+ );
1807
+ } else {
1808
+ items.push(
1809
+ { label: "", value: "" },
1810
+ // Separator
1811
+ { label: "Sandbox", value: chalk9.gray("None") }
1812
+ );
1813
+ }
1814
+ formatList(items, false);
1783
1815
  console.log();
1784
- formatList(
1785
- [
1786
- { label: "Command ID", value: result.cmdId },
1787
- { label: "Preview URL", value: chalk9.cyan(result.previewUrl) }
1788
- ],
1789
- false
1790
- );
1791
- console.log();
1792
- console.log(chalk9.bold("Test with MCP Inspector:"));
1793
- console.log(
1794
- ` npx @modelcontextprotocol/inspector --url ${result.previewUrl}/mcp`
1795
- );
1796
- console.log();
1797
- console.log(
1798
- chalk9.gray("Run 'waniwani mcp logs' to stream server output")
1799
- );
1816
+ if (!result.activeSandbox) {
1817
+ console.log("Start development: waniwani mcp dev");
1818
+ } else if (!serverStatus?.running) {
1819
+ console.log("View logs: waniwani mcp logs");
1820
+ }
1800
1821
  }
1801
1822
  } catch (error) {
1802
1823
  handleError(error, json);
@@ -1804,58 +1825,31 @@ var startCommand = new Command16("start").description("Start the MCP server (npm
1804
1825
  }
1805
1826
  });
1806
1827
 
1807
- // src/commands/mcp/status.ts
1808
- import chalk10 from "chalk";
1828
+ // src/commands/mcp/stop.ts
1809
1829
  import { Command as Command17 } from "commander";
1810
1830
  import ora13 from "ora";
1811
- var statusCommand = new Command17("status").description("Show current MCP sandbox status").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
1831
+ var stopCommand = new Command17("stop").description("Stop the development environment (sandbox + server)").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
1812
1832
  const globalOptions = command.optsWithGlobals();
1813
1833
  const json = globalOptions.json ?? false;
1814
1834
  try {
1815
- let mcpId = options.mcpId;
1816
- if (!mcpId) {
1817
- mcpId = await config.getMcpId();
1818
- if (!mcpId) {
1819
- throw new McpError(
1820
- "No active MCP. Run 'waniwani mcp create <name>' to create one or 'waniwani mcp use <name>' to select one."
1821
- );
1822
- }
1835
+ await requireMcpId(options.mcpId);
1836
+ const sessionId = await requireSessionId();
1837
+ const spinner = ora13("Stopping development environment...").start();
1838
+ try {
1839
+ await api.post(`/api/mcp/sessions/${sessionId}/server`, {
1840
+ action: "stop"
1841
+ });
1842
+ } catch {
1823
1843
  }
1824
- const spinner = ora13("Fetching MCP status...").start();
1825
- const [result, serverStatus] = await Promise.all([
1826
- api.get(`/api/mcp/sandboxes/${mcpId}`),
1827
- api.post(`/api/mcp/sandboxes/${mcpId}/server`, {
1828
- action: "status"
1829
- }).catch(() => ({
1830
- running: false,
1831
- cmdId: void 0,
1832
- previewUrl: void 0
1833
- }))
1834
- ]);
1835
- spinner.stop();
1844
+ await api.delete(`/api/mcp/sessions/${sessionId}`);
1845
+ await config.setSessionId(null);
1846
+ spinner.succeed("Development environment stopped");
1836
1847
  if (json) {
1837
- formatOutput({ ...result, server: serverStatus }, true);
1848
+ formatOutput({ stopped: true }, true);
1838
1849
  } else {
1839
- const statusColor = result.status === "active" ? chalk10.green : chalk10.red;
1840
- const serverRunning = serverStatus.running;
1841
- const serverStatusColor = serverRunning ? chalk10.green : chalk10.yellow;
1842
- formatList(
1843
- [
1844
- { label: "MCP ID", value: result.id },
1845
- { label: "Name", value: result.name },
1846
- { label: "Status", value: statusColor(result.status) },
1847
- { label: "Sandbox ID", value: result.sandboxId },
1848
- { label: "Preview URL", value: result.previewUrl },
1849
- {
1850
- label: "Server",
1851
- value: serverStatusColor(serverRunning ? "Running" : "Stopped")
1852
- },
1853
- ...serverStatus.cmdId ? [{ label: "Server Cmd ID", value: serverStatus.cmdId }] : [],
1854
- { label: "Created", value: result.createdAt },
1855
- { label: "Expires", value: result.expiresAt ?? "N/A" }
1856
- ],
1857
- false
1858
- );
1850
+ formatSuccess("Sandbox stopped.", false);
1851
+ console.log();
1852
+ console.log("Start again: waniwani mcp dev");
1859
1853
  }
1860
1854
  } catch (error) {
1861
1855
  handleError(error, json);
@@ -1863,36 +1857,35 @@ var statusCommand = new Command17("status").description("Show current MCP sandbo
1863
1857
  }
1864
1858
  });
1865
1859
 
1866
- // src/commands/mcp/stop.ts
1860
+ // src/commands/mcp/sync.ts
1867
1861
  import { Command as Command18 } from "commander";
1868
1862
  import ora14 from "ora";
1869
- var stopCommand = new Command18("stop").description("Stop the MCP server process").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
1863
+ var syncCommand = new Command18("sync").description("Pull files from GitHub to local project").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
1870
1864
  const globalOptions = command.optsWithGlobals();
1871
1865
  const json = globalOptions.json ?? false;
1872
1866
  try {
1873
- let mcpId = options.mcpId;
1874
- if (!mcpId) {
1875
- mcpId = await config.getMcpId();
1876
- if (!mcpId) {
1877
- throw new McpError(
1878
- "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
1879
- );
1880
- }
1881
- }
1882
- const spinner = ora14("Stopping MCP server...").start();
1883
- const result = await api.post(
1884
- `/api/mcp/sandboxes/${mcpId}/server`,
1885
- { action: "stop" }
1886
- );
1887
- if (result.stopped) {
1888
- spinner.succeed("MCP server stopped");
1889
- } else {
1890
- spinner.warn("Server was not running");
1867
+ const mcpId = await requireMcpId(options.mcpId);
1868
+ const projectRoot = await findProjectRoot(process.cwd());
1869
+ if (!projectRoot) {
1870
+ throw new CLIError(
1871
+ "Not in a WaniWani project. Run 'waniwani mcp init <name>' first.",
1872
+ "NOT_IN_PROJECT"
1873
+ );
1891
1874
  }
1875
+ const spinner = ora14("Pulling files from GitHub...").start();
1876
+ const result = await pullFilesFromGithub(mcpId, projectRoot);
1877
+ spinner.succeed(`Pulled ${result.count} files from GitHub`);
1892
1878
  if (json) {
1893
- formatOutput(result, true);
1879
+ formatOutput({ files: result.files }, true);
1894
1880
  } else {
1895
- formatSuccess("MCP server stopped.", false);
1881
+ console.log();
1882
+ formatSuccess("Files synced from GitHub!", false);
1883
+ if (result.files.length > 0 && result.files.length <= 10) {
1884
+ console.log();
1885
+ for (const file of result.files) {
1886
+ console.log(` ${file}`);
1887
+ }
1888
+ }
1896
1889
  }
1897
1890
  } catch (error) {
1898
1891
  handleError(error, json);
@@ -1903,12 +1896,14 @@ var stopCommand = new Command18("stop").description("Stop the MCP server process
1903
1896
  // src/commands/mcp/use.ts
1904
1897
  import { Command as Command19 } from "commander";
1905
1898
  import ora15 from "ora";
1906
- var useCommand = new Command19("use").description("Select an MCP to use for subsequent commands").argument("<name>", "Name of the MCP to use").option("--global", "Save to global config instead of project config").action(async (name, options, command) => {
1899
+ var useCommand = new Command19("use").description("Select an MCP to use for subsequent commands").argument("<name>", "Name of the MCP to use").action(async (name, _options, command) => {
1907
1900
  const globalOptions = command.optsWithGlobals();
1908
1901
  const json = globalOptions.json ?? false;
1909
1902
  try {
1910
1903
  const spinner = ora15("Fetching MCPs...").start();
1911
- const mcps = await api.get("/api/mcp/sandboxes");
1904
+ const mcps = await api.get(
1905
+ "/api/mcp/repositories"
1906
+ );
1912
1907
  spinner.stop();
1913
1908
  const mcp = mcps.find((m) => m.name === name);
1914
1909
  if (!mcp) {
@@ -1916,25 +1911,18 @@ var useCommand = new Command19("use").description("Select an MCP to use for subs
1916
1911
  `MCP "${name}" not found. Run 'waniwani mcp list' to see available MCPs.`
1917
1912
  );
1918
1913
  }
1919
- if (mcp.status !== "active") {
1920
- throw new McpError(
1921
- `MCP "${name}" is ${mcp.status}. Only active MCPs can be used.`
1922
- );
1923
- }
1924
- const cfg = options.global ? globalConfig : config;
1925
- await cfg.setMcpId(mcp.id);
1914
+ await config.setMcpId(mcp.id);
1915
+ await config.setSessionId(null);
1926
1916
  if (json) {
1927
- formatOutput({ selected: mcp, scope: cfg.scope }, true);
1917
+ formatOutput({ selected: mcp }, true);
1928
1918
  } else {
1929
- formatSuccess(`Now using MCP "${name}" (${cfg.scope})`, false);
1919
+ formatSuccess(`Now using MCP "${name}"`, false);
1930
1920
  console.log();
1931
- console.log(` MCP ID: ${mcp.id}`);
1932
- console.log(` Preview URL: ${mcp.previewUrl}`);
1921
+ console.log(` MCP ID: ${mcp.id}`);
1933
1922
  console.log();
1934
1923
  console.log("Next steps:");
1935
- console.log(' waniwani task "Add a tool"');
1936
- console.log(" waniwani mcp test");
1937
- console.log(" waniwani mcp status");
1924
+ console.log(" waniwani mcp dev # Start live preview");
1925
+ console.log(" waniwani mcp status # Check status");
1938
1926
  }
1939
1927
  } catch (error) {
1940
1928
  handleError(error, json);
@@ -1943,13 +1931,13 @@ var useCommand = new Command19("use").description("Select an MCP to use for subs
1943
1931
  });
1944
1932
 
1945
1933
  // src/commands/mcp/index.ts
1946
- var mcpCommand = new Command20("mcp").description("MCP sandbox management commands").addCommand(listCommand2).addCommand(useCommand).addCommand(statusCommand).addCommand(startCommand).addCommand(stopCommand).addCommand(logsCommand).addCommand(deleteCommand).addCommand(deployCommand).addCommand(fileCommand).addCommand(runCommandCommand);
1934
+ var mcpCommand = new Command20("mcp").description("MCP management commands").addCommand(initCommand).addCommand(listCommand2).addCommand(useCommand).addCommand(statusCommand).addCommand(devCommand).addCommand(stopCommand).addCommand(logsCommand).addCommand(syncCommand).addCommand(deployCommand).addCommand(deleteCommand).addCommand(fileCommand).addCommand(runCommandCommand);
1947
1935
 
1948
1936
  // src/commands/org/index.ts
1949
1937
  import { Command as Command23 } from "commander";
1950
1938
 
1951
1939
  // src/commands/org/list.ts
1952
- import chalk11 from "chalk";
1940
+ import chalk10 from "chalk";
1953
1941
  import { Command as Command21 } from "commander";
1954
1942
  import ora16 from "ora";
1955
1943
  var listCommand3 = new Command21("list").description("List your organizations").action(async (_, command) => {
@@ -1976,11 +1964,11 @@ var listCommand3 = new Command21("list").description("List your organizations").
1976
1964
  console.log("No organizations found.");
1977
1965
  return;
1978
1966
  }
1979
- console.log(chalk11.bold("\nOrganizations:\n"));
1967
+ console.log(chalk10.bold("\nOrganizations:\n"));
1980
1968
  const rows = orgs.map((o) => {
1981
1969
  const isActive = o.id === activeOrgId;
1982
1970
  return [
1983
- isActive ? chalk11.cyan(`* ${o.name}`) : ` ${o.name}`,
1971
+ isActive ? chalk10.cyan(`* ${o.name}`) : ` ${o.name}`,
1984
1972
  o.slug,
1985
1973
  o.role
1986
1974
  ];
@@ -1990,7 +1978,7 @@ var listCommand3 = new Command21("list").description("List your organizations").
1990
1978
  if (activeOrgId) {
1991
1979
  const activeOrg = orgs.find((o) => o.id === activeOrgId);
1992
1980
  if (activeOrg) {
1993
- console.log(`Active organization: ${chalk11.cyan(activeOrg.name)}`);
1981
+ console.log(`Active organization: ${chalk10.cyan(activeOrg.name)}`);
1994
1982
  }
1995
1983
  }
1996
1984
  console.log("\nSwitch organization: waniwani org switch <name>");
@@ -2043,95 +2031,11 @@ var switchCommand = new Command22("switch").description("Switch to a different o
2043
2031
  // src/commands/org/index.ts
2044
2032
  var orgCommand = new Command23("org").description("Organization management commands").addCommand(listCommand3).addCommand(switchCommand);
2045
2033
 
2046
- // src/commands/push.ts
2047
- import chalk12 from "chalk";
2048
- import { Command as Command24 } from "commander";
2049
- import ora18 from "ora";
2050
- var BATCH_SIZE2 = 50;
2051
- var pushCommand = new Command24("push").description("Sync local files to MCP sandbox").option("--dry-run", "Show what would be synced without uploading").action(async (options, command) => {
2052
- const globalOptions = command.optsWithGlobals();
2053
- const json = globalOptions.json ?? false;
2054
- try {
2055
- const cwd = process.cwd();
2056
- const projectRoot = await findProjectRoot(cwd);
2057
- if (!projectRoot) {
2058
- throw new CLIError(
2059
- "Not in a WaniWani project. Run 'waniwani init <name>' first.",
2060
- "NOT_IN_PROJECT"
2061
- );
2062
- }
2063
- const mcpId = await loadProjectMcpId(projectRoot);
2064
- if (!mcpId) {
2065
- throw new CLIError(
2066
- "No MCP ID found in project config. Run 'waniwani init <name>' first.",
2067
- "NO_MCP_ID"
2068
- );
2069
- }
2070
- const spinner = ora18("Collecting files...").start();
2071
- const files = await collectFiles(projectRoot);
2072
- if (files.length === 0) {
2073
- spinner.info("No files to sync");
2074
- if (json) {
2075
- formatOutput({ synced: 0, files: [] }, true);
2076
- }
2077
- return;
2078
- }
2079
- spinner.text = `Found ${files.length} files`;
2080
- if (options.dryRun) {
2081
- spinner.stop();
2082
- console.log();
2083
- console.log(chalk12.bold("Files that would be synced:"));
2084
- for (const file of files) {
2085
- console.log(` ${file.path}`);
2086
- }
2087
- console.log();
2088
- console.log(`Total: ${files.length} files`);
2089
- return;
2090
- }
2091
- const allWritten = [];
2092
- const totalBatches = Math.ceil(files.length / BATCH_SIZE2);
2093
- for (let i = 0; i < totalBatches; i++) {
2094
- const batch = files.slice(i * BATCH_SIZE2, (i + 1) * BATCH_SIZE2);
2095
- spinner.text = `Syncing files (${i + 1}/${totalBatches})...`;
2096
- const result = await api.post(
2097
- `/api/mcp/sandboxes/${mcpId}/files`,
2098
- {
2099
- files: batch.map((f) => ({
2100
- path: f.path,
2101
- content: f.content,
2102
- encoding: f.encoding
2103
- }))
2104
- }
2105
- );
2106
- allWritten.push(...result.written);
2107
- }
2108
- spinner.succeed(`Synced ${allWritten.length} files`);
2109
- if (json) {
2110
- formatOutput(
2111
- {
2112
- synced: allWritten.length,
2113
- files: allWritten
2114
- },
2115
- true
2116
- );
2117
- } else {
2118
- console.log();
2119
- formatSuccess(`${allWritten.length} files synced to sandbox`, false);
2120
- }
2121
- } catch (error) {
2122
- handleError(error, json);
2123
- process.exit(1);
2124
- }
2125
- });
2126
-
2127
2034
  // src/cli.ts
2128
2035
  var version = "0.1.0";
2129
- var program = new Command25().name("waniwani").description("WaniWani CLI for MCP development workflow").version(version).option("--json", "Output results as JSON").option("--verbose", "Enable verbose logging");
2036
+ var program = new Command24().name("waniwani").description("WaniWani CLI for MCP development workflow").version(version).option("--json", "Output results as JSON").option("--verbose", "Enable verbose logging");
2130
2037
  program.addCommand(loginCommand);
2131
2038
  program.addCommand(logoutCommand);
2132
- program.addCommand(initCommand);
2133
- program.addCommand(pushCommand);
2134
- program.addCommand(devCommand);
2135
2039
  program.addCommand(mcpCommand);
2136
2040
  program.addCommand(orgCommand);
2137
2041
  program.addCommand(configCommand);