codeply-cli 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.
- package/bin/codeply.js +259 -0
- package/lib/agent.mjs +533 -0
- package/lib/ai.js +181 -0
- package/lib/ai.mjs +6 -0
- package/lib/apply-limit.js +41 -0
- package/lib/auth.js +112 -0
- package/lib/auth.mjs +6 -0
- package/lib/config.js +89 -0
- package/lib/edit-engine.js +207 -0
- package/lib/render-md.mjs +617 -0
- package/lib/textarea.mjs +258 -0
- package/lib/tools.mjs +376 -0
- package/lib/tui.js +7 -0
- package/lib/tui.mjs +899 -0
- package/package.json +45 -0
package/bin/codeply.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(c.secondary(`Asking Codeply about ${path.basename(filePath)}…`));
|
|
82
|
+
const result = await editEngine.computeInstructionEdits(instruction, filePath);
|
|
83
|
+
|
|
84
|
+
if (!result.success) {
|
|
85
|
+
console.error(c.red(`Failed: ${result.error}`));
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!result.edits.length) {
|
|
90
|
+
console.log(result.reason || 'No edits produced — the instruction may be too ambiguous or too large for a targeted change.');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
printEditPreview(result.edits);
|
|
95
|
+
if (result.badCount) {
|
|
96
|
+
console.log(c.muted(`Note: ${result.badCount} proposed change(s) couldn't be matched against the file and were skipped.`));
|
|
97
|
+
}
|
|
98
|
+
console.log(c.muted(`${result.reason || ''}${result.reason ? ' — ' : ''}confidence ${result.confidence ?? '?'}%, ${result.tokensUsed || 0} tokens, model ${result.modelUsed || 'unknown'}`));
|
|
99
|
+
|
|
100
|
+
if (opts.dryRun) {
|
|
101
|
+
console.log(c.muted('\n(dry run — nothing written)'));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!opts.yes) {
|
|
106
|
+
const ok = await confirm(c.bold(c.accent(`Apply ${result.edits.length} edit(s) to ${path.basename(filePath)}?`)));
|
|
107
|
+
if (!ok) { console.log(c.secondary('Cancelled.')); return; }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
111
|
+
const applied = editEngine.applyEditsToContent(fileContent, result.edits);
|
|
112
|
+
if (!applied.ok) {
|
|
113
|
+
console.error(`Could not apply edit ${applied.index + 1}: ${applied.error}`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fs.writeFileSync(filePath, applied.content);
|
|
119
|
+
if (usingCodeply) await applyLimit.recordApplyEvent(filePath);
|
|
120
|
+
console.log(c.green(`✓ Applied to ${filePath}`));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// TUI Mode - interactive when no command is provided
|
|
124
|
+
async function runTUI() {
|
|
125
|
+
// Check if running in interactive terminal
|
|
126
|
+
if (!process.stdin.isTTY) {
|
|
127
|
+
const VERSION = require('../package.json').version;
|
|
128
|
+
// C=blue O=red D=yellow E=green P=blue L=red Y=yellow
|
|
129
|
+
// Fixed-width letters + trailing gap so CODEPLY reads cleanly
|
|
130
|
+
const letterColors = [c.blue, c.red, c.yellow, c.green, c.blue, c.red, c.yellow];
|
|
131
|
+
const logoLines = [
|
|
132
|
+
[' ██████╗ ', ' ██████╗ ', '██████╗ ', '███████╗ ', '██████╗ ', '██╗ ', '██╗ ██╗'],
|
|
133
|
+
['██╔════╝ ', '██╔═══██╗', '██╔══██╗ ', '██╔════╝ ', '██╔══██╗ ', '██║ ', '╚██╗ ██╔╝'],
|
|
134
|
+
['██║ ', '██║ ██║', '██║ ██║ ', '█████╗ ', '██████╔╝ ', '██║ ', ' ╚████╔╝ '],
|
|
135
|
+
['██║ ', '██║ ██║', '██║ ██║ ', '██╔══╝ ', '██╔═══╝ ', '██║ ', ' ╚██╔╝ '],
|
|
136
|
+
['╚██████╗ ', '╚██████╔╝', '██████╔╝ ', '███████╗ ', '██║ ', '███████╗ ', ' ██║ '],
|
|
137
|
+
[' ╚═════╝ ', ' ╚═════╝ ', '╚═════╝ ', '╚══════╝ ', '╚═╝ ', '╚══════╝ ', ' ╚═╝ '],
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
console.log('');
|
|
141
|
+
logoLines.forEach((line) => {
|
|
142
|
+
console.log(line.map((seg, i) => letterColors[i](seg)).join(''));
|
|
143
|
+
});
|
|
144
|
+
console.log(`
|
|
145
|
+
${c.secondary('Apply AI instructions. Edit files. Ship faster.')} ${c.muted('v' + VERSION)}
|
|
146
|
+
|
|
147
|
+
${c.bold(c.text('Usage'))}
|
|
148
|
+
${c.blue('codeply')} Launch interactive TUI
|
|
149
|
+
${c.blue('codeply apply')} ${c.dim('<instruction>')} ${c.blue('-f')} ${c.dim('<file>')}
|
|
150
|
+
${c.red('codeply login')} Sign in with your Codeply account
|
|
151
|
+
${c.yellow('codeply logout')} Sign out
|
|
152
|
+
${c.green('codeply whoami')} Show current account
|
|
153
|
+
|
|
154
|
+
${c.muted('Run in a terminal for the full interactive UI · codeply --help')}
|
|
155
|
+
`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const React = await import('react');
|
|
160
|
+
const { render } = await import('ink');
|
|
161
|
+
const { default: App } = await import('../lib/tui.mjs');
|
|
162
|
+
|
|
163
|
+
// Take the screen so the app doesn't look like output that happened to land
|
|
164
|
+
// in a shell. Deliberately a plain clear rather than the alternate screen
|
|
165
|
+
// buffer (\x1b[?1049h): alt-screen would discard scrollback on exit, and the
|
|
166
|
+
// whole point of the <Static> transcript is that you can scroll back through
|
|
167
|
+
// it afterwards.
|
|
168
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
169
|
+
|
|
170
|
+
render(React.default.createElement(App), {
|
|
171
|
+
exitOnCtrlC: false, // handled inside the app (clear draft first, then quit)
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const program = new Command();
|
|
176
|
+
program
|
|
177
|
+
.name('codeply')
|
|
178
|
+
.description('Apply an AI instruction to a file from the terminal.')
|
|
179
|
+
.version(require('../package.json').version);
|
|
180
|
+
|
|
181
|
+
program
|
|
182
|
+
.command('login')
|
|
183
|
+
.description('Sign in with your Codeply account (email + 6-digit code).')
|
|
184
|
+
.action(async () => { await auth.login(); });
|
|
185
|
+
|
|
186
|
+
program
|
|
187
|
+
.command('logout')
|
|
188
|
+
.description('Sign out of the current Codeply session.')
|
|
189
|
+
.action(async () => { await auth.logout(); });
|
|
190
|
+
|
|
191
|
+
program
|
|
192
|
+
.command('whoami')
|
|
193
|
+
.description('Show the currently signed-in account.')
|
|
194
|
+
.action(async () => {
|
|
195
|
+
const session = await auth.getSession();
|
|
196
|
+
console.log(session ? session.user.email : 'Not signed in.');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
program
|
|
200
|
+
.command('provider [name]')
|
|
201
|
+
.description('Show or set the model backend (codeply | ollama).')
|
|
202
|
+
.option('--key <key>', 'Ollama API key (cloud only; omit for a local daemon)')
|
|
203
|
+
.option('--model <model>', 'Ollama model, e.g. gemma4:31b')
|
|
204
|
+
.option('--host <url>', 'Ollama host (default https://ollama.com; local: http://localhost:11434)')
|
|
205
|
+
.action(async (name, opts) => {
|
|
206
|
+
const config = require('../lib/config');
|
|
207
|
+
|
|
208
|
+
if (!name && !opts.key && !opts.model && !opts.host) {
|
|
209
|
+
const cfg = config.getConfig();
|
|
210
|
+
console.log(`Provider ${c.accent(config.describeProvider(cfg))}`);
|
|
211
|
+
if (cfg.provider === 'ollama') {
|
|
212
|
+
console.log(`Host ${cfg.ollama.host}`);
|
|
213
|
+
console.log(`Model ${cfg.ollama.model}`);
|
|
214
|
+
console.log(`Key ${config.maskKey(cfg.ollama.apiKey)}`);
|
|
215
|
+
}
|
|
216
|
+
console.log(c.muted(`\nConfig ${config.configPath}`));
|
|
217
|
+
console.log(c.muted('Set with codeply provider ollama --key <key> --model gemma4:31b'));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (name && name !== 'codeply' && name !== 'ollama') {
|
|
222
|
+
console.error(`Unknown provider "${name}". Use "codeply" or "ollama".`);
|
|
223
|
+
process.exitCode = 1;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const patch = {};
|
|
228
|
+
if (name) patch.provider = name;
|
|
229
|
+
const ollama = {};
|
|
230
|
+
if (opts.key) ollama.apiKey = opts.key;
|
|
231
|
+
if (opts.model) ollama.model = opts.model;
|
|
232
|
+
if (opts.host) ollama.host = opts.host;
|
|
233
|
+
if (Object.keys(ollama).length) patch.ollama = ollama;
|
|
234
|
+
|
|
235
|
+
const saved = config.saveConfig(patch);
|
|
236
|
+
if (!saved.ok) {
|
|
237
|
+
console.error(`Could not save config: ${saved.error}`);
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
console.log(c.green(`✓ Provider: ${config.describeProvider(config.getConfig())}`));
|
|
242
|
+
console.log(c.muted(` saved to ${saved.path}`));
|
|
243
|
+
if (opts.key) console.log(c.muted(' the key is stored outside the repo and is never committed'));
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
program
|
|
247
|
+
.command('apply <instruction>')
|
|
248
|
+
.description('Apply an instruction to a file.')
|
|
249
|
+
.requiredOption('-f, --file <path>', 'file to edit')
|
|
250
|
+
.option('-y, --yes', 'apply without confirmation prompt')
|
|
251
|
+
.option('--dry-run', 'show the proposed edits without writing them')
|
|
252
|
+
.action(runApply);
|
|
253
|
+
|
|
254
|
+
// If no arguments provided, launch TUI
|
|
255
|
+
if (process.argv.length <= 2) {
|
|
256
|
+
runTUI();
|
|
257
|
+
} else {
|
|
258
|
+
program.parseAsync(process.argv);
|
|
259
|
+
}
|