scava-ai 0.1.0 → 0.2.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/README.md +3 -3
- package/cli/account.js +507 -507
- package/cli/check.js +1 -1
- package/cli/lib.js +252 -195
- package/cli/scava.js +287 -285
- package/package.json +2 -2
package/cli/scava.js
CHANGED
|
@@ -1,285 +1,287 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import { CMD, serverUrl, openBrowser, formatProblems, ScavaError, appOfFolder, readFolder, uploadFolder, testLink, pullLink } from './lib.js';
|
|
4
|
-
import { callTool, whoAmI, logout, signIn, finishWithAddress, signInHelper, credentialsPath } from './account.js';
|
|
5
|
-
import { testRun, describe as describeRun } from './check.js';
|
|
6
|
-
|
|
7
|
-
// "scava dev" needs Scava's own server code. That is only in the repo, never in the npm package: there the command is
|
|
8
|
-
// not listed and says so.
|
|
9
|
-
const HAS_DEV = fs.existsSync(new URL('./dev.js', import.meta.url));
|
|
10
|
-
const DEV_HELP = `${CMD} dev [folder] [--port 8788] [--no-browser]
|
|
11
|
-
The app with its server code on this computer, before it goes online: published again on every save, secrets
|
|
12
|
-
from .env, type "run <schedule>" to run a schedule.
|
|
13
|
-
|
|
14
|
-
`;
|
|
15
|
-
|
|
16
|
-
const HELP = `Scava: hosting, sign-in and a database for the apps your AI builds. One command, for you and for any AI with a terminal.
|
|
17
|
-
|
|
18
|
-
${CMD}
|
|
19
|
-
Puts the app in this folder online, on its own address. A new app the first time, a new version after that.
|
|
20
|
-
The folder needs an index.html; for a build,
|
|
21
|
-
browser here and lists what went wrong.
|
|
22
|
-
--name NAME --message TEXT --resolves 1,2 (change requests this version builds) --app APP --no-check
|
|
23
|
-
|
|
24
|
-
${CMD} share WHO...
|
|
25
|
-
Lets people open the app: someone@company.com, "@company.com", group:leads. --remove WHO takes them out.
|
|
26
|
-
Without WHO: who can open it now.
|
|
27
|
-
${CMD} apps
|
|
28
|
-
Your apps, the apps you co-edit, the apps shared with you.
|
|
29
|
-
${CMD} requests
|
|
30
|
-
Change requests you accepted, ready to build. --done: the ones already built.
|
|
31
|
-
${CMD} requests done ID
|
|
32
|
-
Marks one as done (
|
|
33
|
-
${CMD} test
|
|
34
|
-
Opens the live app once in a browser here, lists what goes wrong and what visitors reported.
|
|
35
|
-
${CMD} server
|
|
36
|
-
The server part of the app: state, usage, secrets, schedules, outside services.
|
|
37
|
-
${CMD} pull APP [folder]
|
|
38
|
-
The files of an app into a folder, to work on it here.
|
|
39
|
-
${CMD} docs
|
|
40
|
-
What an app can use on Scava: data, rules, files, server code.
|
|
41
|
-
${CMD} secret set NAME [--host HOST]
|
|
42
|
-
Where to set a key: the browser, so its value never passes through a terminal an AI can read.
|
|
43
|
-
|
|
44
|
-
Inside a folder you
|
|
45
|
-
|
|
46
|
-
The first command that needs your account opens a browser: sign in to Scava and choose Allow. After that it runs
|
|
47
|
-
without a browser. The access stays on this computer, and whoever works under your user here acts as you.
|
|
48
|
-
|
|
49
|
-
${CMD} whoami
|
|
50
|
-
Which account this computer is signed in as.
|
|
51
|
-
${CMD} login
|
|
52
|
-
Signs in now. --no-browser prints the address, --wait SECONDS, --paste ADDRESS (from a browser on another device).
|
|
53
|
-
${CMD} logout
|
|
54
|
-
Ends the access on this computer.
|
|
55
|
-
|
|
56
|
-
${HAS_DEV ? DEV_HELP : ''}With a one-time link from the Scava tools in your AI (no sign-in here):
|
|
57
|
-
|
|
58
|
-
${CMD} upload <folder> <link> [--no-check]
|
|
59
|
-
${CMD} test <link>
|
|
60
|
-
${CMD} pull <link> [folder]
|
|
61
|
-
|
|
62
|
-
Claude or ChatGPT in the browser have no terminal: add a connector with the address https://scava.ai/mcp.
|
|
63
|
-
All of it: https://scava.ai/docs
|
|
64
|
-
`;
|
|
65
|
-
|
|
66
|
-
// Options without a value, and options that may come several times. Everything else takes the next word as its value.
|
|
67
|
-
const FLAGS = new Set(['no-check', 'no-browser', 'done', 'help', 'version']);
|
|
68
|
-
const LISTS = new Set(['remove']);
|
|
69
|
-
|
|
70
|
-
function parse(argv) {
|
|
71
|
-
const pos = [];
|
|
72
|
-
const opts = {};
|
|
73
|
-
for (let i = 0; i < argv.length; i++) {
|
|
74
|
-
const a = argv[i];
|
|
75
|
-
if (!a.startsWith('--')) {
|
|
76
|
-
pos.push(a);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
const eq = a.indexOf('=');
|
|
80
|
-
const k = eq === -1 ? a.slice(2) : a.slice(2, eq);
|
|
81
|
-
let v = eq === -1 ? undefined : a.slice(eq + 1);
|
|
82
|
-
if (v === undefined && !FLAGS.has(k) && argv[i + 1] !== undefined && !argv[i + 1].startsWith('--')) v = argv[++i];
|
|
83
|
-
v ??= true;
|
|
84
|
-
if (LISTS.has(k)) (opts[k] ||= []).push(v);
|
|
85
|
-
else opts[k] = v;
|
|
86
|
-
}
|
|
87
|
-
return { pos, opts };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const text = (v) => (typeof v === 'string' && v ? v : undefined);
|
|
91
|
-
const isLink = (s) => /^https?:\/\//i.test(s ?? '');
|
|
92
|
-
|
|
93
|
-
async function main() {
|
|
94
|
-
const [cmd, ...rest] = process.argv.slice(2);
|
|
95
|
-
// The small program that waits for the browser (account.js), started by the commands below.
|
|
96
|
-
if (cmd === '__sign-in') {
|
|
97
|
-
await signInHelper(rest[0], rest[1]);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const { pos, opts } = parse(rest);
|
|
101
|
-
// "
|
|
102
|
-
if (opts.help || rest.includes('-h')) {
|
|
103
|
-
console.log(HELP);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const server = serverUrl(text(opts.server));
|
|
107
|
-
const signInOptions = {
|
|
108
|
-
browser: !opts['no-browser'],
|
|
109
|
-
...(opts.wait !== undefined && Number.isFinite(Number(opts.wait)) ? { waitMs: Math.max(0, Number(opts.wait)) * 1000 } : {}),
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// Which account a command acts for, once. Under one user on a computer that may not be who you think.
|
|
113
|
-
let named = false;
|
|
114
|
-
const tool = async (name, args) => {
|
|
115
|
-
const r = await callTool(server, name, args, signInOptions);
|
|
116
|
-
if (!named && r.account) console.log(`Account: ${r.account}`);
|
|
117
|
-
named = true;
|
|
118
|
-
return r;
|
|
119
|
-
};
|
|
120
|
-
const appHere = () => {
|
|
121
|
-
const app = text(opts.app) || appOfFolder('.');
|
|
122
|
-
if (!app) throw new ScavaError(`Which app? This folder
|
|
123
|
-
return app;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
switch (cmd) {
|
|
127
|
-
|
|
128
|
-
case '
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
console.log(
|
|
151
|
-
console.log(
|
|
152
|
-
console.log(` ${CMD}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
console.log(r.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
console.log(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
case
|
|
268
|
-
case '
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { CMD, serverUrl, openBrowser, formatProblems, ScavaError, appOfFolder, readFolder, uploadFolder, testLink, pullLink } from './lib.js';
|
|
4
|
+
import { callTool, whoAmI, logout, signIn, finishWithAddress, signInHelper, credentialsPath } from './account.js';
|
|
5
|
+
import { testRun, describe as describeRun } from './check.js';
|
|
6
|
+
|
|
7
|
+
// "scava dev" needs Scava's own server code. That is only in the repo, never in the npm package: there the command is
|
|
8
|
+
// not listed and says so.
|
|
9
|
+
const HAS_DEV = fs.existsSync(new URL('./dev.js', import.meta.url));
|
|
10
|
+
const DEV_HELP = `${CMD} dev [folder] [--port 8788] [--no-browser]
|
|
11
|
+
The app with its server code on this computer, before it goes online: published again on every save, secrets
|
|
12
|
+
from .env, type "run <schedule>" to run a schedule.
|
|
13
|
+
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const HELP = `Scava: hosting, sign-in and a database for the apps your AI builds. One command, for you and for any AI with a terminal.
|
|
17
|
+
|
|
18
|
+
${CMD} setup [folder]
|
|
19
|
+
Puts the app in this folder online, on its own address. A new app the first time, a new version after that.
|
|
20
|
+
The folder needs an index.html; for a build, use the output folder. Afterwards it opens the app once in a
|
|
21
|
+
browser here and lists what went wrong.
|
|
22
|
+
--name NAME --message TEXT --resolves 1,2 (change requests this version builds) --app APP --no-check
|
|
23
|
+
|
|
24
|
+
${CMD} share WHO...
|
|
25
|
+
Lets people open the app: someone@company.com, "@company.com", group:leads. --remove WHO takes them out.
|
|
26
|
+
Without WHO: who can open it now.
|
|
27
|
+
${CMD} apps
|
|
28
|
+
Your apps, the apps you co-edit, the apps shared with you.
|
|
29
|
+
${CMD} requests
|
|
30
|
+
Change requests you accepted, ready to build. --done: the ones already built.
|
|
31
|
+
${CMD} requests done ID
|
|
32
|
+
Marks one as done (setup --resolves does that too). --note TEXT
|
|
33
|
+
${CMD} test
|
|
34
|
+
Opens the live app once in a browser here, lists what goes wrong and what visitors reported.
|
|
35
|
+
${CMD} server
|
|
36
|
+
The server part of the app: state, usage, secrets, schedules, outside services.
|
|
37
|
+
${CMD} pull APP [folder]
|
|
38
|
+
The files of an app into a folder, to work on it here.
|
|
39
|
+
${CMD} docs
|
|
40
|
+
What an app can use on Scava: data, rules, files, server code.
|
|
41
|
+
${CMD} secret set NAME [--host HOST]
|
|
42
|
+
Where to set a key: the browser, so its value never passes through a terminal an AI can read.
|
|
43
|
+
|
|
44
|
+
Inside a folder you put online or pulled the app is known. Elsewhere add --app APP.
|
|
45
|
+
|
|
46
|
+
The first command that needs your account opens a browser: sign in to Scava and choose Allow. After that it runs
|
|
47
|
+
without a browser. The access stays on this computer, and whoever works under your user here acts as you.
|
|
48
|
+
|
|
49
|
+
${CMD} whoami
|
|
50
|
+
Which account this computer is signed in as.
|
|
51
|
+
${CMD} login
|
|
52
|
+
Signs in now. --no-browser prints the address, --wait SECONDS, --paste ADDRESS (from a browser on another device).
|
|
53
|
+
${CMD} logout
|
|
54
|
+
Ends the access on this computer.
|
|
55
|
+
|
|
56
|
+
${HAS_DEV ? DEV_HELP : ''}With a one-time link from the Scava tools in your AI (no sign-in here):
|
|
57
|
+
|
|
58
|
+
${CMD} upload <folder> <link> [--no-check]
|
|
59
|
+
${CMD} test <link>
|
|
60
|
+
${CMD} pull <link> [folder]
|
|
61
|
+
|
|
62
|
+
Claude or ChatGPT in the browser have no terminal: add a connector with the address https://scava.ai/mcp.
|
|
63
|
+
All of it: https://scava.ai/docs
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
// Options without a value, and options that may come several times. Everything else takes the next word as its value.
|
|
67
|
+
const FLAGS = new Set(['no-check', 'no-browser', 'done', 'help', 'version']);
|
|
68
|
+
const LISTS = new Set(['remove']);
|
|
69
|
+
|
|
70
|
+
function parse(argv) {
|
|
71
|
+
const pos = [];
|
|
72
|
+
const opts = {};
|
|
73
|
+
for (let i = 0; i < argv.length; i++) {
|
|
74
|
+
const a = argv[i];
|
|
75
|
+
if (!a.startsWith('--')) {
|
|
76
|
+
pos.push(a);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const eq = a.indexOf('=');
|
|
80
|
+
const k = eq === -1 ? a.slice(2) : a.slice(2, eq);
|
|
81
|
+
let v = eq === -1 ? undefined : a.slice(eq + 1);
|
|
82
|
+
if (v === undefined && !FLAGS.has(k) && argv[i + 1] !== undefined && !argv[i + 1].startsWith('--')) v = argv[++i];
|
|
83
|
+
v ??= true;
|
|
84
|
+
if (LISTS.has(k)) (opts[k] ||= []).push(v);
|
|
85
|
+
else opts[k] = v;
|
|
86
|
+
}
|
|
87
|
+
return { pos, opts };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const text = (v) => (typeof v === 'string' && v ? v : undefined);
|
|
91
|
+
const isLink = (s) => /^https?:\/\//i.test(s ?? '');
|
|
92
|
+
|
|
93
|
+
async function main() {
|
|
94
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
95
|
+
// The small program that waits for the browser (account.js), started by the commands below.
|
|
96
|
+
if (cmd === '__sign-in') {
|
|
97
|
+
await signInHelper(rest[0], rest[1]);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const { pos, opts } = parse(rest);
|
|
101
|
+
// "setup --help" is a question, not a publish.
|
|
102
|
+
if (opts.help || rest.includes('-h')) {
|
|
103
|
+
console.log(HELP);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const server = serverUrl(text(opts.server));
|
|
107
|
+
const signInOptions = {
|
|
108
|
+
browser: !opts['no-browser'],
|
|
109
|
+
...(opts.wait !== undefined && Number.isFinite(Number(opts.wait)) ? { waitMs: Math.max(0, Number(opts.wait)) * 1000 } : {}),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Which account a command acts for, once. Under one user on a computer that may not be who you think.
|
|
113
|
+
let named = false;
|
|
114
|
+
const tool = async (name, args) => {
|
|
115
|
+
const r = await callTool(server, name, args, signInOptions);
|
|
116
|
+
if (!named && r.account) console.log(`Account: ${r.account}`);
|
|
117
|
+
named = true;
|
|
118
|
+
return r;
|
|
119
|
+
};
|
|
120
|
+
const appHere = () => {
|
|
121
|
+
const app = text(opts.app) || appOfFolder('.');
|
|
122
|
+
if (!app) throw new ScavaError(`Which app? This folder is not online yet. Add --app APP (${CMD} apps lists them), or run this in the folder of the app.`);
|
|
123
|
+
return app;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
switch (cmd) {
|
|
127
|
+
// "setup" is the word on every page (07.11.2026). "deploy" (the first version on npm) and "publish" do the same.
|
|
128
|
+
case 'setup':
|
|
129
|
+
case 'deploy':
|
|
130
|
+
case 'publish': {
|
|
131
|
+
// Before anything else: is there an app here at all? Nobody signs in for nothing.
|
|
132
|
+
const folder = readFolder(pos[0] || '.');
|
|
133
|
+
const remembered = appOfFolder(folder.root);
|
|
134
|
+
const app = text(opts.app) || remembered || undefined;
|
|
135
|
+
const resolves = text(opts.resolves)?.split(',').map(Number).filter(Number.isInteger);
|
|
136
|
+
let asked;
|
|
137
|
+
try {
|
|
138
|
+
// No path of this computer leaves it: the answer is a one-time link for this upload.
|
|
139
|
+
asked = await tool('publish_app', { path: '.', app, name: text(opts.name), message: text(opts.message), resolves });
|
|
140
|
+
} catch (e) {
|
|
141
|
+
if (remembered && !text(opts.app)) e.message += `\nThis folder remembers the app ${remembered} (in the file .scava-app). If it belongs to another account, delete that file to start a new app.`;
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
const r = await uploadFolder(asked.data.link, folder.root, { server, folder });
|
|
145
|
+
for (const line of r.lines) console.log(line);
|
|
146
|
+
if (!opts['no-check'] && r.check_url) {
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log(describeRun(await testRun(r.check_url)));
|
|
149
|
+
}
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log('Next:');
|
|
152
|
+
console.log(` ${CMD} share someone@company.com "@company.com" let people open it`);
|
|
153
|
+
console.log(` ${CMD} requests change requests you accepted, ready to build`);
|
|
154
|
+
console.log(` ${CMD} docs what an app can use on Scava: data, rules, files, server code`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
case 'share': {
|
|
158
|
+
const r = await tool('share_app', { app: appHere(), add: pos, remove: opts.remove?.filter((x) => typeof x === 'string') });
|
|
159
|
+
console.log(r.text);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
case 'apps':
|
|
163
|
+
console.log((await tool('list_apps', {})).text);
|
|
164
|
+
return;
|
|
165
|
+
case 'requests': {
|
|
166
|
+
if (pos[0] === 'done') {
|
|
167
|
+
const id = Number(pos[1]);
|
|
168
|
+
if (!Number.isInteger(id)) throw new ScavaError(`Usage: ${CMD} requests done ID [--note TEXT]`);
|
|
169
|
+
console.log((await tool('update_change_request', { id, status: 'done', note: text(opts.note) })).text);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
console.log((await tool('list_change_requests', { app: text(opts.app), status: opts.done ? 'done' : 'accepted' })).text);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
case 'server':
|
|
176
|
+
console.log((await tool('get_server_info', { app: appHere() })).text);
|
|
177
|
+
return;
|
|
178
|
+
case 'test': {
|
|
179
|
+
const link = isLink(pos[0]) ? pos[0] : (await tool('get_app_problems', { app: pos[0] || appHere(), test_run: true })).data.link;
|
|
180
|
+
const r = await testLink(link, { server });
|
|
181
|
+
console.log(describeRun(await testRun(r.url)));
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(r.problems);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
case 'pull': {
|
|
187
|
+
const [first, folder] = pos;
|
|
188
|
+
if (!first) throw new ScavaError(`Usage: ${CMD} pull APP [folder]. ${CMD} apps lists your apps.`);
|
|
189
|
+
const link = isLink(first) ? first : (await tool('pull_app', { app: first, path: '.' })).data.link;
|
|
190
|
+
const r = await pullLink(link, folder, { server });
|
|
191
|
+
console.log(`Pulled "${r.name}" v${r.version} (${r.fileCount} file${r.fileCount === 1 ? '' : 's'}) into ${r.dir}`);
|
|
192
|
+
console.log(`Change the files there, then run ${CMD} setup in that folder: it goes to the same app.`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
case 'upload': {
|
|
196
|
+
const [folder, link] = pos;
|
|
197
|
+
if (!folder || !link) throw new ScavaError(`Usage: ${CMD} upload <folder> <link>. Your AI gets the link from publish_app. Without a link: ${CMD} setup`);
|
|
198
|
+
const r = await uploadFolder(link, folder, { server });
|
|
199
|
+
for (const line of r.lines) console.log(line);
|
|
200
|
+
if (opts['no-check'] || !r.check_url) return;
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(describeRun(await testRun(r.check_url)));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
case 'login': {
|
|
206
|
+
if (typeof opts.paste === 'string') {
|
|
207
|
+
await finishWithAddress(server, opts.paste);
|
|
208
|
+
const me = await whoAmI(server);
|
|
209
|
+
console.log(`Signed in to Scava as ${me?.account ?? 'your account'}. The access stays on this computer, in ${credentialsPath()}.`);
|
|
210
|
+
console.log(`Next: ${CMD} setup`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const me = await whoAmI(server);
|
|
214
|
+
if (me) {
|
|
215
|
+
console.log(`Already signed in to Scava as ${me.account ?? 'your account'}. To use another account: ${CMD} logout`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
await signIn(server, { ...signInOptions, reopen: true });
|
|
219
|
+
console.log(`Next: ${CMD} setup`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
case 'whoami': {
|
|
223
|
+
const me = await whoAmI(server);
|
|
224
|
+
if (!me) {
|
|
225
|
+
console.log(`Not signed in to Scava at ${server}. The next command that needs your account opens the browser.`);
|
|
226
|
+
process.exitCode = 1;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
console.log(`Signed in to Scava as ${me.account ?? 'your account'} (${server}). The access is in ${credentialsPath()}. End it with: ${CMD} logout`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
case 'logout': {
|
|
233
|
+
const r = await logout(server);
|
|
234
|
+
if (!r.was) console.log('This computer was not signed in.');
|
|
235
|
+
else if (r.reached) console.log(`Signed out ${r.account ?? 'your account'} on this computer. The connection at Scava has ended too.`);
|
|
236
|
+
else console.log(`Removed the access of ${r.account ?? 'your account'} from this computer. Scava could not be reached, so disconnect it on the Connect page as well: ${server}/#/connect`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
case 'secret': {
|
|
240
|
+
const [action, name] = pos;
|
|
241
|
+
if (action !== 'set') throw new ScavaError(`Usage: ${CMD} secret set NAME [--host api.example.com] [--app APP]`);
|
|
242
|
+
const slug = text(opts.app) || appOfFolder('.');
|
|
243
|
+
const page = `${server}/#/${slug ? `apps/${encodeURIComponent(slug)}` : ''}`;
|
|
244
|
+
console.log('Keys are set in the browser, so the value never passes through a terminal an AI can read.');
|
|
245
|
+
console.log(`Open ${page} and go to "Server code"${slug ? '' : ' of your app'}.`);
|
|
246
|
+
if (name) console.log(`Name: ${name}${text(opts.host) ? `, only for: ${opts.host}` : ''}`);
|
|
247
|
+
if (!opts['no-browser']) openBrowser(page);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
case 'dev': {
|
|
251
|
+
let dev;
|
|
252
|
+
try {
|
|
253
|
+
({ dev } = await import('./dev.js'));
|
|
254
|
+
} catch (e) {
|
|
255
|
+
if (e?.code !== 'ERR_MODULE_NOT_FOUND') throw e;
|
|
256
|
+
throw new ScavaError('scava dev is not part of the scava package. Test the app online instead: setup runs a test run.');
|
|
257
|
+
}
|
|
258
|
+
await dev({ dir: pos[0] || '.', port: Number(opts.port) || 8788, browser: !opts['no-browser'], email: text(opts.email) });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
case 'docs': {
|
|
262
|
+
const r = await fetch(`${server}/llms.txt`).catch(() => null);
|
|
263
|
+
if (!r?.ok) throw new ScavaError(`Cannot reach Scava at ${server}`);
|
|
264
|
+
console.log(await r.text());
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
case undefined:
|
|
268
|
+
case 'help':
|
|
269
|
+
case '--help':
|
|
270
|
+
case '-h':
|
|
271
|
+
console.log(HELP);
|
|
272
|
+
return;
|
|
273
|
+
case '--version':
|
|
274
|
+
case '-v':
|
|
275
|
+
console.log(JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version);
|
|
276
|
+
return;
|
|
277
|
+
default:
|
|
278
|
+
console.error(`Unknown command: ${cmd}\n`);
|
|
279
|
+
console.log(HELP);
|
|
280
|
+
process.exitCode = 1;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
main().catch((e) => {
|
|
285
|
+
console.error(e instanceof ScavaError ? formatProblems(e) : e.message);
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scava-ai",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Hosting, login and a database for the internal tools your AI agent builds, with sharing and change requests. One command: npx -y scava-ai
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Hosting, login and a database for the internal tools your AI agent builds, with sharing and change requests. One command: npx -y scava-ai setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"scava": "cli/scava.js"
|