@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.
Files changed (59) hide show
  1. package/README.md +36 -0
  2. package/dist/agent.d.ts +5 -0
  3. package/dist/agent.js +477 -0
  4. package/dist/analyst-tui.d.ts +81 -0
  5. package/dist/analyst-tui.js +314 -0
  6. package/dist/api-client.d.ts +4 -0
  7. package/dist/api-client.js +20 -3
  8. package/dist/auth.d.ts +22 -3
  9. package/dist/auth.js +230 -66
  10. package/dist/cli.d.ts +20 -3
  11. package/dist/cli.js +768 -93
  12. package/dist/command-context.d.ts +34 -0
  13. package/dist/command-context.js +38 -0
  14. package/dist/command-registry.d.ts +22 -0
  15. package/dist/command-registry.js +15 -0
  16. package/dist/config.d.ts +40 -1
  17. package/dist/config.js +70 -3
  18. package/dist/doctor.d.ts +79 -0
  19. package/dist/doctor.js +1102 -0
  20. package/dist/errors.d.ts +5 -0
  21. package/dist/errors.js +9 -0
  22. package/dist/harness-report.d.ts +138 -0
  23. package/dist/harness-report.js +1464 -0
  24. package/dist/harness.d.ts +233 -0
  25. package/dist/harness.js +3264 -0
  26. package/dist/init/index.d.ts +30 -1
  27. package/dist/init/index.js +383 -23
  28. package/dist/init/rules.d.ts +5 -1
  29. package/dist/init/rules.js +22 -16
  30. package/dist/init/sdk-guide.d.ts +3 -0
  31. package/dist/init/sdk-guide.js +397 -0
  32. package/dist/init/tenant.js +12 -3
  33. package/dist/keys.d.ts +47 -0
  34. package/dist/keys.js +136 -0
  35. package/dist/output.d.ts +118 -0
  36. package/dist/output.js +228 -0
  37. package/dist/production-intelligence.d.ts +134 -0
  38. package/dist/production-intelligence.js +1113 -0
  39. package/dist/profile-commands.d.ts +10 -0
  40. package/dist/profile-commands.js +252 -0
  41. package/dist/profiles.d.ts +90 -0
  42. package/dist/profiles.js +432 -0
  43. package/dist/prompts.d.ts +6 -0
  44. package/dist/prompts.js +368 -0
  45. package/dist/schemas.d.ts +4 -4
  46. package/dist/skills.d.ts +25 -0
  47. package/dist/skills.js +468 -0
  48. package/dist/sync.d.ts +5 -0
  49. package/dist/sync.js +27 -0
  50. package/dist/telemetry.d.ts +18 -0
  51. package/dist/telemetry.js +63 -0
  52. package/dist/tui.d.ts +40 -0
  53. package/dist/tui.js +387 -0
  54. package/dist/workspace.d.ts +12 -0
  55. package/dist/workspace.js +1294 -0
  56. package/hooks/moda-snapshot.sh +41 -0
  57. package/package.json +10 -7
  58. package/skills/moda-cli/SKILL.md +48 -4
  59. package/skills/integrate-moda/SKILL.md +0 -63
