@moxxy/cli 0.0.12 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +278 -112
  2. package/bin/moxxy +10 -0
  3. package/package.json +36 -53
  4. package/src/api-client.js +286 -0
  5. package/src/cli.js +341 -0
  6. package/src/commands/agent.js +413 -0
  7. package/src/commands/auth.js +326 -0
  8. package/src/commands/channel.js +285 -0
  9. package/src/commands/doctor.js +261 -0
  10. package/src/commands/events.js +80 -0
  11. package/src/commands/gateway.js +428 -0
  12. package/src/commands/heartbeat.js +145 -0
  13. package/src/commands/init.js +767 -0
  14. package/src/commands/mcp.js +278 -0
  15. package/src/commands/plugin.js +583 -0
  16. package/src/commands/provider.js +1934 -0
  17. package/src/commands/skill.js +125 -0
  18. package/src/commands/template.js +237 -0
  19. package/src/commands/uninstall.js +196 -0
  20. package/src/commands/update.js +406 -0
  21. package/src/commands/vault.js +219 -0
  22. package/src/help.js +368 -0
  23. package/src/lib/plugin-registry.js +98 -0
  24. package/src/platform.js +40 -0
  25. package/src/sse-client.js +79 -0
  26. package/src/tui/action-wizards.js +130 -0
  27. package/src/tui/app.jsx +859 -0
  28. package/src/tui/components/action-picker.jsx +86 -0
  29. package/src/tui/components/chat-panel.jsx +120 -0
  30. package/src/tui/components/footer.jsx +13 -0
  31. package/src/tui/components/header.jsx +45 -0
  32. package/src/tui/components/input-area.jsx +384 -0
  33. package/src/tui/components/messages/ask-message.jsx +13 -0
  34. package/src/tui/components/messages/assistant-message.jsx +165 -0
  35. package/src/tui/components/messages/channel-message.jsx +18 -0
  36. package/src/tui/components/messages/event-message.jsx +22 -0
  37. package/src/tui/components/messages/hive-status.jsx +34 -0
  38. package/src/tui/components/messages/skill-message.jsx +31 -0
  39. package/src/tui/components/messages/system-message.jsx +12 -0
  40. package/src/tui/components/messages/thinking.jsx +25 -0
  41. package/src/tui/components/messages/tool-group.jsx +62 -0
  42. package/src/tui/components/messages/tool-message.jsx +66 -0
  43. package/src/tui/components/messages/user-message.jsx +12 -0
  44. package/src/tui/components/model-picker.jsx +138 -0
  45. package/src/tui/components/multiline-input.jsx +72 -0
  46. package/src/tui/events-handler.js +730 -0
  47. package/src/tui/helpers.js +59 -0
  48. package/src/tui/hooks/use-command-handler.js +451 -0
  49. package/src/tui/index.jsx +55 -0
  50. package/src/tui/input-utils.js +26 -0
  51. package/src/tui/markdown-renderer.js +66 -0
  52. package/src/tui/mcp-wizard.js +136 -0
  53. package/src/tui/model-picker.js +174 -0
  54. package/src/tui/slash-commands.js +26 -0
  55. package/src/tui/store.js +12 -0
  56. package/src/tui/theme.js +17 -0
  57. package/src/ui.js +109 -0
  58. package/bin/moxxy.js +0 -2
  59. package/dist/chunk-23LZYKQ6.mjs +0 -1131
  60. package/dist/chunk-2FZEA3NG.mjs +0 -457
  61. package/dist/chunk-3KDPLS22.mjs +0 -1131
  62. package/dist/chunk-3QRJTRBT.mjs +0 -1102
  63. package/dist/chunk-6DZX6EAA.mjs +0 -37
  64. package/dist/chunk-A4WRDUNY.mjs +0 -1242
  65. package/dist/chunk-C46NSEKG.mjs +0 -211
  66. package/dist/chunk-CAUXONEF.mjs +0 -1131
  67. package/dist/chunk-CPL5V56X.mjs +0 -1131
  68. package/dist/chunk-CTBVTTBG.mjs +0 -440
  69. package/dist/chunk-FHHLXTEZ.mjs +0 -1121
  70. package/dist/chunk-FXY3GPVA.mjs +0 -1126
  71. package/dist/chunk-GSNMMI3H.mjs +0 -530
  72. package/dist/chunk-HHOAOGUS.mjs +0 -1242
  73. package/dist/chunk-ITBO7BKI.mjs +0 -1243
  74. package/dist/chunk-J33O35WX.mjs +0 -532
  75. package/dist/chunk-N5JTPB6U.mjs +0 -820
  76. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  77. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  78. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  79. package/dist/chunk-QO2JONHP.mjs +0 -1131
  80. package/dist/chunk-RVAPILHA.mjs +0 -1242
  81. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  82. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  83. package/dist/chunk-SOFST2PV.mjs +0 -1242
  84. package/dist/chunk-SUNUYS6G.mjs +0 -1243
  85. package/dist/chunk-TMZWETMH.mjs +0 -1242
  86. package/dist/chunk-TYD7NMMI.mjs +0 -581
  87. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  88. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  89. package/dist/chunk-UQZKODNW.mjs +0 -1124
  90. package/dist/chunk-USC6R2ON.mjs +0 -1242
  91. package/dist/chunk-W32EQCVC.mjs +0 -823
  92. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  93. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  94. package/dist/cli-2AIWTL6F.mjs +0 -8
  95. package/dist/cli-2QKJ5UUL.mjs +0 -8
  96. package/dist/cli-4RIS6DQX.mjs +0 -8
  97. package/dist/cli-5RH4VBBL.mjs +0 -7
  98. package/dist/cli-7MK4YGOP.mjs +0 -7
  99. package/dist/cli-B4KH6MZI.mjs +0 -8
  100. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  101. package/dist/cli-CVP26EL2.mjs +0 -8
  102. package/dist/cli-DDRVVNAV.mjs +0 -8
  103. package/dist/cli-E7U56QVQ.mjs +0 -8
  104. package/dist/cli-EQNRMLL3.mjs +0 -8
  105. package/dist/cli-F5RUHHH4.mjs +0 -8
  106. package/dist/cli-LX6FFSEF.mjs +0 -8
  107. package/dist/cli-LY74GWKR.mjs +0 -6
  108. package/dist/cli-MAT3ZJHI.mjs +0 -8
  109. package/dist/cli-NJXXTQYF.mjs +0 -8
  110. package/dist/cli-O4ZGFAZG.mjs +0 -8
  111. package/dist/cli-ORVLI3UQ.mjs +0 -8
  112. package/dist/cli-PV43ZVKA.mjs +0 -8
  113. package/dist/cli-REVD6ISM.mjs +0 -8
  114. package/dist/cli-TBX76KQX.mjs +0 -8
  115. package/dist/cli-THCGF7SQ.mjs +0 -8
  116. package/dist/cli-TLX5ENVM.mjs +0 -8
  117. package/dist/cli-TMNI5ZYE.mjs +0 -8
  118. package/dist/cli-TNJHCBQA.mjs +0 -6
  119. package/dist/cli-TUX22CZP.mjs +0 -8
  120. package/dist/cli-XJVH7EEP.mjs +0 -8
  121. package/dist/cli-XXOW4VXJ.mjs +0 -8
  122. package/dist/cli-XZ5RESNB.mjs +0 -6
  123. package/dist/cli-YCBYZ76Q.mjs +0 -8
  124. package/dist/cli-ZLMQCU7X.mjs +0 -8
  125. package/dist/dist-2VGKJRBH.mjs +0 -6820
  126. package/dist/dist-37BNX4QG.mjs +0 -7081
  127. package/dist/dist-7LTHRYKA.mjs +0 -11569
  128. package/dist/dist-7XJPQW5C.mjs +0 -6950
  129. package/dist/dist-AYMVOW7T.mjs +0 -7123
  130. package/dist/dist-BHUWCDRS.mjs +0 -7132
  131. package/dist/dist-FAXRJMEN.mjs +0 -6812
  132. package/dist/dist-HQGANM3P.mjs +0 -6976
  133. package/dist/dist-KATLOZQV.mjs +0 -7054
  134. package/dist/dist-KLSB6YHV.mjs +0 -6964
  135. package/dist/dist-LKIOZQ42.mjs +0 -17
  136. package/dist/dist-UYA4RJUH.mjs +0 -2792
  137. package/dist/dist-ZYHCBILM.mjs +0 -6993
  138. package/dist/index.d.mts +0 -23
  139. package/dist/index.d.ts +0 -23
  140. package/dist/index.js +0 -25531
  141. package/dist/index.mjs +0 -18
  142. package/dist/src-APP5P3UD.mjs +0 -1386
  143. package/dist/src-D5HMDDVE.mjs +0 -1324
  144. package/dist/src-EK3WD4AU.mjs +0 -1327
  145. package/dist/src-LSZFLMFN.mjs +0 -1400
  146. package/dist/src-T77DFTFP.mjs +0 -1407
  147. package/dist/src-WIOCZRAC.mjs +0 -1397
  148. package/dist/src-YK6CHCMW.mjs +0 -1400
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Skill commands: create/remove/list.
3
+ */
4
+ import { parseFlags } from './auth.js';
5
+ import { isInteractive, handleCancel, withSpinner, showResult, pickAgent, pickSkill, p } from '../ui.js';
6
+
7
+ export async function runSkill(client, args) {
8
+ let [action, ...rest] = args;
9
+ const flags = parseFlags(rest);
10
+
11
+ // Interactive sub-menu when no valid action
12
+ if (!['create', 'remove', 'list'].includes(action) && isInteractive()) {
13
+ action = await p.select({
14
+ message: 'Skill action',
15
+ options: [
16
+ { value: 'create', label: 'Create skill', hint: 'install a skill on an agent' },
17
+ { value: 'remove', label: 'Remove skill', hint: 'remove a skill from an agent' },
18
+ { value: 'list', label: 'List skills', hint: 'list agent skills' },
19
+ ],
20
+ });
21
+ handleCancel(action);
22
+ }
23
+
24
+ switch (action) {
25
+ case 'create': {
26
+ let agentId = flags.agent;
27
+
28
+ // Interactive wizard when missing agent
29
+ if (!agentId && isInteractive()) {
30
+ agentId = await pickAgent(client, 'Select agent for skill');
31
+
32
+ const content = flags.content || handleCancel(await p.text({
33
+ message: 'Skill content (SKILL.md with YAML frontmatter)',
34
+ placeholder: 'Paste skill content...',
35
+ validate: (val) => { if (!val) return 'Content is required'; },
36
+ }));
37
+
38
+ const body = { content };
39
+ const result = await withSpinner('Creating skill...', () =>
40
+ client.request(`/v1/agents/${encodeURIComponent(agentId)}/skills/install`, 'POST', body), 'Skill created.');
41
+
42
+ showResult('Skill Created', {
43
+ Agent: agentId,
44
+ Name: result.name,
45
+ Version: result.version,
46
+ });
47
+
48
+ return result;
49
+ }
50
+
51
+ if (!agentId) throw new Error('Required: --agent');
52
+ const body = {
53
+ content: flags.content || '',
54
+ };
55
+ const result = await client.request(`/v1/agents/${encodeURIComponent(agentId)}/skills/install`, 'POST', body);
56
+ console.log(JSON.stringify(result, null, 2));
57
+ return result;
58
+ }
59
+
60
+ case 'remove': {
61
+ let agentId = flags.agent;
62
+ let skillId = flags.skill;
63
+
64
+ if ((!agentId || !skillId) && isInteractive()) {
65
+ if (!agentId) {
66
+ agentId = await pickAgent(client, 'Select agent');
67
+ }
68
+ if (!skillId) {
69
+ skillId = await pickSkill(client, agentId, 'Select skill to remove');
70
+ }
71
+
72
+ const confirmed = await p.confirm({
73
+ message: 'Remove this skill?',
74
+ initialValue: false,
75
+ });
76
+ handleCancel(confirmed);
77
+ if (!confirmed) {
78
+ p.log.info('Cancelled.');
79
+ return;
80
+ }
81
+ }
82
+
83
+ if (!agentId || !skillId) throw new Error('Required: --agent, --skill');
84
+ const result = await client.deleteSkill(agentId, skillId);
85
+ if (isInteractive()) {
86
+ p.log.success(`Skill ${skillId} removed.`);
87
+ } else {
88
+ console.log(`Skill ${skillId} removed.`);
89
+ }
90
+ return result;
91
+ }
92
+
93
+ case 'list': {
94
+ let agentId = flags.agent;
95
+ if (!agentId && isInteractive()) {
96
+ agentId = await pickAgent(client, 'Select agent to list skills');
97
+ }
98
+ if (!agentId) throw new Error('Required: --agent');
99
+
100
+ const skills = isInteractive()
101
+ ? await withSpinner('Fetching skills...', () =>
102
+ client.listSkills(agentId), 'Skills loaded.')
103
+ : await client.listSkills(agentId);
104
+
105
+ if (isInteractive()) {
106
+ if (Array.isArray(skills) && skills.length > 0) {
107
+ for (const s of skills) {
108
+ p.log.info(`${s.name} v${s.version} (${s.slug || s.name})`);
109
+ }
110
+ } else {
111
+ p.log.warn('No skills found for this agent.');
112
+ }
113
+ } else {
114
+ console.log(JSON.stringify(skills, null, 2));
115
+ }
116
+ return skills;
117
+ }
118
+
119
+ default: {
120
+ const { showHelp } = await import('../help.js');
121
+ showHelp('skill', p);
122
+ break;
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Template commands: list/get/create/update/remove/assign.
3
+ */
4
+ import { parseFlags } from './auth.js';
5
+ import { isInteractive, handleCancel, withSpinner, showResult, pickAgent, p } from '../ui.js';
6
+
7
+ async function pickTemplate(client, message = 'Select template') {
8
+ const templates = await client.listTemplates();
9
+ if (!templates || templates.length === 0) {
10
+ throw new Error('No templates found. Create one first.');
11
+ }
12
+ const options = templates.map(t => ({
13
+ value: t.slug,
14
+ label: t.name,
15
+ hint: t.description,
16
+ }));
17
+ const selected = await p.select({ message, options });
18
+ handleCancel(selected);
19
+ return selected;
20
+ }
21
+
22
+ export async function runTemplate(client, args) {
23
+ let [action, ...rest] = args;
24
+ const flags = parseFlags(rest);
25
+
26
+ // Interactive sub-menu when no valid action
27
+ if (!['list', 'get', 'create', 'update', 'remove', 'assign'].includes(action) && isInteractive()) {
28
+ action = await p.select({
29
+ message: 'Template action',
30
+ options: [
31
+ { value: 'list', label: 'List templates', hint: 'list all templates' },
32
+ { value: 'get', label: 'Get template', hint: 'view template details' },
33
+ { value: 'create', label: 'Create template', hint: 'create a new template' },
34
+ { value: 'update', label: 'Update template', hint: 'update an existing template' },
35
+ { value: 'remove', label: 'Remove template', hint: 'delete a template' },
36
+ { value: 'assign', label: 'Assign template', hint: 'assign a template to an agent' },
37
+ ],
38
+ });
39
+ handleCancel(action);
40
+ }
41
+
42
+ switch (action) {
43
+ case 'list': {
44
+ const templates = isInteractive()
45
+ ? await withSpinner('Fetching templates...', () =>
46
+ client.listTemplates(), 'Templates loaded.')
47
+ : await client.listTemplates();
48
+
49
+ if (isInteractive()) {
50
+ if (Array.isArray(templates) && templates.length > 0) {
51
+ for (const t of templates) {
52
+ const tags = t.tags && t.tags.length > 0 ? ` [${t.tags.join(', ')}]` : '';
53
+ p.log.info(`${t.name} v${t.version} (${t.slug})${tags}`);
54
+ }
55
+ } else {
56
+ p.log.warn('No templates found.');
57
+ }
58
+ } else {
59
+ console.log(JSON.stringify(templates, null, 2));
60
+ }
61
+ return templates;
62
+ }
63
+
64
+ case 'get': {
65
+ let slug = flags.slug || rest[0];
66
+ if (!slug && isInteractive()) {
67
+ slug = await pickTemplate(client, 'Select template to view');
68
+ }
69
+ if (!slug) throw new Error('Required: --slug or positional argument');
70
+
71
+ const template = isInteractive()
72
+ ? await withSpinner('Fetching template...', () =>
73
+ client.getTemplate(slug), 'Template loaded.')
74
+ : await client.getTemplate(slug);
75
+
76
+ if (isInteractive()) {
77
+ showResult('Template', {
78
+ Name: template.name,
79
+ Slug: template.slug,
80
+ Version: template.version,
81
+ Description: template.description,
82
+ Tags: (template.tags || []).join(', ') || '(none)',
83
+ });
84
+ if (template.body) {
85
+ p.log.info(`\nContent:\n${template.body}`);
86
+ }
87
+ } else {
88
+ console.log(JSON.stringify(template, null, 2));
89
+ }
90
+ return template;
91
+ }
92
+
93
+ case 'create': {
94
+ let content = flags.content;
95
+
96
+ if (!content && isInteractive()) {
97
+ content = handleCancel(await p.text({
98
+ message: 'Template content (TEMPLATE.md with YAML frontmatter)',
99
+ placeholder: 'Paste template content...',
100
+ validate: (val) => { if (!val) return 'Content is required'; },
101
+ }));
102
+ }
103
+ if (!content) throw new Error('Required: --content');
104
+
105
+ const result = isInteractive()
106
+ ? await withSpinner('Creating template...', () =>
107
+ client.createTemplate(content), 'Template created.')
108
+ : await client.createTemplate(content);
109
+
110
+ if (isInteractive()) {
111
+ showResult('Template Created', {
112
+ Name: result.name,
113
+ Slug: result.slug,
114
+ Version: result.version,
115
+ });
116
+ } else {
117
+ console.log(JSON.stringify(result, null, 2));
118
+ }
119
+ return result;
120
+ }
121
+
122
+ case 'update': {
123
+ let slug = flags.slug;
124
+ let content = flags.content;
125
+
126
+ if ((!slug || !content) && isInteractive()) {
127
+ if (!slug) {
128
+ slug = await pickTemplate(client, 'Select template to update');
129
+ }
130
+ if (!content) {
131
+ content = handleCancel(await p.text({
132
+ message: 'New template content',
133
+ placeholder: 'Paste updated content...',
134
+ validate: (val) => { if (!val) return 'Content is required'; },
135
+ }));
136
+ }
137
+ }
138
+ if (!slug || !content) throw new Error('Required: --slug, --content');
139
+
140
+ const result = isInteractive()
141
+ ? await withSpinner('Updating template...', () =>
142
+ client.updateTemplate(slug, content), 'Template updated.')
143
+ : await client.updateTemplate(slug, content);
144
+
145
+ if (isInteractive()) {
146
+ showResult('Template Updated', {
147
+ Slug: slug,
148
+ Name: result.name,
149
+ Version: result.version,
150
+ });
151
+ } else {
152
+ console.log(JSON.stringify(result, null, 2));
153
+ }
154
+ return result;
155
+ }
156
+
157
+ case 'remove': {
158
+ let slug = flags.slug || rest[0];
159
+
160
+ if (!slug && isInteractive()) {
161
+ slug = await pickTemplate(client, 'Select template to remove');
162
+ const confirmed = await p.confirm({
163
+ message: `Remove template "${slug}"?`,
164
+ initialValue: false,
165
+ });
166
+ handleCancel(confirmed);
167
+ if (!confirmed) {
168
+ p.log.info('Cancelled.');
169
+ return;
170
+ }
171
+ }
172
+ if (!slug) throw new Error('Required: --slug or positional argument');
173
+
174
+ await (isInteractive()
175
+ ? withSpinner('Removing template...', () =>
176
+ client.deleteTemplate(slug), 'Template removed.')
177
+ : client.deleteTemplate(slug));
178
+
179
+ if (isInteractive()) {
180
+ p.log.success(`Template ${slug} removed.`);
181
+ } else {
182
+ console.log(`Template ${slug} removed.`);
183
+ }
184
+ return;
185
+ }
186
+
187
+ case 'assign': {
188
+ let agentId = flags.agent;
189
+ let slug = flags.slug || flags.template;
190
+
191
+ if ((!agentId || slug === undefined) && isInteractive()) {
192
+ if (!agentId) {
193
+ agentId = await pickAgent(client, 'Select agent');
194
+ }
195
+
196
+ const clearOrAssign = await p.select({
197
+ message: 'Action',
198
+ options: [
199
+ { value: 'assign', label: 'Assign template', hint: 'assign a template to this agent' },
200
+ { value: 'clear', label: 'Clear template', hint: 'remove template assignment' },
201
+ ],
202
+ });
203
+ handleCancel(clearOrAssign);
204
+
205
+ if (clearOrAssign === 'clear') {
206
+ slug = null;
207
+ } else {
208
+ slug = await pickTemplate(client, 'Select template to assign');
209
+ }
210
+ }
211
+
212
+ if (!agentId) throw new Error('Required: --agent');
213
+
214
+ const result = isInteractive()
215
+ ? await withSpinner('Updating agent template...', () =>
216
+ client.setAgentTemplate(agentId, slug), 'Agent template updated.')
217
+ : await client.setAgentTemplate(agentId, slug);
218
+
219
+ if (isInteractive()) {
220
+ if (slug) {
221
+ p.log.success(`Template "${slug}" assigned to agent "${agentId}".`);
222
+ } else {
223
+ p.log.success(`Template cleared from agent "${agentId}".`);
224
+ }
225
+ } else {
226
+ console.log(JSON.stringify(result, null, 2));
227
+ }
228
+ return result;
229
+ }
230
+
231
+ default: {
232
+ const { showHelp } = await import('../help.js');
233
+ showHelp('template', p);
234
+ break;
235
+ }
236
+ }
237
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Uninstall command: remove all Moxxy data from the system.
3
+ * Removes ~/.moxxy (database, agents, config) but does NOT remove the CLI package.
4
+ */
5
+ import { p, handleCancel } from '../ui.js';
6
+ import { LOGO } from '../cli.js';
7
+ import { getMoxxyHome } from './init.js';
8
+ import { shellUnsetInstruction, shellProfileName } from '../platform.js';
9
+ import { existsSync, rmSync, readdirSync, statSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { execSync } from 'node:child_process';
12
+ import { platform } from 'node:os';
13
+
14
+ export async function runUninstall(client, args) {
15
+ console.log(LOGO);
16
+ p.intro('Uninstall Moxxy');
17
+
18
+ const moxxyHome = getMoxxyHome();
19
+
20
+ // ── 1. Show what will be removed ──
21
+
22
+ const items = [];
23
+
24
+ if (existsSync(moxxyHome)) {
25
+ const size = getDirSize(moxxyHome);
26
+ items.push(`${moxxyHome} (${formatBytes(size)})`);
27
+
28
+ // List contents for visibility
29
+ if (existsSync(join(moxxyHome, 'moxxy.db'))) {
30
+ items.push(' - Database (moxxy.db)');
31
+ }
32
+ if (existsSync(join(moxxyHome, 'agents'))) {
33
+ const agents = readdirSync(join(moxxyHome, 'agents'));
34
+ items.push(` - ${agents.length} agent workspace(s)`);
35
+ }
36
+ if (existsSync(join(moxxyHome, 'config'))) {
37
+ items.push(' - Configuration files');
38
+ }
39
+ } else {
40
+ p.log.info(`Moxxy home not found at ${moxxyHome}. Nothing to remove.`);
41
+ }
42
+
43
+ // Check for running gateway
44
+ let gatewayRunning = false;
45
+ try {
46
+ const resp = await fetch(`${client.baseUrl}/v1/providers`);
47
+ if (resp) gatewayRunning = true;
48
+ } catch {
49
+ // Not running
50
+ }
51
+
52
+ if (gatewayRunning) {
53
+ p.log.warn('Gateway is currently running. It should be stopped first.');
54
+ }
55
+
56
+ if (items.length === 0 && !gatewayRunning) {
57
+ p.outro('Nothing to uninstall.');
58
+ return;
59
+ }
60
+
61
+ // ── 2. Show removal summary ──
62
+
63
+ if (items.length > 0) {
64
+ p.note(items.join('\n'), 'The following will be permanently deleted');
65
+ }
66
+
67
+ // ── 3. Confirm ──
68
+
69
+ const confirmed = await p.confirm({
70
+ message: 'Are you sure you want to remove all Moxxy data? This cannot be undone.',
71
+ initialValue: false,
72
+ });
73
+ handleCancel(confirmed);
74
+
75
+ if (!confirmed) {
76
+ p.outro('Uninstall cancelled.');
77
+ return;
78
+ }
79
+
80
+ // Double confirmation for safety
81
+ const reallyConfirmed = await p.confirm({
82
+ message: 'Really delete everything? Type Yes to confirm.',
83
+ initialValue: false,
84
+ });
85
+ handleCancel(reallyConfirmed);
86
+
87
+ if (!reallyConfirmed) {
88
+ p.outro('Uninstall cancelled.');
89
+ return;
90
+ }
91
+
92
+ // ── 4. Stop gateway if running ──
93
+
94
+ if (gatewayRunning) {
95
+ p.log.step('Stopping gateway...');
96
+ try {
97
+ if (platform() === 'win32') {
98
+ // Windows: use netstat + taskkill
99
+ const out = execSync('netstat -ano | findstr :3000', { encoding: 'utf-8', stdio: 'pipe' }).trim();
100
+ const pids = new Set();
101
+ for (const line of out.split('\n').filter(Boolean)) {
102
+ const parts = line.trim().split(/\s+/);
103
+ const pid = parts[parts.length - 1];
104
+ if (pid && pid !== '0') pids.add(pid);
105
+ }
106
+ for (const pid of pids) {
107
+ try { execSync(`taskkill /PID ${pid} /F`, { stdio: 'pipe' }); } catch { /* already dead */ }
108
+ }
109
+ if (pids.size > 0) p.log.success('Gateway stopped.');
110
+ } else {
111
+ // Unix: use lsof
112
+ const pids = execSync("lsof -ti:3000 2>/dev/null || true", { encoding: 'utf-8' }).trim();
113
+ if (pids) {
114
+ for (const pid of pids.split('\n').filter(Boolean)) {
115
+ try { process.kill(parseInt(pid), 'SIGTERM'); } catch { /* already dead */ }
116
+ }
117
+ p.log.success('Gateway stopped.');
118
+ }
119
+ }
120
+ } catch {
121
+ p.log.warn('Could not stop gateway automatically. Please stop it manually.');
122
+ }
123
+ }
124
+
125
+ // ── 4b. Stop running plugins ──
126
+
127
+ try {
128
+ const { pluginPaths, readRegistry, isProcessAlive } = await import('../lib/plugin-registry.js');
129
+ const { registryFile } = pluginPaths();
130
+ if (existsSync(registryFile)) {
131
+ const registry = readRegistry();
132
+ for (const plug of Object.values(registry.plugins)) {
133
+ if (plug.pid && isProcessAlive(plug.pid)) {
134
+ try {
135
+ process.kill(plug.pid, 'SIGTERM');
136
+ p.log.info(`Stopped plugin: ${plug.name} (PID ${plug.pid})`);
137
+ } catch { /* already dead */ }
138
+ }
139
+ }
140
+ }
141
+ } catch { /* no plugins */ }
142
+
143
+ // ── 5. Remove ~/.moxxy ──
144
+
145
+ if (existsSync(moxxyHome)) {
146
+ try {
147
+ rmSync(moxxyHome, { recursive: true, force: true });
148
+ p.log.success(`Removed ${moxxyHome}`);
149
+ } catch (err) {
150
+ p.log.error(`Failed to remove ${moxxyHome}: ${err.message}`);
151
+ process.exitCode = 1;
152
+ return;
153
+ }
154
+ }
155
+
156
+ // ── 6. Clean environment reminder ──
157
+
158
+ const envVars = ['MOXXY_TOKEN', 'MOXXY_API_URL', 'MOXXY_HOME'].filter(v => process.env[v]);
159
+
160
+ const instructions = [];
161
+ if (envVars.length > 0) {
162
+ instructions.push(`Remove these from ${shellProfileName()}:`);
163
+ for (const v of envVars) {
164
+ instructions.push(` ${shellUnsetInstruction(v)}`);
165
+ }
166
+ }
167
+ instructions.push('');
168
+ instructions.push('To remove the CLI itself:');
169
+ instructions.push(' npm remove -g moxxy-cli');
170
+
171
+ p.note(instructions.join('\n'), 'Manual cleanup');
172
+
173
+ p.outro('Moxxy has been uninstalled. Goodbye!');
174
+ }
175
+
176
+ function getDirSize(dirPath) {
177
+ let size = 0;
178
+ try {
179
+ const entries = readdirSync(dirPath, { withFileTypes: true });
180
+ for (const entry of entries) {
181
+ const fullPath = join(dirPath, entry.name);
182
+ if (entry.isDirectory()) {
183
+ size += getDirSize(fullPath);
184
+ } else {
185
+ try { size += statSync(fullPath).size; } catch { /* skip */ }
186
+ }
187
+ }
188
+ } catch { /* skip */ }
189
+ return size;
190
+ }
191
+
192
+ function formatBytes(bytes) {
193
+ if (bytes < 1024) return `${bytes} B`;
194
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
195
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
196
+ }