shoud-cli 1.0.11 → 3.0.2
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/shoud.js +220 -116
- package/install.sh +13 -8
- package/package.json +10 -11
- package/src/auth/credentials.js +49 -0
- package/src/auth/deviceFlow.js +89 -111
- package/src/job/baseline.js +82 -0
- package/src/job/checkpoint.js +61 -0
- package/src/job/manager.js +89 -0
- package/src/job/model.js +54 -0
- package/src/job/receipt.js +92 -0
- package/src/job/undo.js +104 -0
- package/src/project/discovery.js +93 -0
- package/src/project/ignore.js +31 -0
- package/src/project/shoudMd.js +38 -0
- package/src/runtime/agentLoop.js +281 -98
- package/src/runtime/budget.js +30 -0
- package/src/runtime/noProgress.js +38 -0
- package/src/runtime/verification.js +39 -0
- package/src/security/permissions.js +132 -0
- package/src/security/riskClassifier.js +127 -0
- package/src/security/secrets.js +57 -0
- package/src/security/shellParser.js +56 -0
- package/src/tools/files.js +81 -0
- package/src/tools/git.js +32 -0
- package/src/tools/index.js +92 -156
- package/src/tools/project.js +7 -0
- package/src/tools/search.js +49 -0
- package/src/tools/shell.js +71 -0
- package/src/ui/banner.js +20 -0
- package/src/ui/output.js +17 -0
- package/src/utils/config.js +47 -0
- package/src/utils/errors.js +21 -0
- package/src/context/checkpoint.js +0 -82
- package/src/permissions/engine.js +0 -133
package/bin/shoud.js
CHANGED
|
@@ -1,144 +1,248 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
const { program } = require('commander');
|
|
4
3
|
const chalk = require('chalk');
|
|
5
|
-
const
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
|
|
8
|
+
const { initDirs } = require('../src/utils/config');
|
|
9
|
+
const { ExitCodes } = require('../src/utils/errors');
|
|
10
|
+
const { printStaticBanner } = require('../src/ui/banner');
|
|
11
|
+
const { login, getEmail } = require('../src/auth/deviceFlow');
|
|
12
|
+
const { createJob, JobStatus } = require('../src/job/model');
|
|
13
|
+
const { saveJob, loadJob, listJobs, findResumableJob } = require('../src/job/manager');
|
|
6
14
|
const { executeTask } = require('../src/runtime/agentLoop');
|
|
15
|
+
const { undoJob, undoLatest } = require('../src/job/undo');
|
|
16
|
+
const { formatReceipt } = require('../src/job/receipt');
|
|
17
|
+
const { discoverProject, formatProject } = require('../src/project/discovery');
|
|
7
18
|
|
|
8
|
-
|
|
9
|
-
const BANNER_ART = [
|
|
10
|
-
" ███████╗██╗ ██╗ ██████╗ ██╗ ██╗██████╗ ",
|
|
11
|
-
" ██╔════╝██║ ██║██╔═══██╗██║ ██║██╔══██╗",
|
|
12
|
-
" ███████╗███████║██║ ██║██║ ██║██║ ██║",
|
|
13
|
-
" ╚════██║██╔══██║██║ ██║██║ ██║██║ ██║",
|
|
14
|
-
" ███████║██║ ██║╚██████╔╝╚██████╔╝██████╔╝",
|
|
15
|
-
" ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ "
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
// ─── Static Banner (prints at the start of commands) ──────
|
|
19
|
-
function printStaticBanner() {
|
|
20
|
-
const color = chalk.hex('#B5F96C');
|
|
21
|
-
console.log(color(BANNER_ART.join('\n')));
|
|
22
|
-
console.log(color(' ┌──────────────────────────────────────────────────────────┐'));
|
|
23
|
-
console.log(color(' │ Give your computer a job. │'));
|
|
24
|
-
console.log(color(' └──────────────────────────────────────────────────────────┘'));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ─── Animated Banner (for help) ─────────────────────────────
|
|
28
|
-
const SLEEP = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
-
|
|
30
|
-
async function renderAnimatedBanner() {
|
|
31
|
-
if (!process.stdout.isTTY) {
|
|
32
|
-
printStaticBanner();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const colorGradients = [
|
|
37
|
-
['#364B1D', '#5E8232', '#86B947', '#B5F96C'],
|
|
38
|
-
['#5E8232', '#86B947', '#B5F96C', '#DDFFA8'],
|
|
39
|
-
['#86B947', '#B5F96C', '#FFFFFF', '#B5F96C'],
|
|
40
|
-
['#B5F96C', '#B5F96C', '#B5F96C', '#9EEB49']
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
for (const colors of colorGradients) {
|
|
44
|
-
process.stdout.write('\x1B[?25l');
|
|
45
|
-
process.stdout.write('\r\x1B[K');
|
|
46
|
-
console.clear();
|
|
47
|
-
console.log();
|
|
48
|
-
|
|
49
|
-
BANNER_ART.forEach((line, index) => {
|
|
50
|
-
const color = colors[index % colors.length];
|
|
51
|
-
console.log(chalk.hex(color).bold(line));
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
console.log(
|
|
55
|
-
chalk.hex('#3B472E')(
|
|
56
|
-
' ┌──────────────────────────────────────────────────────────┐\n' +
|
|
57
|
-
' │'
|
|
58
|
-
) +
|
|
59
|
-
chalk.hex('#B5F96C').bold(' Give your computer a job. ') +
|
|
60
|
-
chalk.hex('#3B472E')(
|
|
61
|
-
'│\n' +
|
|
62
|
-
' └──────────────────────────────────────────────────────────┘\n'
|
|
63
|
-
)
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
await SLEEP(75);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
process.stdout.write('\x1B[?25h');
|
|
70
|
-
}
|
|
19
|
+
initDirs();
|
|
71
20
|
|
|
72
|
-
// ─── Custom Help ──────────────────────────────────────────────
|
|
73
|
-
async function customHelp() {
|
|
74
|
-
console.log(chalk.hex('#B5F96C').bold('\n SHOUD Terminal Agent - Command Reference\n'));
|
|
75
|
-
console.log(chalk.gray(' Usage: shoud [command] OR shoud "[prompt]"\n'));
|
|
76
|
-
|
|
77
|
-
console.log(chalk.white.bold(' Commands:'));
|
|
78
|
-
console.log(` ${chalk.hex('#B5F96C')('shoud login')} Authenticate this device with your Google account.`);
|
|
79
|
-
console.log(` ${chalk.hex('#B5F96C')('shoud help')} Display this help menu.`);
|
|
80
|
-
console.log(` ${chalk.hex('#B5F96C')('shoud status')} Check your current credit balance and active plan.`);
|
|
81
|
-
console.log(` ${chalk.hex('#B5F96C')('shoud --version')} Show the version number.\n`);
|
|
82
|
-
|
|
83
|
-
console.log(chalk.white.bold(' Autonomous Execution:'));
|
|
84
|
-
console.log(chalk.gray(' Wrap your instructions in quotes to trigger the agent loop.'));
|
|
85
|
-
console.log(` ${chalk.hex('#B5F96C')('>')} shoud "Refactor the auth middleware to use JWTs"`);
|
|
86
|
-
console.log(` ${chalk.hex('#B5F96C')('>')} shoud "Run npm test and fix any failing test cases"\n`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ─── Main ──────────────────────────────────────────────────────
|
|
90
21
|
async function main() {
|
|
91
22
|
const args = process.argv.slice(2);
|
|
92
23
|
|
|
93
|
-
// Intercept
|
|
94
|
-
if (args.length === 0
|
|
95
|
-
|
|
96
|
-
args[0] === '--version' || args[0] === '-v') {
|
|
97
|
-
if (args[0] === '--version' || args[0] === '-v') {
|
|
98
|
-
program.version('1.0.9');
|
|
99
|
-
program.parse(process.argv);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
await renderAnimatedBanner();
|
|
103
|
-
await customHelp();
|
|
104
|
-
return;
|
|
24
|
+
// Intercept bare invocation
|
|
25
|
+
if (args.length === 0) {
|
|
26
|
+
return handleBareInvocation();
|
|
105
27
|
}
|
|
106
28
|
|
|
107
29
|
program
|
|
108
|
-
.
|
|
109
|
-
.
|
|
30
|
+
.name('shoud')
|
|
31
|
+
.version('2.0.0')
|
|
32
|
+
.description('SHOUD Terminal Agent')
|
|
33
|
+
.argument('[prompt...]', 'The task you want SHOUD to execute')
|
|
34
|
+
.option('--budget <amount>', 'Budget for this job in USD', parseFloat)
|
|
35
|
+
.option('--json', 'Emit JSON output (for scripting)')
|
|
36
|
+
.option('--no-color', 'Disable colored output')
|
|
37
|
+
.action(async (promptArr, opts) => {
|
|
38
|
+
if (!promptArr || promptArr.length === 0) {
|
|
39
|
+
await handleBareInvocation();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await runJob(promptArr.join(' '), opts);
|
|
43
|
+
});
|
|
110
44
|
|
|
111
45
|
program
|
|
112
46
|
.command('login')
|
|
113
|
-
.description('Authenticate this device
|
|
114
|
-
.action(() => {
|
|
115
|
-
|
|
116
|
-
|
|
47
|
+
.description('Authenticate this device')
|
|
48
|
+
.action(async () => {
|
|
49
|
+
try { await login(); process.exit(0); }
|
|
50
|
+
catch (e) { console.error(chalk.red(e.message)); process.exit(1); }
|
|
117
51
|
});
|
|
118
52
|
|
|
119
|
-
// Placeholder for status
|
|
120
53
|
program
|
|
121
54
|
.command('status')
|
|
122
|
-
.description('
|
|
55
|
+
.description('Show active job or last completed job')
|
|
56
|
+
.action(() => showStatus());
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command('jobs')
|
|
60
|
+
.description('List all jobs')
|
|
123
61
|
.action(() => {
|
|
124
|
-
|
|
62
|
+
const jobs = listJobs();
|
|
63
|
+
if (!jobs.length) { console.log('No jobs found.'); return; }
|
|
64
|
+
for (const j of jobs) {
|
|
65
|
+
console.log(` ${j.id} ${j.status.padEnd(24)} $${(j.spent || 0).toFixed(4)} ${j.prompt?.slice(0, 60)}`);
|
|
66
|
+
}
|
|
125
67
|
});
|
|
126
68
|
|
|
127
69
|
program
|
|
128
|
-
.
|
|
129
|
-
.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
70
|
+
.command('undo [jobId]')
|
|
71
|
+
.description('Undo changes from a job (or the most recent completed job)')
|
|
72
|
+
.action(async (jobId) => {
|
|
73
|
+
try {
|
|
74
|
+
let result;
|
|
75
|
+
if (jobId) result = { job: { id: jobId }, result: undoJob(jobId) };
|
|
76
|
+
else result = undoLatest();
|
|
77
|
+
console.log(chalk.green(`\n✓ Reverted job ${result.job.id}.`));
|
|
78
|
+
if (result.result.preservedUserChanges) {
|
|
79
|
+
console.log(chalk.green('✓ Pre-existing user changes preserved.'));
|
|
80
|
+
}
|
|
81
|
+
process.exit(0);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error(chalk.red(`\n✗ Undo failed: ${e.message}`));
|
|
84
|
+
process.exit(1);
|
|
133
85
|
}
|
|
134
|
-
const taskPrompt = promptArr.join(' ');
|
|
135
|
-
// ✅ Show the static banner at the start
|
|
136
|
-
printStaticBanner();
|
|
137
|
-
await executeTask(taskPrompt);
|
|
138
|
-
// No banner at the end (removed)
|
|
139
86
|
});
|
|
140
87
|
|
|
141
|
-
program
|
|
88
|
+
program
|
|
89
|
+
.command('receipt [jobId]')
|
|
90
|
+
.description('Show the receipt for a job')
|
|
91
|
+
.action((jobId) => {
|
|
92
|
+
const id = jobId || listJobs()[0]?.id;
|
|
93
|
+
if (!id) { console.log('No jobs found.'); return; }
|
|
94
|
+
const job = loadJob(id);
|
|
95
|
+
if (!job) { console.log(`Job ${id} not found.`); return; }
|
|
96
|
+
console.log('\n' + formatReceipt(job));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
program
|
|
100
|
+
.command('project')
|
|
101
|
+
.description('Show detected project info')
|
|
102
|
+
.action(() => {
|
|
103
|
+
const p = discoverProject(process.cwd());
|
|
104
|
+
console.log(chalk.bold('\nProject info\n'));
|
|
105
|
+
console.log(formatProject(p));
|
|
106
|
+
console.log();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await program.parseAsync(process.argv);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handleBareInvocation() {
|
|
113
|
+
printStaticBanner();
|
|
114
|
+
const resumable = findResumableJob();
|
|
115
|
+
|
|
116
|
+
if (resumable) {
|
|
117
|
+
console.log(chalk.yellow(`\nInterrupted job found: ${resumable.id}`));
|
|
118
|
+
console.log(chalk.gray(` ${resumable.prompt}`));
|
|
119
|
+
console.log(chalk.gray(` Status: ${resumable.status} Spent: $${(resumable.spent || 0).toFixed(4)}`));
|
|
120
|
+
|
|
121
|
+
const { action } = await inquirer.prompt([{
|
|
122
|
+
type: 'list', name: 'action',
|
|
123
|
+
message: 'What would you like to do?',
|
|
124
|
+
choices: [
|
|
125
|
+
{ name: 'Resume it', value: 'resume' },
|
|
126
|
+
{ name: 'View receipt', value: 'receipt' },
|
|
127
|
+
{ name: 'Undo its changes', value: 'undo' },
|
|
128
|
+
{ name: 'Start a new job', value: 'new' },
|
|
129
|
+
{ name: 'Quit', value: 'quit' },
|
|
130
|
+
],
|
|
131
|
+
}]);
|
|
132
|
+
|
|
133
|
+
if (action === 'resume') {
|
|
134
|
+
const job = loadJob(resumable.id);
|
|
135
|
+
job.status = JobStatus.RUNNING;
|
|
136
|
+
await executeTask(job);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (action === 'receipt') {
|
|
140
|
+
const job = loadJob(resumable.id);
|
|
141
|
+
console.log('\n' + formatReceipt(job));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (action === 'undo') {
|
|
145
|
+
try { undoJob(resumable.id); console.log(chalk.green('\n✓ Reverted.')); }
|
|
146
|
+
catch (e) { console.error(chalk.red(`✗ ${e.message}`)); }
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (action === 'quit') return;
|
|
150
|
+
// 'new' falls through
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// No resumable job, no prompt → print usage
|
|
154
|
+
console.log(chalk.bold('\nUsage:') + ' shoud "[prompt]"');
|
|
155
|
+
console.log(chalk.gray(' or: shoud login (authenticate)'));
|
|
156
|
+
console.log(chalk.gray(' or: shoud jobs (list jobs)'));
|
|
157
|
+
console.log(chalk.gray(' or: shoud undo (revert last job)'));
|
|
158
|
+
console.log();
|
|
159
|
+
console.log(chalk.bold('Commands:'));
|
|
160
|
+
for (const c of [
|
|
161
|
+
['shoud login', 'Authenticate this device'],
|
|
162
|
+
['shoud status', 'Show active/last job'],
|
|
163
|
+
['shoud jobs', 'List all jobs'],
|
|
164
|
+
['shoud undo [id]', 'Revert changes from a job'],
|
|
165
|
+
['shoud receipt [id]', 'Show a job receipt'],
|
|
166
|
+
['shoud project', 'Show detected project info'],
|
|
167
|
+
]) {
|
|
168
|
+
console.log(` ${chalk.hex('#B5F96C')(c[0].padEnd(22))} ${chalk.gray(c[1])}`);
|
|
169
|
+
}
|
|
170
|
+
console.log();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function runJob(prompt, opts) {
|
|
174
|
+
printStaticBanner();
|
|
175
|
+
|
|
176
|
+
const email = await getEmail();
|
|
177
|
+
if (!email) {
|
|
178
|
+
console.log(chalk.red('\nNot signed in. Run `shoud login` first.'));
|
|
179
|
+
process.exit(ExitCodes.AUTH_REQUIRED);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const projectRoot = process.cwd();
|
|
183
|
+
const job = createJob({
|
|
184
|
+
prompt,
|
|
185
|
+
projectRoot,
|
|
186
|
+
budget: opts.budget || 2.0,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Pre-fill verification commands from project discovery + SHOUD.md
|
|
190
|
+
const project = discoverProject(projectRoot);
|
|
191
|
+
const { loadShoudMd } = require('../src/project/shoudMd');
|
|
192
|
+
const shoud = loadShoudMd(projectRoot);
|
|
193
|
+
job.verification.commands = shoud.verification.length ? shoud.verification : (project.verification || []);
|
|
194
|
+
|
|
195
|
+
saveJob(job);
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const finalJob = await executeTask(job);
|
|
199
|
+
let exitCode = ExitCodes.VERIFIED;
|
|
200
|
+
switch (finalJob.status) {
|
|
201
|
+
case JobStatus.VERIFIED: exitCode = ExitCodes.VERIFIED; break;
|
|
202
|
+
case JobStatus.IMPLEMENTED_NOT_VERIFIED: exitCode = ExitCodes.VERIFIED; break;
|
|
203
|
+
case JobStatus.FAILED: exitCode = ExitCodes.FAILED; break;
|
|
204
|
+
case JobStatus.BLOCKED: exitCode = ExitCodes.BLOCKED; break;
|
|
205
|
+
case JobStatus.BUDGET_EXCEEDED: exitCode = ExitCodes.BUDGET_EXCEEDED; break;
|
|
206
|
+
case JobStatus.PERMISSION_DENIED: exitCode = ExitCodes.PERMISSION_DENIED; break;
|
|
207
|
+
case JobStatus.CANCELLED: exitCode = ExitCodes.CANCELLED; break;
|
|
208
|
+
default: exitCode = ExitCodes.FAILED;
|
|
209
|
+
}
|
|
210
|
+
if (opts.json) {
|
|
211
|
+
process.stdout.write(JSON.stringify({
|
|
212
|
+
jobId: finalJob.id,
|
|
213
|
+
status: finalJob.status,
|
|
214
|
+
spent: finalJob.spent,
|
|
215
|
+
changedFiles: finalJob.changedFiles,
|
|
216
|
+
verification: finalJob.verification.results.map(r => ({ command: r.command, ok: r.ok })),
|
|
217
|
+
}, null, 2) + '\n');
|
|
218
|
+
}
|
|
219
|
+
process.exit(exitCode);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err.code === ExitCodes.AUTH_REQUIRED) {
|
|
222
|
+
console.error(chalk.red('\n' + err.message));
|
|
223
|
+
process.exit(ExitCodes.AUTH_REQUIRED);
|
|
224
|
+
}
|
|
225
|
+
console.error(chalk.red(`\n✗ ${err.message}`));
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function showStatus() {
|
|
231
|
+
const jobs = listJobs();
|
|
232
|
+
if (!jobs.length) { console.log('No jobs yet.'); return; }
|
|
233
|
+
const active = jobs.find(j => !['verified', 'failed', 'cancelled', 'blocked', 'budget_exceeded'].includes(j.status));
|
|
234
|
+
const j = active || jobs[0];
|
|
235
|
+
console.log(chalk.bold('\nJob ' + j.id));
|
|
236
|
+
console.log(` Prompt: ${j.prompt}`);
|
|
237
|
+
console.log(` Status: ${j.status}`);
|
|
238
|
+
console.log(` Spent: $${(j.spent || 0).toFixed(4)}`);
|
|
239
|
+
console.log(` Started: ${j.startedAt}`);
|
|
240
|
+
if (j.completedAt) console.log(` Ended: ${j.completedAt}`);
|
|
241
|
+
console.log();
|
|
142
242
|
}
|
|
143
243
|
|
|
144
|
-
main()
|
|
244
|
+
main().catch((err) => {
|
|
245
|
+
console.error(chalk.red(`\nFatal: ${err.message}`));
|
|
246
|
+
if (process.env.SHOUD_DEBUG) console.error(err.stack);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
});
|
package/install.sh
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
|
-
echo "Installing SHOUD Agent..."
|
|
4
|
+
echo "Installing SHOUD Terminal Agent v2..."
|
|
5
5
|
echo "Give your computer a job."
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
if ! command -v node &> /dev/null; then
|
|
8
|
+
echo "Error: Node.js >= 18 required. Install from https://nodejs.org"
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]")
|
|
13
|
+
if [ "$NODE_MAJOR" -lt 18 ]; then
|
|
14
|
+
echo "Error: Node.js >= 18 required (found $(node -v))."
|
|
15
|
+
exit 1
|
|
12
16
|
fi
|
|
13
17
|
|
|
14
18
|
npm install -g shoud-cli
|
|
15
19
|
|
|
16
20
|
echo ""
|
|
17
|
-
echo "✓
|
|
18
|
-
echo "
|
|
21
|
+
echo "✓ Installed."
|
|
22
|
+
echo " Authenticate with: shoud login"
|
|
23
|
+
echo " Run a job with: shoud \"fix the build\""
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shoud-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "SHOUD Terminal Agent: Give your computer a job.",
|
|
5
5
|
"main": "bin/shoud.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"shoud": "bin/shoud.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"start": "node ./bin/shoud.js"
|
|
10
|
+
"start": "node ./bin/shoud.js",
|
|
11
|
+
"test": "node --test tests/"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"axios": "^1.6.8",
|
|
@@ -15,21 +16,19 @@
|
|
|
15
16
|
"commander": "^11.0.0",
|
|
16
17
|
"inquirer": "^8.2.6",
|
|
17
18
|
"open": "^8.4.2",
|
|
18
|
-
"ora": "^5.4.1"
|
|
19
|
+
"ora": "^5.4.1",
|
|
20
|
+
"fast-glob": "^3.3.2",
|
|
21
|
+
"ignore": "^5.3.0",
|
|
22
|
+
"diff": "^5.1.0"
|
|
19
23
|
},
|
|
20
24
|
"engines": {
|
|
21
|
-
"node": "18.0.0"
|
|
25
|
+
"node": ">=18.0.0"
|
|
22
26
|
},
|
|
23
27
|
"files": [
|
|
24
28
|
"bin/",
|
|
25
29
|
"src/",
|
|
26
30
|
"install.sh",
|
|
27
|
-
"
|
|
28
|
-
"!**/*.spec.js"
|
|
31
|
+
"README.md"
|
|
29
32
|
],
|
|
30
|
-
"license": "MIT"
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "https://github.com/your-username/shoud-monorepo.git"
|
|
34
|
-
}
|
|
33
|
+
"license": "MIT"
|
|
35
34
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { paths } = require('../utils/config');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stores CLI credentials. Attempts OS keychain first, falls back to 0600 file.
|
|
6
|
+
* OS keychain support is via optional peer dependency `keytar` — if unavailable,
|
|
7
|
+
* we transparently fall back so the CLI works everywhere.
|
|
8
|
+
*/
|
|
9
|
+
let keytar = null;
|
|
10
|
+
try { keytar = require('keytar'); } catch (_) { keytar = null; }
|
|
11
|
+
|
|
12
|
+
const SERVICE = 'shoud-cli';
|
|
13
|
+
const ACCOUNT = 'default';
|
|
14
|
+
|
|
15
|
+
async function saveCredentials(payload) {
|
|
16
|
+
const json = JSON.stringify(payload);
|
|
17
|
+
if (keytar) {
|
|
18
|
+
try {
|
|
19
|
+
await keytar.setPassword(SERVICE, ACCOUNT, json);
|
|
20
|
+
return { storage: 'keychain' };
|
|
21
|
+
} catch (_) { /* fall through */ }
|
|
22
|
+
}
|
|
23
|
+
fs.writeFileSync(paths.SESSION_FILE, json, { encoding: 'utf-8', mode: 0o600 });
|
|
24
|
+
return { storage: 'file' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function loadCredentials() {
|
|
28
|
+
if (keytar) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = await keytar.getPassword(SERVICE, ACCOUNT);
|
|
31
|
+
if (raw) return JSON.parse(raw);
|
|
32
|
+
} catch (_) {}
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
if (fs.existsSync(paths.SESSION_FILE)) {
|
|
36
|
+
return JSON.parse(fs.readFileSync(paths.SESSION_FILE, 'utf-8'));
|
|
37
|
+
}
|
|
38
|
+
} catch (_) {}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function clearCredentials() {
|
|
43
|
+
if (keytar) { try { await keytar.deletePassword(SERVICE, ACCOUNT); } catch (_) {} }
|
|
44
|
+
if (fs.existsSync(paths.SESSION_FILE)) {
|
|
45
|
+
try { fs.unlinkSync(paths.SESSION_FILE); } catch (_) {}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { saveCredentials, loadCredentials, clearCredentials };
|