codeply-cli 0.2.0 → 0.3.1
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/THIRD_PARTY_NOTICES.md +63 -0
- package/bin/codeply.js +558 -370
- package/lib/agent.mjs +1627 -160
- package/lib/ai.js +1107 -387
- package/lib/apply-limit.js +33 -51
- package/lib/apply-patch.js +217 -0
- package/lib/arity-table.json +138 -0
- package/lib/arity.js +70 -0
- package/lib/auth.js +118 -112
- package/lib/commands.js +103 -0
- package/lib/config.js +392 -136
- package/lib/design-library/artifact-template.html +500 -0
- package/lib/design-library/assemble-artifact.mjs +21 -0
- package/lib/design-library/build.mjs +226 -0
- package/lib/design-library/cleanup.mjs +45 -0
- package/lib/design-library/index.json +22380 -0
- package/lib/design-library/make-table-data.mjs +39 -0
- package/lib/design-library/query.mjs +104 -0
- package/lib/diagnostics.js +192 -0
- package/lib/edit-engine.js +171 -14
- package/lib/github-agent.mjs +307 -0
- package/lib/history.js +75 -0
- package/lib/mcp.js +258 -0
- package/lib/native-tools.mjs +189 -0
- package/lib/oauth-connectors.js +478 -0
- package/lib/permissions.js +109 -0
- package/lib/plugins.js +353 -0
- package/lib/render-md.mjs +617 -617
- package/lib/server.mjs +457 -0
- package/lib/session-db.js +220 -0
- package/lib/share.js +168 -0
- package/lib/skills.js +80 -22
- package/lib/snapshot.js +146 -0
- package/lib/subagents.js +164 -0
- package/lib/terminal-hints.js +154 -0
- package/lib/textarea.mjs +258 -258
- package/lib/tools.mjs +1658 -79
- package/lib/tui.js +7 -7
- package/lib/tui.mjs +1086 -992
- package/lib/typecheck-worker.js +147 -0
- package/lib/web-tools.js +158 -0
- package/package.json +4 -3
- package/skills/premium-web-design/SKILL.md +154 -0
package/bin/codeply.js
CHANGED
|
@@ -1,370 +1,558 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Codeply CLI
|
|
4
|
-
*
|
|
5
|
-
* Single-shot by design (v1): one instruction, one file, one verified diff,
|
|
6
|
-
* one write. No multi-file auto-detection, no conversation memory, no local
|
|
7
|
-
* response cache
|
|
8
|
-
* this is worth building out further.
|
|
9
|
-
*/
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const readline = require('readline/promises');
|
|
13
|
-
const { Command } = require('commander');
|
|
14
|
-
|
|
15
|
-
const auth = require('../lib/auth');
|
|
16
|
-
const ai = require('../lib/ai');
|
|
17
|
-
const editEngine = require('../lib/edit-engine');
|
|
18
|
-
const applyLimit = require('../lib/apply-limit');
|
|
19
|
-
const config = require('../lib/config');
|
|
20
|
-
|
|
21
|
-
// Google brand palette
|
|
22
|
-
const c = {
|
|
23
|
-
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
24
|
-
red: (s) => `\x1b[38;2;234;67;53m${s}\x1b[0m`, // #EA4335
|
|
25
|
-
green: (s) => `\x1b[38;2;52;168;83m${s}\x1b[0m`, // #34A853
|
|
26
|
-
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
27
|
-
blue: (s) => `\x1b[38;2;66;133;244m${s}\x1b[0m`, // #4285F4
|
|
28
|
-
yellow: (s) => `\x1b[38;2;251;188;5m${s}\x1b[0m`, // #FBBC05
|
|
29
|
-
accent: (s) => `\x1b[38;2;66;133;244m${s}\x1b[0m`, // primary = blue
|
|
30
|
-
secondary: (s) => `\x1b[38;2;154;160;166m${s}\x1b[0m`,
|
|
31
|
-
muted: (s) => `\x1b[38;2;95;99;104m${s}\x1b[0m`,
|
|
32
|
-
text: (s) => `\x1b[38;2;232;234;237m${s}\x1b[0m`,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
async function confirm(question) {
|
|
36
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
37
|
-
try {
|
|
38
|
-
const answer = (await rl.question(`${question} (y/N) `)).trim().toLowerCase();
|
|
39
|
-
return answer === 'y' || answer === 'yes';
|
|
40
|
-
} finally {
|
|
41
|
-
rl.close();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function printEditPreview(edits) {
|
|
46
|
-
edits.forEach((e, i) => {
|
|
47
|
-
console.log(c.muted(`\n── edit ${i + 1}/${edits.length} ──`));
|
|
48
|
-
e.search.split('\n').forEach(l => console.log(c.red(`- ${l}`)));
|
|
49
|
-
e.replace.split('\n').forEach(l => console.log(c.green(`+ ${l}`)));
|
|
50
|
-
});
|
|
51
|
-
console.log('');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function runApply(instruction, opts) {
|
|
55
|
-
const session = await auth.getSession();
|
|
56
|
-
if (!session) {
|
|
57
|
-
console.error('Not signed in. Run `codeply login` first.');
|
|
58
|
-
process.exitCode = 1;
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const filePath = path.resolve(process.cwd(), opts.file);
|
|
63
|
-
if (!fs.existsSync(filePath)) {
|
|
64
|
-
console.error(`File not found: ${filePath}`);
|
|
65
|
-
process.exitCode = 1;
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// The shared 100/day cap only applies when spending the Codeply proxy
|
|
70
|
-
// Ollama (local or the user's own cloud key) doesn't touch that account.
|
|
71
|
-
const usingCodeply = config.getConfig().provider === 'codeply';
|
|
72
|
-
if (usingCodeply) {
|
|
73
|
-
const limit = await applyLimit.checkApplyLimit();
|
|
74
|
-
if (!limit.allowed) {
|
|
75
|
-
console.error(limit.error || `Daily apply limit reached (${limit.count}/${limit.limit}). This is shared with the Codeply desktop app and resets at midnight UTC.`);
|
|
76
|
-
console.error(c.muted(config.byokHint()));
|
|
77
|
-
process.exitCode = 1;
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
console.log(c.secondary(`Asking Codeply about ${path.basename(filePath)}…`));
|
|
83
|
-
const result = await editEngine.computeInstructionEdits(instruction, filePath);
|
|
84
|
-
|
|
85
|
-
if (!result.success) {
|
|
86
|
-
console.error(c.red(`Failed: ${result.error}`));
|
|
87
|
-
process.exitCode = 1;
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
if (!result.edits.length) {
|
|
91
|
-
console.log(result.reason || 'No edits produced
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
printEditPreview(result.edits);
|
|
96
|
-
if (result.badCount) {
|
|
97
|
-
console.log(c.muted(`Note: ${result.badCount} proposed change(s) couldn't be matched against the file and were skipped.`));
|
|
98
|
-
}
|
|
99
|
-
console.log(c.muted(`${result.reason || ''}${result.reason ? '
|
|
100
|
-
|
|
101
|
-
if (opts.dryRun) {
|
|
102
|
-
console.log(c.muted('\n(dry run
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (!opts.yes) {
|
|
107
|
-
const ok = await confirm(c.bold(c.accent(`Apply ${result.edits.length} edit(s) to ${path.basename(filePath)}?`)));
|
|
108
|
-
if (!ok) { console.log(c.secondary('Cancelled.')); return; }
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
112
|
-
const applied = editEngine.applyEditsToContent(fileContent, result.edits);
|
|
113
|
-
if (!applied.ok) {
|
|
114
|
-
console.error(`Could not apply edit ${applied.index + 1}: ${applied.error}`);
|
|
115
|
-
process.exitCode = 1;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
fs.writeFileSync(filePath, applied.content);
|
|
120
|
-
if (usingCodeply) {
|
|
121
|
-
const linesAdded = result.edits.reduce((n, e) => n + e.replace.split('\n').length, 0);
|
|
122
|
-
const linesRemoved = result.edits.reduce((n, e) => n + e.search.split('\n').length, 0);
|
|
123
|
-
await applyLimit.recordApplyEvent(filePath, linesAdded, linesRemoved);
|
|
124
|
-
}
|
|
125
|
-
console.log(c.green(`✓ Applied to ${filePath}`));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// TUI Mode - interactive when no command is provided
|
|
129
|
-
async function runTUI() {
|
|
130
|
-
// Check if running in interactive terminal
|
|
131
|
-
if (!process.stdin.isTTY) {
|
|
132
|
-
const VERSION = require('../package.json').version;
|
|
133
|
-
// C=blue O=red D=yellow E=green P=blue L=red Y=yellow
|
|
134
|
-
// Fixed-width letters + trailing gap so CODEPLY reads cleanly
|
|
135
|
-
const letterColors = [c.blue, c.red, c.yellow, c.green, c.blue, c.red, c.yellow];
|
|
136
|
-
const logoLines = [
|
|
137
|
-
[' ██████╗ ', ' ██████╗ ', '██████╗ ', '███████╗ ', '██████╗ ', '██╗ ', '██╗ ██╗'],
|
|
138
|
-
['██╔════╝ ', '██╔═══██╗', '██╔══██╗ ', '██╔════╝ ', '██╔══██╗ ', '██║ ', '╚██╗ ██╔╝'],
|
|
139
|
-
['██║ ', '██║ ██║', '██║ ██║ ', '█████╗ ', '██████╔╝ ', '██║ ', ' ╚████╔╝ '],
|
|
140
|
-
['██║ ', '██║ ██║', '██║ ██║ ', '██╔══╝ ', '██╔═══╝ ', '██║ ', ' ╚██╔╝ '],
|
|
141
|
-
['╚██████╗ ', '╚██████╔╝', '██████╔╝ ', '███████╗ ', '██║ ', '███████╗ ', ' ██║ '],
|
|
142
|
-
[' ╚═════╝ ', ' ╚═════╝ ', '╚═════╝ ', '╚══════╝ ', '╚═╝ ', '╚══════╝ ', ' ╚═╝ '],
|
|
143
|
-
];
|
|
144
|
-
|
|
145
|
-
console.log('');
|
|
146
|
-
logoLines.forEach((line) => {
|
|
147
|
-
console.log(line.map((seg, i) => letterColors[i](seg)).join(''));
|
|
148
|
-
});
|
|
149
|
-
console.log(`
|
|
150
|
-
${c.secondary('Apply AI instructions. Edit files. Ship faster.')} ${c.muted('v' + VERSION)}
|
|
151
|
-
|
|
152
|
-
${c.bold(c.text('Usage'))}
|
|
153
|
-
${c.blue('codeply')} Launch interactive TUI
|
|
154
|
-
${c.blue('codeply apply')} ${c.dim('<instruction>')} ${c.blue('-f')} ${c.dim('<file>')}
|
|
155
|
-
${c.red('codeply login')} Sign in with your Codeply account
|
|
156
|
-
${c.yellow('codeply logout')} Sign out
|
|
157
|
-
${c.green('codeply whoami')} Show current account
|
|
158
|
-
|
|
159
|
-
${c.muted('Run in a terminal for the full interactive UI · codeply --help')}
|
|
160
|
-
`);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const React = await import('react');
|
|
165
|
-
const { render } = await import('ink');
|
|
166
|
-
const { default: App } = await import('../lib/tui.mjs');
|
|
167
|
-
|
|
168
|
-
// Take the screen so the app doesn't look like output that happened to land
|
|
169
|
-
// in a shell. Deliberately a plain clear rather than the alternate screen
|
|
170
|
-
// buffer (\x1b[?1049h): alt-screen would discard scrollback on exit, and the
|
|
171
|
-
// whole point of the <Static> transcript is that you can scroll back through
|
|
172
|
-
// it afterwards.
|
|
173
|
-
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
174
|
-
|
|
175
|
-
render(React.default.createElement(App), {
|
|
176
|
-
exitOnCtrlC: false, // handled inside the app (clear draft first, then quit)
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const program = new Command();
|
|
181
|
-
program
|
|
182
|
-
.name('codeply')
|
|
183
|
-
.description('Apply an AI instruction to a file from the terminal.')
|
|
184
|
-
.version(require('../package.json').version);
|
|
185
|
-
|
|
186
|
-
program
|
|
187
|
-
.command('login')
|
|
188
|
-
.description('Sign in with your Codeply account (email +
|
|
189
|
-
.action(async () => { await auth.login(); });
|
|
190
|
-
|
|
191
|
-
program
|
|
192
|
-
.command('logout')
|
|
193
|
-
.description('Sign out of the current Codeply session.')
|
|
194
|
-
.action(async () => { await auth.logout(); });
|
|
195
|
-
|
|
196
|
-
program
|
|
197
|
-
.command('whoami')
|
|
198
|
-
.description('Show the currently signed-in account.')
|
|
199
|
-
.action(async () => {
|
|
200
|
-
const session = await auth.getSession();
|
|
201
|
-
console.log(session ? session.user.email : 'Not signed in.');
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
program
|
|
205
|
-
.command('provider [name]')
|
|
206
|
-
.description('Show or set the model backend (codeply | ollama | openrouter | groq | anthropic | openai).')
|
|
207
|
-
.option('--key <key>', 'API key for ollama (cloud only), openrouter, groq, anthropic, or
|
|
208
|
-
.option('--model <model>', 'model name for the selected provider')
|
|
209
|
-
.option('--host <url>', 'ollama host only (default https://ollama.com; local: http://localhost:11434)')
|
|
210
|
-
.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
console.log(`
|
|
219
|
-
console.log(`
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
console.log(`
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
console.log(c.muted(
|
|
226
|
-
console.log(c.muted('
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (opts.
|
|
247
|
-
if (opts.
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (opts.
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
.
|
|
271
|
-
.
|
|
272
|
-
.
|
|
273
|
-
.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
.
|
|
283
|
-
.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
.
|
|
307
|
-
.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
console.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Codeply CLI - apply an AI instruction to a file from the terminal.
|
|
4
|
+
*
|
|
5
|
+
* Single-shot by design (v1): one instruction, one file, one verified diff,
|
|
6
|
+
* one write. No multi-file auto-detection, no conversation memory, no local
|
|
7
|
+
* response cache - those are desktop-app features that can come later if
|
|
8
|
+
* this is worth building out further.
|
|
9
|
+
*/
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const readline = require('readline/promises');
|
|
13
|
+
const { Command } = require('commander');
|
|
14
|
+
|
|
15
|
+
const auth = require('../lib/auth');
|
|
16
|
+
const ai = require('../lib/ai');
|
|
17
|
+
const editEngine = require('../lib/edit-engine');
|
|
18
|
+
const applyLimit = require('../lib/apply-limit');
|
|
19
|
+
const config = require('../lib/config');
|
|
20
|
+
|
|
21
|
+
// Google brand palette
|
|
22
|
+
const c = {
|
|
23
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
24
|
+
red: (s) => `\x1b[38;2;234;67;53m${s}\x1b[0m`, // #EA4335
|
|
25
|
+
green: (s) => `\x1b[38;2;52;168;83m${s}\x1b[0m`, // #34A853
|
|
26
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
27
|
+
blue: (s) => `\x1b[38;2;66;133;244m${s}\x1b[0m`, // #4285F4
|
|
28
|
+
yellow: (s) => `\x1b[38;2;251;188;5m${s}\x1b[0m`, // #FBBC05
|
|
29
|
+
accent: (s) => `\x1b[38;2;66;133;244m${s}\x1b[0m`, // primary = blue
|
|
30
|
+
secondary: (s) => `\x1b[38;2;154;160;166m${s}\x1b[0m`,
|
|
31
|
+
muted: (s) => `\x1b[38;2;95;99;104m${s}\x1b[0m`,
|
|
32
|
+
text: (s) => `\x1b[38;2;232;234;237m${s}\x1b[0m`,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
async function confirm(question) {
|
|
36
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
37
|
+
try {
|
|
38
|
+
const answer = (await rl.question(`${question} (y/N) `)).trim().toLowerCase();
|
|
39
|
+
return answer === 'y' || answer === 'yes';
|
|
40
|
+
} finally {
|
|
41
|
+
rl.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printEditPreview(edits) {
|
|
46
|
+
edits.forEach((e, i) => {
|
|
47
|
+
console.log(c.muted(`\n── edit ${i + 1}/${edits.length} ──`));
|
|
48
|
+
e.search.split('\n').forEach(l => console.log(c.red(`- ${l}`)));
|
|
49
|
+
e.replace.split('\n').forEach(l => console.log(c.green(`+ ${l}`)));
|
|
50
|
+
});
|
|
51
|
+
console.log('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function runApply(instruction, opts) {
|
|
55
|
+
const session = await auth.getSession();
|
|
56
|
+
if (!session) {
|
|
57
|
+
console.error('Not signed in. Run `codeply login` first.');
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const filePath = path.resolve(process.cwd(), opts.file);
|
|
63
|
+
if (!fs.existsSync(filePath)) {
|
|
64
|
+
console.error(`File not found: ${filePath}`);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The shared 100/day cap only applies when spending the Codeply proxy -
|
|
70
|
+
// Ollama (local or the user's own cloud key) doesn't touch that account.
|
|
71
|
+
const usingCodeply = config.getConfig().provider === 'codeply';
|
|
72
|
+
if (usingCodeply) {
|
|
73
|
+
const limit = await applyLimit.checkApplyLimit();
|
|
74
|
+
if (!limit.allowed) {
|
|
75
|
+
console.error(limit.error || `Daily apply limit reached (${limit.count}/${limit.limit}). This is shared with the Codeply desktop app and resets at midnight UTC.`);
|
|
76
|
+
console.error(c.muted(config.byokHint()));
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(c.secondary(`Asking Codeply about ${path.basename(filePath)}…`));
|
|
83
|
+
const result = await editEngine.computeInstructionEdits(instruction, filePath);
|
|
84
|
+
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
console.error(c.red(`Failed: ${result.error}`));
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!result.edits.length) {
|
|
91
|
+
console.log(result.reason || 'No edits produced - the instruction may be too ambiguous or too large for a targeted change.');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
printEditPreview(result.edits);
|
|
96
|
+
if (result.badCount) {
|
|
97
|
+
console.log(c.muted(`Note: ${result.badCount} proposed change(s) couldn't be matched against the file and were skipped.`));
|
|
98
|
+
}
|
|
99
|
+
console.log(c.muted(`${result.reason || ''}${result.reason ? ' - ' : ''}confidence ${result.confidence ?? '?'}%, ${result.tokensUsed || 0} tokens, model ${result.modelUsed || 'unknown'}`));
|
|
100
|
+
|
|
101
|
+
if (opts.dryRun) {
|
|
102
|
+
console.log(c.muted('\n(dry run - nothing written)'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!opts.yes) {
|
|
107
|
+
const ok = await confirm(c.bold(c.accent(`Apply ${result.edits.length} edit(s) to ${path.basename(filePath)}?`)));
|
|
108
|
+
if (!ok) { console.log(c.secondary('Cancelled.')); return; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
112
|
+
const applied = editEngine.applyEditsToContent(fileContent, result.edits);
|
|
113
|
+
if (!applied.ok) {
|
|
114
|
+
console.error(`Could not apply edit ${applied.index + 1}: ${applied.error}`);
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fs.writeFileSync(filePath, applied.content);
|
|
120
|
+
if (usingCodeply) {
|
|
121
|
+
const linesAdded = result.edits.reduce((n, e) => n + e.replace.split('\n').length, 0);
|
|
122
|
+
const linesRemoved = result.edits.reduce((n, e) => n + e.search.split('\n').length, 0);
|
|
123
|
+
await applyLimit.recordApplyEvent(filePath, linesAdded, linesRemoved);
|
|
124
|
+
}
|
|
125
|
+
console.log(c.green(`✓ Applied to ${filePath}`));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// TUI Mode - interactive when no command is provided
|
|
129
|
+
async function runTUI() {
|
|
130
|
+
// Check if running in interactive terminal
|
|
131
|
+
if (!process.stdin.isTTY) {
|
|
132
|
+
const VERSION = require('../package.json').version;
|
|
133
|
+
// C=blue O=red D=yellow E=green P=blue L=red Y=yellow
|
|
134
|
+
// Fixed-width letters + trailing gap so CODEPLY reads cleanly
|
|
135
|
+
const letterColors = [c.blue, c.red, c.yellow, c.green, c.blue, c.red, c.yellow];
|
|
136
|
+
const logoLines = [
|
|
137
|
+
[' ██████╗ ', ' ██████╗ ', '██████╗ ', '███████╗ ', '██████╗ ', '██╗ ', '██╗ ██╗'],
|
|
138
|
+
['██╔════╝ ', '██╔═══██╗', '██╔══██╗ ', '██╔════╝ ', '██╔══██╗ ', '██║ ', '╚██╗ ██╔╝'],
|
|
139
|
+
['██║ ', '██║ ██║', '██║ ██║ ', '█████╗ ', '██████╔╝ ', '██║ ', ' ╚████╔╝ '],
|
|
140
|
+
['██║ ', '██║ ██║', '██║ ██║ ', '██╔══╝ ', '██╔═══╝ ', '██║ ', ' ╚██╔╝ '],
|
|
141
|
+
['╚██████╗ ', '╚██████╔╝', '██████╔╝ ', '███████╗ ', '██║ ', '███████╗ ', ' ██║ '],
|
|
142
|
+
[' ╚═════╝ ', ' ╚═════╝ ', '╚═════╝ ', '╚══════╝ ', '╚═╝ ', '╚══════╝ ', ' ╚═╝ '],
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
console.log('');
|
|
146
|
+
logoLines.forEach((line) => {
|
|
147
|
+
console.log(line.map((seg, i) => letterColors[i](seg)).join(''));
|
|
148
|
+
});
|
|
149
|
+
console.log(`
|
|
150
|
+
${c.secondary('Apply AI instructions. Edit files. Ship faster.')} ${c.muted('v' + VERSION)}
|
|
151
|
+
|
|
152
|
+
${c.bold(c.text('Usage'))}
|
|
153
|
+
${c.blue('codeply')} Launch interactive TUI
|
|
154
|
+
${c.blue('codeply apply')} ${c.dim('<instruction>')} ${c.blue('-f')} ${c.dim('<file>')}
|
|
155
|
+
${c.red('codeply login')} Sign in with your Codeply account
|
|
156
|
+
${c.yellow('codeply logout')} Sign out
|
|
157
|
+
${c.green('codeply whoami')} Show current account
|
|
158
|
+
|
|
159
|
+
${c.muted('Run in a terminal for the full interactive UI · codeply --help')}
|
|
160
|
+
`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const React = await import('react');
|
|
165
|
+
const { render } = await import('ink');
|
|
166
|
+
const { default: App } = await import('../lib/tui.mjs');
|
|
167
|
+
|
|
168
|
+
// Take the screen so the app doesn't look like output that happened to land
|
|
169
|
+
// in a shell. Deliberately a plain clear rather than the alternate screen
|
|
170
|
+
// buffer (\x1b[?1049h): alt-screen would discard scrollback on exit, and the
|
|
171
|
+
// whole point of the <Static> transcript is that you can scroll back through
|
|
172
|
+
// it afterwards.
|
|
173
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
174
|
+
|
|
175
|
+
render(React.default.createElement(App), {
|
|
176
|
+
exitOnCtrlC: false, // handled inside the app (clear draft first, then quit)
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const program = new Command();
|
|
181
|
+
program
|
|
182
|
+
.name('codeply')
|
|
183
|
+
.description('Apply an AI instruction to a file from the terminal.')
|
|
184
|
+
.version(require('../package.json').version);
|
|
185
|
+
|
|
186
|
+
program
|
|
187
|
+
.command('login')
|
|
188
|
+
.description('Sign in with your Codeply account (email + emailed code).')
|
|
189
|
+
.action(async () => { await auth.login(); });
|
|
190
|
+
|
|
191
|
+
program
|
|
192
|
+
.command('logout')
|
|
193
|
+
.description('Sign out of the current Codeply session.')
|
|
194
|
+
.action(async () => { await auth.logout(); });
|
|
195
|
+
|
|
196
|
+
program
|
|
197
|
+
.command('whoami')
|
|
198
|
+
.description('Show the currently signed-in account.')
|
|
199
|
+
.action(async () => {
|
|
200
|
+
const session = await auth.getSession();
|
|
201
|
+
console.log(session ? session.user.email : 'Not signed in.');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
program
|
|
205
|
+
.command('provider [name]')
|
|
206
|
+
.description('Show or set the model backend (codeply | ollama | openrouter | groq | anthropic | openai | google | qwen).')
|
|
207
|
+
.option('--key <key>', 'API key for ollama (cloud only), openrouter, groq, anthropic, openai, google, or qwen')
|
|
208
|
+
.option('--model <model>', 'model name for the selected provider')
|
|
209
|
+
.option('--host <url>', 'ollama host only (default https://ollama.com; local: http://localhost:11434)')
|
|
210
|
+
.option('--base-url <url>', 'qwen only - Alibaba Model Studio compatible-mode endpoint (deployment-specific)')
|
|
211
|
+
.action(async (name, opts) => {
|
|
212
|
+
const config = require('../lib/config');
|
|
213
|
+
|
|
214
|
+
if (!name && !opts.key && !opts.model && !opts.host && !opts.baseUrl) {
|
|
215
|
+
const cfg = config.getConfig();
|
|
216
|
+
console.log(`Provider ${c.accent(config.describeProvider(cfg))}`);
|
|
217
|
+
if (cfg.provider === 'ollama') {
|
|
218
|
+
console.log(`Host ${cfg.ollama.host}`);
|
|
219
|
+
console.log(`Model ${cfg.ollama.model}`);
|
|
220
|
+
console.log(`Key ${config.maskKey(cfg.ollama.apiKey)}`);
|
|
221
|
+
} else if (config.BYOK_PROVIDERS.includes(cfg.provider)) {
|
|
222
|
+
console.log(`Model ${cfg[cfg.provider].model}`);
|
|
223
|
+
console.log(`Key ${config.maskKey(cfg[cfg.provider].apiKey)}`);
|
|
224
|
+
}
|
|
225
|
+
console.log(c.muted(`\nConfig ${config.configPath}`));
|
|
226
|
+
console.log(c.muted(`Providers ${config.PROVIDERS.join(', ')}`));
|
|
227
|
+
console.log(c.muted('Set with codeply provider openrouter --key <key> [--model <model>]'));
|
|
228
|
+
if (cfg.provider === 'codeply') {
|
|
229
|
+
console.log(c.muted(`\n${config.byokHint()}`));
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (name && !config.PROVIDERS.includes(name)) {
|
|
235
|
+
console.error(`Unknown provider "${name}". Use one of: ${config.PROVIDERS.join(', ')}.`);
|
|
236
|
+
process.exitCode = 1;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const patch = {};
|
|
241
|
+
if (name) patch.provider = name;
|
|
242
|
+
const target = name || config.getConfig().provider;
|
|
243
|
+
|
|
244
|
+
if (target === 'ollama') {
|
|
245
|
+
const ollama = {};
|
|
246
|
+
if (opts.key) ollama.apiKey = opts.key;
|
|
247
|
+
if (opts.model) ollama.model = opts.model;
|
|
248
|
+
if (opts.host) ollama.host = opts.host;
|
|
249
|
+
if (Object.keys(ollama).length) patch.ollama = ollama;
|
|
250
|
+
} else if (config.BYOK_PROVIDERS.includes(target)) {
|
|
251
|
+
const byok = {};
|
|
252
|
+
if (opts.key) byok.apiKey = opts.key;
|
|
253
|
+
if (opts.model) byok.model = opts.model;
|
|
254
|
+
if (target === 'qwen' && opts.baseUrl) byok.baseUrl = opts.baseUrl;
|
|
255
|
+
if (Object.keys(byok).length) patch[target] = byok;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const saved = config.saveConfig(patch);
|
|
259
|
+
if (!saved.ok) {
|
|
260
|
+
console.error(`Could not save config: ${saved.error}`);
|
|
261
|
+
process.exitCode = 1;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
console.log(c.green(`✓ Provider: ${config.describeProvider(config.getConfig())}`));
|
|
265
|
+
console.log(c.muted(` saved to ${saved.path}`));
|
|
266
|
+
if (opts.key) console.log(c.muted(' the key is stored outside the repo and is never committed'));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
program
|
|
270
|
+
.command('apply <instruction>')
|
|
271
|
+
.description('Apply an instruction to a file.')
|
|
272
|
+
.requiredOption('-f, --file <path>', 'file to edit')
|
|
273
|
+
.option('-y, --yes', 'apply without confirmation prompt')
|
|
274
|
+
.option('--dry-run', 'show the proposed edits without writing them')
|
|
275
|
+
.action(runApply);
|
|
276
|
+
|
|
277
|
+
const skillCmd = program
|
|
278
|
+
.command('skill')
|
|
279
|
+
.description('List, install, or remove agent skills (SKILL.md playbooks the agent can load mid-task).');
|
|
280
|
+
|
|
281
|
+
skillCmd
|
|
282
|
+
.command('list', { isDefault: true })
|
|
283
|
+
.description('List every available skill.')
|
|
284
|
+
.option('-a, --all', 'include full descriptions (default shows a compact table)')
|
|
285
|
+
.action((opts) => {
|
|
286
|
+
const skills = require('../lib/skills');
|
|
287
|
+
const list = skills.listSkills();
|
|
288
|
+
if (list.length === 0) {
|
|
289
|
+
console.log('No skills installed.');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const daily = list.filter((s) => s.daily);
|
|
293
|
+
const rest = list.filter((s) => !s.daily);
|
|
294
|
+
console.log(c.bold(`${list.length} skills`) + c.muted(` (${daily.length} loaded by default, ${rest.length} available via list_skills or /skill install)`));
|
|
295
|
+
console.log('');
|
|
296
|
+
for (const s of list) {
|
|
297
|
+
const tag = s.source === 'user' ? c.green('[user]') : s.daily ? c.blue('[daily]') : c.muted('[library]');
|
|
298
|
+
console.log(`${s.name.padEnd(30)} ${tag}`);
|
|
299
|
+
if (opts.all) console.log(c.muted(` ${s.description}`));
|
|
300
|
+
}
|
|
301
|
+
console.log('');
|
|
302
|
+
console.log(c.muted(`Bundled from affaan-m/ECC - see skills/SOURCE.md. Add your own: codeply skill install <path-or-github-url>`));
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
skillCmd
|
|
306
|
+
.command('install <source>')
|
|
307
|
+
.description('Install a skill from a local directory (containing SKILL.md) or a GitHub URL/owner-repo.')
|
|
308
|
+
.option('-n, --name <name>', 'name to install under (single-skill sources only; ignored for a whole-repo install)')
|
|
309
|
+
.action(async (source, opts) => {
|
|
310
|
+
const skills = require('../lib/skills');
|
|
311
|
+
const isLocal = fs.existsSync(source);
|
|
312
|
+
|
|
313
|
+
if (isLocal) {
|
|
314
|
+
const result = skills.installFromLocalDir(path.resolve(process.cwd(), source), opts.name);
|
|
315
|
+
if (!result.ok) { console.error(c.red(result.error)); process.exitCode = 1; return; }
|
|
316
|
+
console.log(c.green(`✓ Installed "${result.name}"`));
|
|
317
|
+
console.log(c.muted(` ${result.dest}`));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
console.log(c.secondary(`Fetching from ${source}…`));
|
|
322
|
+
let result;
|
|
323
|
+
try {
|
|
324
|
+
result = await skills.installFromGitHub(source, opts.name);
|
|
325
|
+
} catch (e) {
|
|
326
|
+
console.error(c.red(`Install failed: ${e.message}`));
|
|
327
|
+
process.exitCode = 1;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (!result.ok) {
|
|
331
|
+
console.error(c.red(result.error || `No skills installed from ${source}.`));
|
|
332
|
+
process.exitCode = 1;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
console.log(c.green(`✓ Installed ${result.installed.length} skill(s) from ${result.source}`));
|
|
336
|
+
for (const i of result.installed) console.log(c.muted(` ${i.name} (${i.fileCount} file(s)) → ${i.dest}`));
|
|
337
|
+
if (result.failed.length) {
|
|
338
|
+
console.log(c.yellow(` ${result.failed.length} skipped (fetch failed):`));
|
|
339
|
+
for (const f of result.failed) console.log(c.muted(` ${f.dir}: ${f.error}`));
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
skillCmd
|
|
344
|
+
.command('remove <name>')
|
|
345
|
+
.description('Remove a user-installed skill. Bundled skills cannot be removed this way.')
|
|
346
|
+
.action((name) => {
|
|
347
|
+
const skills = require('../lib/skills');
|
|
348
|
+
const result = skills.removeSkill(name);
|
|
349
|
+
if (!result.ok) { console.error(c.red(result.error)); process.exitCode = 1; return; }
|
|
350
|
+
console.log(c.green(`✓ Removed "${name}"`));
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
skillCmd
|
|
354
|
+
.command('show <name>')
|
|
355
|
+
.description('Print a skill\'s full instructions.')
|
|
356
|
+
.action((name) => {
|
|
357
|
+
const skills = require('../lib/skills');
|
|
358
|
+
const body = skills.loadSkillBody(name);
|
|
359
|
+
if (body == null) {
|
|
360
|
+
console.error(c.red(`No skill named "${name}". Run \`codeply skill list\` to see what's available.`));
|
|
361
|
+
process.exitCode = 1;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
console.log(body);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const pluginCmd = program
|
|
368
|
+
.command('plugin')
|
|
369
|
+
.description('Install, share and manage plugins: bundles of commands, skills, MCP servers and instructions.');
|
|
370
|
+
|
|
371
|
+
function printPluginSummary(s) {
|
|
372
|
+
console.log(c.bold(`${s.name}${s.version ? ' ' + s.version : ''}`) + (s.author ? c.muted(` by ${s.author}`) : ''));
|
|
373
|
+
if (s.description) console.log(` ${s.description}`);
|
|
374
|
+
const parts = [];
|
|
375
|
+
if (s.commands) parts.push(`${s.commands} command${s.commands === 1 ? '' : 's'} (/${s.name}:...)`);
|
|
376
|
+
if (s.skills) parts.push(`${s.skills} skill${s.skills === 1 ? '' : 's'}`);
|
|
377
|
+
if (s.instructions) parts.push('agent instructions');
|
|
378
|
+
if (parts.length) console.log(c.muted(` Adds: ${parts.join(', ')}`));
|
|
379
|
+
if (s.mcpServers.length) {
|
|
380
|
+
console.log(c.yellow(' Starts MCP servers (these can run programs on this computer):'));
|
|
381
|
+
for (const m of s.mcpServers) console.log(` ${m.name}: ${m.runs || m.url}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
pluginCmd
|
|
386
|
+
.command('list', { isDefault: true })
|
|
387
|
+
.description('List installed plugins.')
|
|
388
|
+
.action(() => {
|
|
389
|
+
const list = require('../lib/plugins').listPlugins(process.cwd());
|
|
390
|
+
if (!list.length) {
|
|
391
|
+
console.log('No plugins installed. Try: codeply plugin install <owner/repo | git URL | folder>');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
for (const p of list) {
|
|
395
|
+
const tag = [p.scope, p.enabled ? '' : 'disabled'].filter(Boolean).join(', ');
|
|
396
|
+
console.log(`${p.name.padEnd(24)} ${p.version.padEnd(8)} ${c.muted(`[${tag}]`)} ${p.description}`);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
pluginCmd
|
|
401
|
+
.command('install <source>')
|
|
402
|
+
.description('Install from owner/repo, a git URL (add #branch or #tag), or a local folder.')
|
|
403
|
+
.option('--project', 'install for this project only (default: every project)')
|
|
404
|
+
.option('--force', 'replace an existing plugin of the same name')
|
|
405
|
+
.option('-y, --yes', 'do not ask before installing')
|
|
406
|
+
.action(async (source, opts) => {
|
|
407
|
+
const plugins = require('../lib/plugins');
|
|
408
|
+
console.log(c.secondary(`Fetching ${source}...`));
|
|
409
|
+
const prepared = await plugins.prepareInstall(source);
|
|
410
|
+
if (!prepared.ok) { console.error(c.red(prepared.error)); process.exitCode = 1; return; }
|
|
411
|
+
console.log('');
|
|
412
|
+
printPluginSummary(prepared.summary);
|
|
413
|
+
console.log('');
|
|
414
|
+
if (!opts.yes && !(await confirm(c.bold(c.accent(`Install ${prepared.name}?`))))) { plugins.discardInstall(prepared); console.log('Cancelled.'); return; }
|
|
415
|
+
const done = plugins.finishInstall(prepared, { scope: opts.project ? 'project' : 'user', cwd: process.cwd(), force: !!opts.force });
|
|
416
|
+
if (!done.ok) { console.error(c.red(done.error)); process.exitCode = 1; return; }
|
|
417
|
+
console.log(c.green(`Installed ${done.name}`) + c.muted(` ${done.dest}`));
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
pluginCmd
|
|
421
|
+
.command('update [name]')
|
|
422
|
+
.description('Re-fetch a plugin (or all of them) from where it was installed.')
|
|
423
|
+
.option('-y, --yes', 'do not ask before updating')
|
|
424
|
+
.action(async (name, opts) => {
|
|
425
|
+
const plugins = require('../lib/plugins');
|
|
426
|
+
const names = name ? [name] : plugins.listPlugins(process.cwd()).filter((p) => p.source).map((p) => p.name);
|
|
427
|
+
if (!names.length) { console.log('Nothing to update.'); return; }
|
|
428
|
+
for (const n of names) {
|
|
429
|
+
const prepared = await plugins.prepareUpdate(n, process.cwd());
|
|
430
|
+
if (!prepared.ok) { console.error(c.red(`${n}: ${prepared.error}`)); process.exitCode = 1; continue; }
|
|
431
|
+
const oldMcp = new Set(prepared.before.mcpServers.map((m) => `${m.name}|${m.runs}|${m.url}`));
|
|
432
|
+
const added = prepared.summary.mcpServers.filter((m) => !oldMcp.has(`${m.name}|${m.runs}|${m.url}`));
|
|
433
|
+
if (added.length && !opts.yes) {
|
|
434
|
+
console.log(c.yellow(`${n} now starts new or changed MCP servers:`));
|
|
435
|
+
for (const m of added) console.log(` ${m.name}: ${m.runs || m.url}`);
|
|
436
|
+
if (!(await confirm(c.bold(c.accent(`Update ${n}?`))))) { plugins.discardInstall(prepared); console.log(`Skipped ${n}.`); continue; }
|
|
437
|
+
}
|
|
438
|
+
const done = plugins.finishInstall(prepared, { scope: prepared.scope, cwd: process.cwd(), force: true });
|
|
439
|
+
if (!done.ok) { console.error(c.red(`${n}: ${done.error}`)); process.exitCode = 1; continue; }
|
|
440
|
+
console.log(c.green(`Updated ${n}`) + (done.summary.version ? c.muted(` ${done.summary.version}`) : ''));
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
pluginCmd
|
|
445
|
+
.command('remove <name>')
|
|
446
|
+
.description('Uninstall a plugin.')
|
|
447
|
+
.action((name) => {
|
|
448
|
+
const r = require('../lib/plugins').removePlugin(name, process.cwd());
|
|
449
|
+
if (!r.ok) { console.error(c.red(r.error)); process.exitCode = 1; return; }
|
|
450
|
+
console.log(c.green(`Removed ${r.name}`));
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
for (const [verb, on] of [['enable', true], ['disable', false]]) {
|
|
454
|
+
pluginCmd
|
|
455
|
+
.command(`${verb} <name>`)
|
|
456
|
+
.description(`${on ? 'Turn a plugin back on' : 'Turn a plugin off without removing it'}.`)
|
|
457
|
+
.action((name) => {
|
|
458
|
+
const r = require('../lib/plugins').setEnabled(name, on, process.cwd());
|
|
459
|
+
if (!r.ok) { console.error(c.red(r.error)); process.exitCode = 1; return; }
|
|
460
|
+
console.log(c.green(`${on ? 'Enabled' : 'Disabled'} ${r.name}`));
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
pluginCmd
|
|
465
|
+
.command('init <name>')
|
|
466
|
+
.description('Create a new plugin folder to fill in and push to GitHub.')
|
|
467
|
+
.action((name) => {
|
|
468
|
+
const r = require('../lib/plugins').scaffoldPlugin(name, process.cwd());
|
|
469
|
+
if (!r.ok) { console.error(c.red(r.error)); process.exitCode = 1; return; }
|
|
470
|
+
console.log(c.green(`Created ${r.dir}`));
|
|
471
|
+
console.log(c.muted(`Try it locally: codeply plugin install ${r.dir}\nShare it: push the folder to GitHub, then others run: codeply plugin install <you>/${r.name}`));
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const githubCmd = program
|
|
475
|
+
.command('github')
|
|
476
|
+
.description('The GitHub agent: comment /codeply on an issue or pull request and Craft does the work in your own Actions runner.');
|
|
477
|
+
|
|
478
|
+
githubCmd
|
|
479
|
+
.command('install')
|
|
480
|
+
.description('Add the workflow file that lets /codeply comments start a run.')
|
|
481
|
+
.action(async () => {
|
|
482
|
+
const { installWorkflow } = await import('../lib/github-agent.mjs');
|
|
483
|
+
const r = installWorkflow(process.cwd());
|
|
484
|
+
if (!r.ok) { console.error(c.red(r.error)); process.exitCode = 1; return; }
|
|
485
|
+
console.log(c.green(`Created ${r.file}`));
|
|
486
|
+
console.log([
|
|
487
|
+
'',
|
|
488
|
+
'Finish in your repository settings:',
|
|
489
|
+
' 1. Settings > Secrets and variables > Actions > New repository secret: CODEPLY_API_KEY (your own model key).',
|
|
490
|
+
' 2. Optional variables: CODEPLY_PROVIDER (openai, anthropic, gemini, openrouter, groq, deepseek), CODEPLY_MODEL, CODEPLY_BASE_URL.',
|
|
491
|
+
' 3. Settings > Actions > General > Workflow permissions: "Read and write" and allow Actions to create pull requests.',
|
|
492
|
+
' 4. Commit the workflow, then comment "/codeply <what you want>" on an issue or pull request.',
|
|
493
|
+
'',
|
|
494
|
+
'Only the owner, members and collaborators can start a run. "/codeply ask ..." and "/codeply plan ..." never change files.',
|
|
495
|
+
].join('\n'));
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
githubCmd
|
|
499
|
+
.command('run')
|
|
500
|
+
.description('Handle the current GitHub Actions event (used by the workflow).')
|
|
501
|
+
.option('--max-steps <n>', 'step budget for the agent', '40')
|
|
502
|
+
.action(async (opts) => {
|
|
503
|
+
const g = await import('../lib/github-agent.mjs');
|
|
504
|
+
const cfg = { ...process.env };
|
|
505
|
+
const token = cfg.GITHUB_TOKEN || cfg.GH_TOKEN;
|
|
506
|
+
// The agent runs commands and MCP servers; it must never see these.
|
|
507
|
+
for (const k of ['GITHUB_TOKEN', 'GH_TOKEN', 'CODEPLY_API_KEY', 'ACTIONS_RUNTIME_TOKEN', 'ACTIONS_ID_TOKEN_REQUEST_TOKEN']) delete process.env[k];
|
|
508
|
+
if (!cfg.GITHUB_EVENT_NAME || !cfg.GITHUB_EVENT_PATH) { console.error(c.red('This runs inside GitHub Actions (GITHUB_EVENT_NAME and GITHUB_EVENT_PATH are not set).')); process.exitCode = 1; return; }
|
|
509
|
+
let event;
|
|
510
|
+
try { event = JSON.parse(fs.readFileSync(cfg.GITHUB_EVENT_PATH, 'utf8')); } catch (e) { console.error(c.red(`Could not read the event: ${e.message}`)); process.exitCode = 1; return; }
|
|
511
|
+
const r = g.routeFromEnv(cfg);
|
|
512
|
+
// A setup error is still reported on the issue, so the person who asked isn't left waiting.
|
|
513
|
+
const out = await g.runGithubAgent({
|
|
514
|
+
eventName: cfg.GITHUB_EVENT_NAME, event, token, route: r.route, setupError: r.error, env: cfg, cwd: process.cwd(),
|
|
515
|
+
maxSteps: Math.max(1, Number(opts.maxSteps) || 40), log: (m) => console.log(m),
|
|
516
|
+
});
|
|
517
|
+
console.log(`${out.status}${out.url ? ` ${out.url}` : ''}`);
|
|
518
|
+
if (out.status === 'failed') { console.error(c.red(out.message || 'Failed.')); process.exitCode = 1; }
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
program
|
|
522
|
+
.command('serve')
|
|
523
|
+
.description('Run the engine as a local HTTP API with a live event stream, for the desktop app, phone, editors and scripts.')
|
|
524
|
+
.option('-p, --port <port>', 'port to listen on (0 picks a free one)', '4096')
|
|
525
|
+
.option('--host <host>', 'address to bind; anything but 127.0.0.1 exposes the engine to the network', '127.0.0.1')
|
|
526
|
+
.option('--password <password>', 'password clients must send (default: CODEPLY_SERVER_PASSWORD, else a random one is printed)')
|
|
527
|
+
.option('--cwd <dir>', 'default project folder for new chats', process.cwd())
|
|
528
|
+
.option('--data-dir <dir>', 'where chats are stored (default ~/.codeply/server)')
|
|
529
|
+
.option('--cors <origin...>', 'browser origins allowed to call the API')
|
|
530
|
+
.action(async (opts) => {
|
|
531
|
+
const { startServer } = await import('../lib/server.mjs');
|
|
532
|
+
const password = opts.password || process.env.CODEPLY_SERVER_PASSWORD || undefined;
|
|
533
|
+
let srv;
|
|
534
|
+
try {
|
|
535
|
+
srv = await startServer({ port: Number(opts.port), host: opts.host, password, cwd: opts.cwd, dataDir: opts.dataDir, allowedOrigins: opts.cors });
|
|
536
|
+
} catch (e) {
|
|
537
|
+
console.error(c.red(`Could not start the server: ${e.message}`));
|
|
538
|
+
process.exitCode = 1;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
console.log(c.green(`✓ Codeply engine listening on ${srv.url}`));
|
|
542
|
+
console.log(` Password ${srv.password}${password ? '' : c.muted(' (generated for this run)')}`);
|
|
543
|
+
console.log(c.muted(` Chats ${srv.storage === 'json' ? 'JSON file (this Node has no SQLite)' : `SQLite (${srv.storage})`}`));
|
|
544
|
+
console.log(c.muted(` Try curl -H "Authorization: Bearer ${srv.password}" ${srv.url}/doc`));
|
|
545
|
+
if (!['127.0.0.1', 'localhost', '::1'].includes(opts.host)) {
|
|
546
|
+
console.log(c.yellow(' Bound beyond this machine over plain HTTP. Put it behind a tunnel or TLS proxy before exposing it.'));
|
|
547
|
+
}
|
|
548
|
+
const stop = async () => { await srv.close(); process.exit(0); };
|
|
549
|
+
process.on('SIGINT', stop);
|
|
550
|
+
process.on('SIGTERM', stop);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// If no arguments provided, launch TUI
|
|
554
|
+
if (process.argv.length <= 2) {
|
|
555
|
+
runTUI();
|
|
556
|
+
} else {
|
|
557
|
+
program.parseAsync(process.argv);
|
|
558
|
+
}
|