@@ -0,0 +1,432 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, isAbsolute, join, normalize, relative } from 'node:path';
4
+ export const PROFILE_CONFIG_VERSION = 1;
5
+ export const PROD_BASE_URL = 'https://www.moda.dev';
6
+ export const DEFAULT_INGEST_URL = 'https://moda-ingest.modas.workers.dev';
7
+ export const DEFAULT_PROFILE_NAME = 'prod';
8
+ function getEnv(options = {}) {
9
+ return options.env ?? process.env;
10
+ }
11
+ export function resolveModaHome(options = {}) {
12
+ const env = getEnv(options);
13
+ return options.home || env.HOME || env.USERPROFILE || homedir();
14
+ }
15
+ export function getModaPaths(options = {}) {
16
+ const home = resolveModaHome(options);
17
+ const dir = join(home, '.moda');
18
+ return {
19
+ home,
20
+ dir,
21
+ legacyConfigPath: join(dir, 'config.json'),
22
+ legacyAuthPath: join(dir, 'auth.json'),
23
+ profileConfigPath: join(dir, 'profiles.json'),
24
+ secretsDir: join(dir, 'secrets'),
25
+ };
26
+ }
27
+ export function getModaDir(options = {}) {
28
+ return getModaPaths(options).dir;
29
+ }
30
+ export function getLegacyConfigPath(options = {}) {
31
+ return getModaPaths(options).legacyConfigPath;
32
+ }
33
+ function isRecord(value) {
34
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
35
+ }
36
+ function optionalString(value) {
37
+ return typeof value === 'string' && value.trim() ? value : undefined;
38
+ }
39
+ function readJsonFile(path) {
40
+ return JSON.parse(readFileSync(path, 'utf8'));
41
+ }
42
+ function safeReadRecord(path) {
43
+ try {
44
+ if (!existsSync(path))
45
+ return undefined;
46
+ const parsed = readJsonFile(path);
47
+ return isRecord(parsed) ? parsed : undefined;
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ export function loadLegacyCliConfig(options = {}) {
54
+ const parsed = safeReadRecord(getModaPaths(options).legacyConfigPath);
55
+ if (!parsed)
56
+ return {};
57
+ return {
58
+ api_key: optionalString(parsed.api_key),
59
+ ingest_url: optionalString(parsed.ingest_url),
60
+ tenant_id: optionalString(parsed.tenant_id),
61
+ };
62
+ }
63
+ export function writeLegacyCliConfig(update, options = {}) {
64
+ const paths = getModaPaths(options);
65
+ mkdirSync(paths.dir, { recursive: true });
66
+ const merged = { ...loadLegacyCliConfig(options), ...update };
67
+ writeSecretJsonFile(paths.legacyConfigPath, merged);
68
+ return merged;
69
+ }
70
+ function profileDefaults(name, environment, now) {
71
+ const local = environment === 'local';
72
+ const staging = environment === 'staging';
73
+ return {
74
+ name,
75
+ environment,
76
+ base_url: local
77
+ ? 'http://localhost:3000'
78
+ : staging
79
+ ? 'https://staging.moda.dev'
80
+ : PROD_BASE_URL,
81
+ ingest_url: local ? 'http://localhost:8787' : DEFAULT_INGEST_URL,
82
+ metadata: {
83
+ created_at: now.toISOString(),
84
+ source: 'default',
85
+ },
86
+ };
87
+ }
88
+ export function createDefaultProfiles(options = {}) {
89
+ const now = options.now ?? new Date();
90
+ return {
91
+ local: profileDefaults('local', 'local', now),
92
+ staging: profileDefaults('staging', 'staging', now),
93
+ prod: profileDefaults('prod', 'prod', now),
94
+ };
95
+ }
96
+ function normalizeEnvironment(value) {
97
+ if (value === 'local' ||
98
+ value === 'dev' ||
99
+ value === 'staging' ||
100
+ value === 'prod') {
101
+ return value;
102
+ }
103
+ return 'prod';
104
+ }
105
+ function isSecretReference(value) {
106
+ return (value.startsWith('env:') ||
107
+ value.startsWith('file:') ||
108
+ value === 'legacy:config.api_key' ||
109
+ value === 'legacy:auth.access_token');
110
+ }
111
+ function normalizeSecretReference(value) {
112
+ if (typeof value !== 'string')
113
+ return undefined;
114
+ return isSecretReference(value) ? value : undefined;
115
+ }
116
+ export function assertValidProfileName(name) {
117
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)) {
118
+ throw new Error(`Invalid profile name '${name}'. Use letters, numbers, dots, dashes, or underscores.`);
119
+ }
120
+ }
121
+ function normalizeProfile(fallbackName, value, options) {
122
+ if (!isRecord(value))
123
+ return undefined;
124
+ const name = optionalString(value.name) ?? fallbackName;
125
+ try {
126
+ assertValidProfileName(name);
127
+ }
128
+ catch {
129
+ return undefined;
130
+ }
131
+ const defaults = profileDefaults(name, normalizeEnvironment(value.environment), options.now ?? new Date());
132
+ return {
133
+ ...defaults,
134
+ base_url: optionalString(value.base_url) ?? defaults.base_url,
135
+ ingest_url: optionalString(value.ingest_url) ?? defaults.ingest_url,
136
+ tenant_id: optionalString(value.tenant_id),
137
+ workspace_id: optionalString(value.workspace_id),
138
+ api_key_ref: normalizeSecretReference(value.api_key_ref),
139
+ auth_token_ref: normalizeSecretReference(value.auth_token_ref),
140
+ metadata: isRecord(value.metadata) ? value.metadata : defaults.metadata,
141
+ };
142
+ }
143
+ function inspectLegacyAuth(path) {
144
+ const parsed = safeReadRecord(path);
145
+ const expiresAt = typeof parsed?.expires_at === 'number' ? parsed.expires_at : undefined;
146
+ return {
147
+ path,
148
+ present: Boolean(parsed),
149
+ has_access_token: typeof parsed?.access_token === 'string',
150
+ expires_at: expiresAt,
151
+ expired: expiresAt === undefined ? undefined : Date.now() / 1000 > expiresAt - 60,
152
+ };
153
+ }
154
+ export function inspectProfileMigrationState(options = {}) {
155
+ const paths = getModaPaths(options);
156
+ const legacyConfig = loadLegacyCliConfig(options);
157
+ return {
158
+ legacy_config: {
159
+ path: paths.legacyConfigPath,
160
+ present: existsSync(paths.legacyConfigPath),
161
+ has_api_key: Boolean(legacyConfig.api_key),
162
+ has_ingest_url: Boolean(legacyConfig.ingest_url),
163
+ has_tenant_id: Boolean(legacyConfig.tenant_id),
164
+ },
165
+ legacy_auth: inspectLegacyAuth(paths.legacyAuthPath),
166
+ };
167
+ }
168
+ function applyLegacyAwareness(config, options) {
169
+ const legacy = loadLegacyCliConfig(options);
170
+ const migration = inspectProfileMigrationState(options);
171
+ const prod = config.profiles.prod;
172
+ if (prod && (legacy.api_key || legacy.ingest_url || legacy.tenant_id)) {
173
+ config.profiles.prod = {
174
+ ...prod,
175
+ ingest_url: legacy.ingest_url ?? prod.ingest_url,
176
+ tenant_id: legacy.tenant_id ?? prod.tenant_id,
177
+ api_key_ref: legacy.api_key ? 'legacy:config.api_key' : prod.api_key_ref,
178
+ auth_token_ref: migration.legacy_auth.has_access_token
179
+ ? 'legacy:auth.access_token'
180
+ : prod.auth_token_ref,
181
+ metadata: {
182
+ ...prod.metadata,
183
+ source: 'migration',
184
+ legacy_config_path: migration.legacy_config.path,
185
+ legacy_auth_path: migration.legacy_auth.path,
186
+ },
187
+ };
188
+ }
189
+ return {
190
+ ...config,
191
+ migrations: migration,
192
+ };
193
+ }
194
+ function normalizeProfileConfig(raw, options = {}) {
195
+ const profiles = createDefaultProfiles(options);
196
+ let activeProfile = DEFAULT_PROFILE_NAME;
197
+ if (isRecord(raw)) {
198
+ if (isRecord(raw.profiles)) {
199
+ for (const [name, profile] of Object.entries(raw.profiles)) {
200
+ const normalized = normalizeProfile(name, profile, options);
201
+ if (normalized)
202
+ profiles[normalized.name] = normalized;
203
+ }
204
+ }
205
+ const configuredActive = optionalString(raw.active_profile);
206
+ if (configuredActive && profiles[configuredActive]) {
207
+ activeProfile = configuredActive;
208
+ }
209
+ }
210
+ return applyLegacyAwareness({
211
+ version: PROFILE_CONFIG_VERSION,
212
+ active_profile: activeProfile,
213
+ profiles,
214
+ migrations: inspectProfileMigrationState(options),
215
+ }, options);
216
+ }
217
+ export function loadProfileConfig(options = {}) {
218
+ const path = getModaPaths(options).profileConfigPath;
219
+ const parsed = safeReadRecord(path);
220
+ return normalizeProfileConfig(parsed, options);
221
+ }
222
+ export function saveProfileConfig(config, options = {}) {
223
+ const active = config.active_profile || DEFAULT_PROFILE_NAME;
224
+ if (!config.profiles[active]) {
225
+ throw new Error(`Cannot activate missing profile '${active}'.`);
226
+ }
227
+ const path = getModaPaths(options).profileConfigPath;
228
+ writeSecretJsonFile(path, {
229
+ version: PROFILE_CONFIG_VERSION,
230
+ active_profile: active,
231
+ profiles: config.profiles,
232
+ migrations: inspectProfileMigrationState(options),
233
+ });
234
+ }
235
+ export function getActiveProfile(options = {}) {
236
+ const config = loadProfileConfig(options);
237
+ const envProfile = optionalString(getEnv(options).MODA_PROFILE);
238
+ const requested = options.profile || envProfile || config.active_profile;
239
+ const profile = config.profiles[requested];
240
+ if (!profile) {
241
+ throw new Error(`Moda profile '${requested}' does not exist.`);
242
+ }
243
+ return profile;
244
+ }
245
+ export function setActiveProfile(name, options = {}) {
246
+ assertValidProfileName(name);
247
+ const config = loadProfileConfig(options);
248
+ if (!config.profiles[name]) {
249
+ throw new Error(`Moda profile '${name}' does not exist.`);
250
+ }
251
+ const next = { ...config, active_profile: name };
252
+ saveProfileConfig(next, options);
253
+ return next;
254
+ }
255
+ export function upsertProfile(profile, options = {}) {
256
+ assertValidProfileName(profile.name);
257
+ const config = loadProfileConfig(options);
258
+ const next = {
259
+ ...config,
260
+ profiles: {
261
+ ...config.profiles,
262
+ [profile.name]: {
263
+ ...profile,
264
+ metadata: {
265
+ ...profile.metadata,
266
+ updated_at: (options.now ?? new Date()).toISOString(),
267
+ },
268
+ },
269
+ },
270
+ };
271
+ saveProfileConfig(next, options);
272
+ return next;
273
+ }
274
+ export function syncLegacyConfigToProfile(legacy, options = {}) {
275
+ const active = getActiveProfile(options);
276
+ return upsertProfile({
277
+ ...active,
278
+ ingest_url: legacy.ingest_url ?? active.ingest_url,
279
+ tenant_id: legacy.tenant_id ?? active.tenant_id,
280
+ api_key_ref: legacy.api_key ? 'legacy:config.api_key' : active.api_key_ref,
281
+ metadata: {
282
+ ...active.metadata,
283
+ source: 'migration',
284
+ legacy_config_path: getModaPaths(options).legacyConfigPath,
285
+ },
286
+ }, options);
287
+ }
288
+ function resolveFileSecretPath(reference, options) {
289
+ if (!reference.startsWith('file:'))
290
+ return undefined;
291
+ const rawPath = reference.slice('file:'.length);
292
+ if (!rawPath)
293
+ return undefined;
294
+ const paths = getModaPaths(options);
295
+ const secretPath = isAbsolute(rawPath)
296
+ ? normalize(rawPath)
297
+ : normalize(join(paths.dir, rawPath));
298
+ const rel = relative(paths.dir, secretPath);
299
+ if (rel.startsWith('..') || isAbsolute(rel)) {
300
+ throw new Error(`Secret reference escapes the Moda directory: ${reference}`);
301
+ }
302
+ return secretPath;
303
+ }
304
+ export function secretFilePathFromRef(reference, options = {}) {
305
+ return resolveFileSecretPath(reference, options);
306
+ }
307
+ export function resolveSecretReference(reference, options = {}) {
308
+ if (!reference)
309
+ return undefined;
310
+ const env = getEnv(options);
311
+ if (reference.startsWith('env:')) {
312
+ return env[reference.slice('env:'.length)] || undefined;
313
+ }
314
+ if (reference === 'legacy:config.api_key') {
315
+ return loadLegacyCliConfig(options).api_key;
316
+ }
317
+ if (reference === 'legacy:auth.access_token') {
318
+ const parsed = safeReadRecord(getModaPaths(options).legacyAuthPath);
319
+ return optionalString(parsed?.access_token);
320
+ }
321
+ const filePath = resolveFileSecretPath(reference, options);
322
+ if (!filePath)
323
+ return undefined;
324
+ const parsed = safeReadRecord(filePath);
325
+ if (parsed) {
326
+ return (optionalString(parsed.value) ??
327
+ optionalString(parsed.api_key) ??
328
+ optionalString(parsed.access_token));
329
+ }
330
+ try {
331
+ const raw = readFileSync(filePath, 'utf8').trim();
332
+ return raw || undefined;
333
+ }
334
+ catch {
335
+ return undefined;
336
+ }
337
+ }
338
+ export function resolveApiKeyFromActiveProfile(options = {}) {
339
+ try {
340
+ return resolveSecretReference(getActiveProfile(options).api_key_ref, options);
341
+ }
342
+ catch {
343
+ return undefined;
344
+ }
345
+ }
346
+ export function resolveBaseUrl(options = {}) {
347
+ const envBase = optionalString(getEnv(options).MODA_BASE_URL);
348
+ if (envBase)
349
+ return envBase;
350
+ try {
351
+ return getActiveProfile(options).base_url || PROD_BASE_URL;
352
+ }
353
+ catch {
354
+ return PROD_BASE_URL;
355
+ }
356
+ }
357
+ export function resolveIngestUrlFromActiveProfile(options = {}) {
358
+ try {
359
+ return getActiveProfile(options).ingest_url;
360
+ }
361
+ catch {
362
+ return undefined;
363
+ }
364
+ }
365
+ export function resolveTenantIdFromActiveProfile(options = {}) {
366
+ try {
367
+ return getActiveProfile(options).tenant_id;
368
+ }
369
+ catch {
370
+ return undefined;
371
+ }
372
+ }
373
+ function modeString(mode) {
374
+ return `0${(mode & 0o777).toString(8).padStart(3, '0')}`;
375
+ }
376
+ export function ensureSecretFilePermissions(path, repair = true) {
377
+ if (!existsSync(path)) {
378
+ return { path, exists: false, secure: false, repaired: false };
379
+ }
380
+ const before = statSync(path);
381
+ if (!before.isFile()) {
382
+ return {
383
+ path,
384
+ exists: true,
385
+ secure: false,
386
+ repaired: false,
387
+ mode: modeString(before.mode),
388
+ reason: 'not_file',
389
+ };
390
+ }
391
+ if ((before.mode & 0o077) !== 0 && repair) {
392
+ try {
393
+ chmodSync(path, 0o600);
394
+ const after = statSync(path);
395
+ return {
396
+ path,
397
+ exists: true,
398
+ secure: (after.mode & 0o077) === 0,
399
+ repaired: true,
400
+ mode: modeString(after.mode),
401
+ };
402
+ }
403
+ catch {
404
+ return {
405
+ path,
406
+ exists: true,
407
+ secure: false,
408
+ repaired: false,
409
+ mode: modeString(before.mode),
410
+ reason: 'chmod_failed',
411
+ };
412
+ }
413
+ }
414
+ return {
415
+ path,
416
+ exists: true,
417
+ secure: (before.mode & 0o077) === 0,
418
+ repaired: false,
419
+ mode: modeString(before.mode),
420
+ };
421
+ }
422
+ export function writeSecretJsonFile(path, value) {
423
+ mkdirSync(dirname(path), { recursive: true });
424
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
425
+ try {
426
+ chmodSync(path, 0o600);
427
+ }
428
+ catch {
429
+ // Some filesystems do not support chmod; write mode covers the common case.
430
+ }
431
+ }
432
+ //# sourceMappingURL=profiles.js.map
@@ -0,0 +1,6 @@
1
+ import type { ProfileIOOptions } from './profiles.js';
2
+ export declare function runPromptsCommand(positionals: string[], flags: Record<string, string>, profileOptions?: ProfileIOOptions): Promise<void>;
3
+ export declare function initPrompts(options?: {
4
+ silent?: boolean;
5
+ }): Promise<void>;
6
+ //# sourceMappingURL=prompts.d.ts.map