@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 +868 -964
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
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
|
|
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().
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
return (await this.load()).apiUrl || DEFAULT_API_URL;
|
|
82
|
+
async getSessionId() {
|
|
83
|
+
return (await this.load()).sessionId;
|
|
70
84
|
}
|
|
71
|
-
async
|
|
85
|
+
async setSessionId(id) {
|
|
72
86
|
const data = await this.load();
|
|
73
|
-
data.
|
|
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/
|
|
239
|
-
import {
|
|
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
|
|
289
|
-
return !!
|
|
299
|
+
const token = await config.getAccessToken();
|
|
300
|
+
return !!token;
|
|
290
301
|
}
|
|
291
302
|
async getAccessToken() {
|
|
292
|
-
|
|
293
|
-
return store.accessToken;
|
|
303
|
+
return config.getAccessToken();
|
|
294
304
|
}
|
|
295
305
|
async getRefreshToken() {
|
|
296
|
-
|
|
297
|
-
return store.refreshToken;
|
|
306
|
+
return config.getRefreshToken();
|
|
298
307
|
}
|
|
299
308
|
async setTokens(accessToken, refreshToken, expiresIn, clientId) {
|
|
300
|
-
|
|
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
|
-
|
|
312
|
-
await this.saveStore(emptyStore);
|
|
312
|
+
return config.clearAuth();
|
|
313
313
|
}
|
|
314
314
|
async isTokenExpired() {
|
|
315
|
-
|
|
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
|
|
321
|
-
const
|
|
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/
|
|
354
|
-
var
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
".
|
|
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
|
|
488
|
-
const
|
|
489
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}
|
|
506
|
-
if (
|
|
507
|
-
|
|
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
|
|
393
|
+
return response.json();
|
|
510
394
|
}
|
|
511
|
-
async function
|
|
512
|
-
const
|
|
513
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
664
|
+
console.log(chalk3.bold("\nWaniWani CLI Login\n"));
|
|
1145
665
|
}
|
|
1146
|
-
const spinner =
|
|
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(
|
|
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
|
|
1213
|
-
var logoutCommand = new
|
|
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 {
|
|
1242
|
-
import
|
|
1243
|
-
|
|
1244
|
-
|
|
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/
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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(
|
|
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("
|
|
1119
|
+
formatSuccess("Files pushed to GitHub!", false);
|
|
1302
1120
|
console.log();
|
|
1303
|
-
console.log(
|
|
1304
|
-
|
|
1305
|
-
|
|
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
|
-
|
|
1309
|
-
|
|
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
|
|
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
|
|
1324
|
-
import
|
|
1325
|
-
var listCommand = new
|
|
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
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
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/
|
|
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
|
|
1375
|
-
import { Command as
|
|
1376
|
-
import
|
|
1377
|
-
var readCommand = new
|
|
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
|
-
|
|
1382
|
-
|
|
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 =
|
|
1309
|
+
const spinner = ora6(`Reading ${path}...`).start();
|
|
1392
1310
|
const result = await api.get(
|
|
1393
|
-
`/api/mcp/
|
|
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
|
|
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
|
|
1423
|
-
import { Command as
|
|
1424
|
-
import
|
|
1425
|
-
var writeCommand = new
|
|
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
|
-
|
|
1430
|
-
|
|
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
|
|
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 =
|
|
1370
|
+
const spinner = ora7(`Writing ${path}...`).start();
|
|
1460
1371
|
const result = await api.post(
|
|
1461
|
-
`/api/mcp/
|
|
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
|
|
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").
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1560
|
-
|
|
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/
|
|
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
|
|
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/
|
|
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
|
-
|
|
1703
|
-
|
|
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/
|
|
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/
|
|
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
|
|
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
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
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
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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({
|
|
1848
|
+
formatOutput({ stopped: true }, true);
|
|
1838
1849
|
} else {
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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/
|
|
1860
|
+
// src/commands/mcp/sync.ts
|
|
1867
1861
|
import { Command as Command18 } from "commander";
|
|
1868
1862
|
import ora14 from "ora";
|
|
1869
|
-
var
|
|
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
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
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
|
-
|
|
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").
|
|
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(
|
|
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
|
-
|
|
1920
|
-
|
|
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
|
|
1917
|
+
formatOutput({ selected: mcp }, true);
|
|
1928
1918
|
} else {
|
|
1929
|
-
formatSuccess(`Now using MCP "${name}"
|
|
1919
|
+
formatSuccess(`Now using MCP "${name}"`, false);
|
|
1930
1920
|
console.log();
|
|
1931
|
-
console.log(` 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(
|
|
1936
|
-
console.log(" waniwani mcp
|
|
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
|
|
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
|
|
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(
|
|
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 ?
|
|
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: ${
|
|
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
|
|
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);
|