@moda-ai/cli 1.1.1 → 1.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 +36 -0
- package/dist/agent.d.ts +5 -0
- package/dist/agent.js +477 -0
- package/dist/analyst-tui.d.ts +81 -0
- package/dist/analyst-tui.js +314 -0
- package/dist/api-client.d.ts +4 -0
- package/dist/api-client.js +20 -3
- package/dist/auth.d.ts +22 -3
- package/dist/auth.js +230 -66
- package/dist/cli.d.ts +20 -3
- package/dist/cli.js +768 -93
- package/dist/command-context.d.ts +34 -0
- package/dist/command-context.js +38 -0
- package/dist/command-registry.d.ts +22 -0
- package/dist/command-registry.js +15 -0
- package/dist/config.d.ts +40 -1
- package/dist/config.js +70 -3
- package/dist/doctor.d.ts +79 -0
- package/dist/doctor.js +1102 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +9 -0
- package/dist/harness-report.d.ts +138 -0
- package/dist/harness-report.js +1464 -0
- package/dist/harness.d.ts +233 -0
- package/dist/harness.js +3264 -0
- package/dist/init/index.d.ts +30 -1
- package/dist/init/index.js +383 -23
- package/dist/init/rules.d.ts +5 -1
- package/dist/init/rules.js +22 -16
- package/dist/init/sdk-guide.d.ts +3 -0
- package/dist/init/sdk-guide.js +397 -0
- package/dist/init/tenant.js +12 -3
- package/dist/keys.d.ts +47 -0
- package/dist/keys.js +136 -0
- package/dist/output.d.ts +118 -0
- package/dist/output.js +228 -0
- package/dist/production-intelligence.d.ts +134 -0
- package/dist/production-intelligence.js +1113 -0
- package/dist/profile-commands.d.ts +10 -0
- package/dist/profile-commands.js +252 -0
- package/dist/profiles.d.ts +90 -0
- package/dist/profiles.js +432 -0
- package/dist/prompts.d.ts +6 -0
- package/dist/prompts.js +368 -0
- package/dist/schemas.d.ts +4 -4
- package/dist/skills.d.ts +25 -0
- package/dist/skills.js +468 -0
- package/dist/sync.d.ts +5 -0
- package/dist/sync.js +27 -0
- package/dist/telemetry.d.ts +18 -0
- package/dist/telemetry.js +63 -0
- package/dist/tui.d.ts +40 -0
- package/dist/tui.js +387 -0
- package/dist/workspace.d.ts +12 -0
- package/dist/workspace.js +1294 -0
- package/hooks/moda-snapshot.sh +41 -0
- package/package.json +10 -7
- package/skills/moda-cli/SKILL.md +48 -4
- package/skills/integrate-moda/SKILL.md +0 -63
package/dist/auth.js
CHANGED
|
@@ -1,40 +1,160 @@
|
|
|
1
1
|
// src/auth.ts — Browser-based authentication for moda init via frontend
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
5
|
+
import { resolveModaBaseUrl } from './config.js';
|
|
6
|
+
import { getModaPaths, loadProfileConfig, saveProfileConfig, secretFilePathFromRef, writeSecretJsonFile, } from './profiles.js';
|
|
7
7
|
const CALLBACK_PORT = 8432;
|
|
8
8
|
const AUTH_TIMEOUT_MS = 120_000;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
function escapeHtml(value) {
|
|
10
|
+
return value
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
function safeEqual(a, b) {
|
|
18
|
+
const aBuf = Buffer.from(a);
|
|
19
|
+
const bBuf = Buffer.from(b);
|
|
20
|
+
if (aBuf.length !== bBuf.length)
|
|
21
|
+
return false;
|
|
22
|
+
return timingSafeEqual(aBuf, bBuf);
|
|
23
|
+
}
|
|
24
|
+
function tokenPath(options = {}) {
|
|
25
|
+
return getModaPaths(options).legacyAuthPath;
|
|
26
|
+
}
|
|
27
|
+
function authTokenRefForProfile(profile) {
|
|
28
|
+
return `file:secrets/auth/${profile}.json`;
|
|
29
|
+
}
|
|
30
|
+
function parseStoredAuth(value) {
|
|
31
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const data = value;
|
|
35
|
+
if (typeof data.access_token !== 'string' ||
|
|
36
|
+
typeof data.expires_at !== 'number') {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
access_token: data.access_token,
|
|
41
|
+
expires_at: data.expires_at,
|
|
42
|
+
profile: typeof data.profile === 'string' ? data.profile : undefined,
|
|
43
|
+
token_type: data.token_type === 'bearer' ? 'bearer' : undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function isAuthSessionValid(data, now = Date.now()) {
|
|
47
|
+
if (!data)
|
|
48
|
+
return false;
|
|
49
|
+
return now / 1000 <= data.expires_at - 60;
|
|
50
|
+
}
|
|
51
|
+
function readStoredAuth(path) {
|
|
12
52
|
try {
|
|
13
|
-
if (!existsSync(
|
|
53
|
+
if (!existsSync(path))
|
|
14
54
|
return null;
|
|
15
|
-
|
|
16
|
-
if (data.expires_at && Date.now() / 1000 > data.expires_at - 60) {
|
|
17
|
-
return null; // Expired or about to expire
|
|
18
|
-
}
|
|
19
|
-
return data.access_token || null;
|
|
55
|
+
return parseStoredAuth(JSON.parse(readFileSync(path, 'utf8')));
|
|
20
56
|
}
|
|
21
57
|
catch {
|
|
22
58
|
return null;
|
|
23
59
|
}
|
|
24
60
|
}
|
|
25
|
-
function
|
|
26
|
-
|
|
61
|
+
export function loadToken(options = {}) {
|
|
62
|
+
return loadAuthSession(options)?.access_token ?? null;
|
|
63
|
+
}
|
|
64
|
+
export function loadAuthSession(options = {}) {
|
|
65
|
+
const profileSession = loadProfileAuthSession(options);
|
|
66
|
+
if (profileSession)
|
|
67
|
+
return profileSession;
|
|
68
|
+
const legacy = readStoredAuth(tokenPath(options));
|
|
69
|
+
if (!legacy)
|
|
70
|
+
return null;
|
|
71
|
+
if (!options.includeExpired && !isAuthSessionValid(legacy)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return legacy;
|
|
75
|
+
}
|
|
76
|
+
function loadProfileAuthSession(options) {
|
|
77
|
+
const profileName = options.profile ?? (options.env ?? process.env).MODA_PROFILE;
|
|
78
|
+
if (!profileName)
|
|
79
|
+
return null;
|
|
80
|
+
try {
|
|
81
|
+
const profile = loadProfileConfig(options).profiles[profileName];
|
|
82
|
+
const tokenRef = profile?.auth_token_ref;
|
|
83
|
+
if (!tokenRef)
|
|
84
|
+
return null;
|
|
85
|
+
const path = secretFilePathFromRef(tokenRef, options);
|
|
86
|
+
if (!path)
|
|
87
|
+
return null;
|
|
88
|
+
const session = readStoredAuth(path);
|
|
89
|
+
if (!session)
|
|
90
|
+
return null;
|
|
91
|
+
if (!options.includeExpired && !isAuthSessionValid(session))
|
|
92
|
+
return null;
|
|
93
|
+
return session;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function saveAuthSession(accessToken, expiresIn, options = {}) {
|
|
100
|
+
const profile = options.profile ?? (options.env ?? process.env).MODA_PROFILE;
|
|
27
101
|
const data = {
|
|
28
102
|
access_token: accessToken,
|
|
29
103
|
expires_at: Math.floor(Date.now() / 1000) + expiresIn,
|
|
104
|
+
profile,
|
|
105
|
+
token_type: 'bearer',
|
|
30
106
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
107
|
+
if (profile) {
|
|
108
|
+
const ref = authTokenRefForProfile(profile);
|
|
109
|
+
const path = secretFilePathFromRef(ref, options);
|
|
110
|
+
if (!path)
|
|
111
|
+
throw new Error(`Unsupported auth token reference: ${ref}`);
|
|
112
|
+
const config = loadProfileConfig(options);
|
|
113
|
+
const existing = config.profiles[profile];
|
|
114
|
+
if (!existing) {
|
|
115
|
+
throw new Error(`Moda profile '${profile}' does not exist.`);
|
|
116
|
+
}
|
|
117
|
+
writeSecretJsonFile(path, data);
|
|
118
|
+
saveProfileConfig({
|
|
119
|
+
...config,
|
|
120
|
+
profiles: {
|
|
121
|
+
...config.profiles,
|
|
122
|
+
[profile]: {
|
|
123
|
+
...existing,
|
|
124
|
+
auth_token_ref: ref,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}, options);
|
|
128
|
+
return data;
|
|
34
129
|
}
|
|
35
|
-
|
|
36
|
-
|
|
130
|
+
writeSecretJsonFile(tokenPath(options), data);
|
|
131
|
+
return data;
|
|
132
|
+
}
|
|
133
|
+
export function clearAuthSession(options = {}) {
|
|
134
|
+
const profile = options.profile ?? (options.env ?? process.env).MODA_PROFILE;
|
|
135
|
+
if (profile) {
|
|
136
|
+
const ref = authTokenRefForProfile(profile);
|
|
137
|
+
const path = secretFilePathFromRef(ref, options);
|
|
138
|
+
if (path)
|
|
139
|
+
rmSync(path, { force: true });
|
|
140
|
+
const config = loadProfileConfig(options);
|
|
141
|
+
const existing = config.profiles[profile];
|
|
142
|
+
if (existing) {
|
|
143
|
+
const { auth_token_ref: _drop, ...profileWithoutToken } = existing;
|
|
144
|
+
saveProfileConfig({
|
|
145
|
+
...config,
|
|
146
|
+
profiles: {
|
|
147
|
+
...config.profiles,
|
|
148
|
+
[profile]: profileWithoutToken,
|
|
149
|
+
},
|
|
150
|
+
}, options);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
37
153
|
}
|
|
154
|
+
rmSync(tokenPath(options), { force: true });
|
|
155
|
+
}
|
|
156
|
+
function saveToken(accessToken, expiresIn) {
|
|
157
|
+
saveAuthSession(accessToken, expiresIn);
|
|
38
158
|
}
|
|
39
159
|
// --- Login Flow ---
|
|
40
160
|
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
@@ -58,97 +178,141 @@ if (h) {
|
|
|
58
178
|
}
|
|
59
179
|
</script>
|
|
60
180
|
</body></html>`;
|
|
61
|
-
|
|
181
|
+
async function openAuthUrl(authUrl) {
|
|
182
|
+
const open = (await import('open')).default;
|
|
183
|
+
await open(authUrl);
|
|
184
|
+
}
|
|
185
|
+
function closeAuthServer(server) {
|
|
186
|
+
server.close((err) => {
|
|
187
|
+
if (err && err.code !== 'ERR_SERVER_NOT_RUNNING') {
|
|
188
|
+
// Nothing useful to surface to users during auth teardown.
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
// Node 18 keeps idle keep-alive sockets open after server.close(). The browser
|
|
192
|
+
// auth callback is single-use, so close idle sockets aggressively.
|
|
193
|
+
server.closeIdleConnections?.();
|
|
194
|
+
}
|
|
195
|
+
export async function login(screen = 'login', options = {}) {
|
|
62
196
|
return new Promise((resolve, reject) => {
|
|
63
197
|
let settled = false;
|
|
198
|
+
let localPort = options.callbackPort ?? CALLBACK_PORT;
|
|
199
|
+
let timeout;
|
|
200
|
+
const state = randomBytes(16).toString('hex');
|
|
64
201
|
const server = createServer(async (req, res) => {
|
|
65
202
|
if (settled)
|
|
66
203
|
return;
|
|
67
|
-
const url = new URL(req.url || '/', `http://localhost:${
|
|
204
|
+
const url = new URL(req.url || '/', `http://localhost:${localPort}`);
|
|
68
205
|
// Serve fragment extractor at /callback — the frontend redirects here
|
|
69
206
|
// with token in the URL fragment (#token=...) which never reaches the server.
|
|
70
207
|
// This page extracts it client-side and forwards to /callback-token as query params.
|
|
71
208
|
if (url.pathname === '/callback') {
|
|
72
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
209
|
+
res.writeHead(200, { 'Content-Type': 'text/html', Connection: 'close' });
|
|
73
210
|
res.end(FRAGMENT_EXTRACTOR_HTML);
|
|
74
211
|
return;
|
|
75
212
|
}
|
|
76
213
|
if (url.pathname !== '/callback-token') {
|
|
77
|
-
res.writeHead(404);
|
|
214
|
+
res.writeHead(404, { Connection: 'close' });
|
|
78
215
|
res.end('Not found');
|
|
79
216
|
return;
|
|
80
217
|
}
|
|
81
218
|
const token = url.searchParams.get('token');
|
|
82
219
|
const expiresIn = url.searchParams.get('expires_in');
|
|
83
220
|
const error = url.searchParams.get('error');
|
|
221
|
+
const callbackState = url.searchParams.get('state');
|
|
222
|
+
// Bind the callback to this login session: any page in the browser (or
|
|
223
|
+
// any process on localhost) could POST to this endpoint, so a missing or
|
|
224
|
+
// mismatched state value means we must reject before touching `token`.
|
|
225
|
+
if (!callbackState || !safeEqual(callbackState, state)) {
|
|
226
|
+
res.writeHead(400, { 'Content-Type': 'text/html', Connection: 'close' });
|
|
227
|
+
res.end('<html><body><h2>Error</h2><p>Invalid authentication state.</p></body></html>', () => {
|
|
228
|
+
fail(new Error('Invalid authentication state.'));
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
84
232
|
if (error) {
|
|
85
|
-
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
86
|
-
res.end(`<html><body><h2>Error</h2><p>${error}</p></body></html
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
reject(new Error(`Authentication error: ${error}`));
|
|
233
|
+
res.writeHead(400, { 'Content-Type': 'text/html', Connection: 'close' });
|
|
234
|
+
res.end(`<html><body><h2>Error</h2><p>${escapeHtml(error)}</p></body></html>`, () => {
|
|
235
|
+
fail(new Error(`Authentication error: ${error}`));
|
|
236
|
+
});
|
|
90
237
|
return;
|
|
91
238
|
}
|
|
92
239
|
if (!token) {
|
|
93
|
-
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
94
|
-
res.end('<html><body><h2>Error</h2><p>No token received.</p></body></html>')
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
reject(new Error('No token received'));
|
|
240
|
+
res.writeHead(400, { 'Content-Type': 'text/html', Connection: 'close' });
|
|
241
|
+
res.end('<html><body><h2>Error</h2><p>No token received.</p></body></html>', () => {
|
|
242
|
+
fail(new Error('No token received'));
|
|
243
|
+
});
|
|
98
244
|
return;
|
|
99
245
|
}
|
|
100
246
|
saveToken(token, parseInt(expiresIn || '86400', 10));
|
|
101
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
102
|
-
res.end(SUCCESS_HTML)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
resolve(token);
|
|
247
|
+
res.writeHead(200, { 'Content-Type': 'text/html', Connection: 'close' });
|
|
248
|
+
res.end(SUCCESS_HTML, () => {
|
|
249
|
+
succeed(token);
|
|
250
|
+
});
|
|
106
251
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
252
|
+
function removeSignalHandlers() {
|
|
253
|
+
process.off('SIGINT', handleSigint);
|
|
254
|
+
process.off('SIGTERM', handleSigterm);
|
|
255
|
+
}
|
|
256
|
+
function finish(callback) {
|
|
257
|
+
if (settled)
|
|
258
|
+
return;
|
|
259
|
+
settled = true;
|
|
260
|
+
if (timeout)
|
|
261
|
+
clearTimeout(timeout);
|
|
262
|
+
removeSignalHandlers();
|
|
263
|
+
// Always attempt to close the server: if SIGINT arrives between
|
|
264
|
+
// server.listen() and its listening callback, the socket may already be
|
|
265
|
+
// bound by the OS. closeAuthServer ignores ERR_SERVER_NOT_RUNNING.
|
|
266
|
+
closeAuthServer(server);
|
|
267
|
+
callback();
|
|
268
|
+
}
|
|
269
|
+
function succeed(token) {
|
|
270
|
+
finish(() => resolve(token));
|
|
271
|
+
}
|
|
272
|
+
function fail(error) {
|
|
273
|
+
finish(() => reject(error));
|
|
274
|
+
}
|
|
275
|
+
function handleSigint() {
|
|
276
|
+
fail(new Error('Authentication cancelled.'));
|
|
277
|
+
}
|
|
278
|
+
function handleSigterm() {
|
|
279
|
+
fail(new Error('Authentication terminated.'));
|
|
280
|
+
}
|
|
281
|
+
timeout = setTimeout(() => {
|
|
282
|
+
fail(new Error('Authentication timed out. Please try again.'));
|
|
283
|
+
}, options.timeoutMs ?? AUTH_TIMEOUT_MS);
|
|
116
284
|
server.on('error', (err) => {
|
|
117
285
|
if (err.code === 'EADDRINUSE') {
|
|
118
|
-
|
|
286
|
+
fail(new Error(`Port ${localPort} is in use. Close the process using it and try again.`));
|
|
119
287
|
}
|
|
120
288
|
else {
|
|
121
|
-
|
|
289
|
+
fail(err);
|
|
122
290
|
}
|
|
123
291
|
});
|
|
124
|
-
server.listen(
|
|
125
|
-
const
|
|
292
|
+
server.listen(localPort, '127.0.0.1', async () => {
|
|
293
|
+
const address = server.address();
|
|
294
|
+
if (address && typeof address === 'object') {
|
|
295
|
+
localPort = address.port;
|
|
296
|
+
}
|
|
297
|
+
const authUrl = `${resolveModaBaseUrl()}/auth/cli?port=${localPort}&screen=${screen}&state=${state}`;
|
|
126
298
|
try {
|
|
127
|
-
|
|
128
|
-
await open(authUrl);
|
|
299
|
+
await (options.openUrl ?? openAuthUrl)(authUrl);
|
|
129
300
|
}
|
|
130
301
|
catch {
|
|
131
302
|
console.error(` Open this URL in your browser:\n ${authUrl}`);
|
|
132
303
|
}
|
|
133
304
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (!settled) {
|
|
137
|
-
settled = true;
|
|
138
|
-
server.close();
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
process.on('SIGINT', cleanup);
|
|
142
|
-
process.on('SIGTERM', cleanup);
|
|
305
|
+
process.once('SIGINT', handleSigint);
|
|
306
|
+
process.once('SIGTERM', handleSigterm);
|
|
143
307
|
});
|
|
144
308
|
}
|
|
145
309
|
// --- Authenticated Fetch ---
|
|
146
|
-
export async function authFetch(path, options = {}) {
|
|
147
|
-
const token = loadToken();
|
|
310
|
+
export async function authFetch(path, options = {}, authOptions = {}) {
|
|
311
|
+
const token = loadToken(authOptions);
|
|
148
312
|
if (!token) {
|
|
149
313
|
throw new Error('Not authenticated. Run "moda init" to log in.');
|
|
150
314
|
}
|
|
151
|
-
const url = `${
|
|
315
|
+
const url = `${resolveModaBaseUrl()}${path}`;
|
|
152
316
|
const headers = new Headers(options.headers);
|
|
153
317
|
headers.set('Authorization', `Bearer ${token}`);
|
|
154
318
|
if (!headers.has('Content-Type') && options.body) {
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import { type TelemetryErrorType } from './telemetry.js';
|
|
3
|
+
import { type CommandContext } from './command-context.js';
|
|
4
|
+
import { type CommandDefinition } from './command-registry.js';
|
|
5
|
+
export { CliInputError } from './errors.js';
|
|
6
|
+
export interface ParsedArgs {
|
|
3
7
|
command: string;
|
|
4
8
|
positional: string | undefined;
|
|
9
|
+
positionals: string[];
|
|
5
10
|
flags: Record<string, string>;
|
|
6
11
|
}
|
|
7
12
|
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
8
13
|
export declare function flagsToArgs(flags: Record<string, string>, positionalKey?: string, positionalValue?: string): Record<string, unknown>;
|
|
9
|
-
export declare function runCommand(command: string, positional: string | undefined, flags: Record<string, string
|
|
10
|
-
|
|
14
|
+
export declare function runCommand(command: string, positional: string | undefined, flags: Record<string, string>, positionals?: string[], context?: CommandContext): Promise<void>;
|
|
15
|
+
interface CommandDispatchResult {
|
|
16
|
+
exitCode: number;
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
}
|
|
19
|
+
interface CliCommandDefinition extends CommandDefinition<CommandContext, CommandDispatchResult> {
|
|
20
|
+
validateConfigBeforeRun: boolean;
|
|
21
|
+
resetApiRequestCountBeforeRun: boolean;
|
|
22
|
+
telemetry: 'none' | 'result' | 'result-and-error';
|
|
23
|
+
}
|
|
24
|
+
export declare const commandRegistry: import("./command-registry.js").CommandRegistry<CommandContext, CommandDispatchResult, CliCommandDefinition>;
|
|
25
|
+
export declare function classifyCliError(error: unknown): TelemetryErrorType;
|
|
26
|
+
export declare function printCliError(error: unknown): void;
|
|
27
|
+
export declare function dispatchParsedCommand(parsed: ParsedArgs, startedAt?: number): Promise<number>;
|
|
11
28
|
//# sourceMappingURL=cli.d.ts.map
|