@myvillage/cli 1.5.0 → 1.6.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.
@@ -0,0 +1,158 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { getConfigDir } from './config.js';
5
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
6
+
7
+ const SOULPRINT_DIR_NAME = 'soulprint';
8
+
9
+ // ── Directory Helpers ──────────────────────────────────
10
+
11
+ export function getSoulprintDir() {
12
+ return join(getConfigDir(), SOULPRINT_DIR_NAME);
13
+ }
14
+
15
+ export function getDatasetsDir() {
16
+ return join(getSoulprintDir(), 'datasets');
17
+ }
18
+
19
+ export function getModelsDir() {
20
+ return join(getSoulprintDir(), 'models');
21
+ }
22
+
23
+ export function getConfigsDir() {
24
+ return join(getSoulprintDir(), 'configs');
25
+ }
26
+
27
+ export function getLogsDir() {
28
+ return join(getSoulprintDir(), 'logs');
29
+ }
30
+
31
+ export function getVenvDir() {
32
+ return join(getSoulprintDir(), 'venv');
33
+ }
34
+
35
+ export function getScriptsDir() {
36
+ return join(getSoulprintDir(), 'scripts');
37
+ }
38
+
39
+ // ── Workspace State ────────────────────────────────────
40
+
41
+ export function isWorkspaceInitialized() {
42
+ const dir = getSoulprintDir();
43
+ return existsSync(dir) && existsSync(join(dir, 'workspace.yaml'));
44
+ }
45
+
46
+ export function ensureWorkspace() {
47
+ return isWorkspaceInitialized();
48
+ }
49
+
50
+ export function readWorkspaceConfig() {
51
+ const configPath = join(getSoulprintDir(), 'workspace.yaml');
52
+ if (!existsSync(configPath)) return null;
53
+ try {
54
+ return parseYaml(readFileSync(configPath, 'utf-8'));
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ export function writeWorkspaceConfig(config) {
61
+ const configPath = join(getSoulprintDir(), 'workspace.yaml');
62
+ writeFileSync(configPath, stringifyYaml(config, { lineWidth: 0 }));
63
+ }
64
+
65
+ // ── Dataset Helpers ────────────────────────────────────
66
+
67
+ export function getLocalDatasetDir(slug, version) {
68
+ return join(getDatasetsDir(), slug, `v${version}`);
69
+ }
70
+
71
+ export function isDatasetDownloaded(slug, version) {
72
+ const dir = getLocalDatasetDir(slug, version);
73
+ return existsSync(dir) && existsSync(join(dir, 'manifest.json'));
74
+ }
75
+
76
+ export function listLocalDatasets() {
77
+ const dir = getDatasetsDir();
78
+ if (!existsSync(dir)) return [];
79
+ return readdirSync(dir, { withFileTypes: true })
80
+ .filter(e => e.isDirectory())
81
+ .map(e => e.name);
82
+ }
83
+
84
+ // ── Python Environment ─────────────────────────────────
85
+
86
+ export function getPythonPath() {
87
+ const venvPython = join(getVenvDir(), 'bin', 'python');
88
+ if (existsSync(venvPython)) return venvPython;
89
+ return null;
90
+ }
91
+
92
+ export function isPythonAvailable() {
93
+ try {
94
+ execSync('python3 --version', { stdio: 'pipe' });
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ export function getPythonVersion() {
102
+ try {
103
+ return execSync('python3 --version', { stdio: 'pipe' }).toString().trim();
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ export function detectGPU() {
110
+ try {
111
+ const nvidiaSmi = execSync('nvidia-smi --query-gpu=name,memory.total --format=csv,noheader', { stdio: 'pipe' }).toString().trim();
112
+ if (nvidiaSmi) {
113
+ const [name, vram] = nvidiaSmi.split(', ');
114
+ return { type: 'cuda', name, vram };
115
+ }
116
+ } catch { /* no CUDA */ }
117
+
118
+ try {
119
+ const sysctl = execSync('sysctl -n machdep.cpu.brand_string', { stdio: 'pipe' }).toString().trim();
120
+ if (sysctl.includes('Apple')) {
121
+ const mem = execSync('sysctl -n hw.memsize', { stdio: 'pipe' }).toString().trim();
122
+ const memGB = Math.round(parseInt(mem) / 1073741824);
123
+ return { type: 'mps', name: sysctl, vram: `${memGB}GB unified` };
124
+ }
125
+ } catch { /* not Apple Silicon */ }
126
+
127
+ return { type: 'cpu', name: 'CPU only', vram: null };
128
+ }
129
+
130
+ export function getMachineInfo() {
131
+ const gpu = detectGPU();
132
+ return {
133
+ gpu: gpu.name,
134
+ gpuType: gpu.type,
135
+ vram: gpu.vram,
136
+ os: `${process.platform} ${process.arch}`,
137
+ python: getPythonVersion(),
138
+ nodeVersion: process.version,
139
+ };
140
+ }
141
+
142
+ // ── Training Job Local State ───────────────────────────
143
+
144
+ export function getJobOutputDir(jobId) {
145
+ return join(getModelsDir(), jobId);
146
+ }
147
+
148
+ export function getJobLogFile(jobId) {
149
+ const dir = getLogsDir();
150
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
151
+ return join(dir, `${jobId}.jsonl`);
152
+ }
153
+
154
+ export function appendJobLog(jobId, entry) {
155
+ const logFile = getJobLogFile(jobId);
156
+ const line = JSON.stringify({ ...entry, ts: new Date().toISOString() }) + '\n';
157
+ writeFileSync(logFile, line, { flag: 'a' });
158
+ }
@@ -0,0 +1,104 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { loadCredentials } from './auth.js';
4
+ import { brand, villageSpinner } from './brand.js';
5
+ import {
6
+ getVillagerVillages,
7
+ getVillagerByVillagerId,
8
+ getVillageCommunities,
9
+ joinCommunity,
10
+ } from './api.js';
11
+
12
+ /**
13
+ * Resolves a villager's village context: which village they're acting on behalf of
14
+ * and the corresponding MAN community.
15
+ *
16
+ * Returns { villagerUuid, village, community } or null if resolution fails.
17
+ */
18
+ export async function resolveVillageContext() {
19
+ const creds = loadCredentials();
20
+ if (!creds) return null;
21
+
22
+ const spinner = villageSpinner('Resolving village context...').start();
23
+
24
+ try {
25
+ // 1. Get villager UUID
26
+ let villagerUuid = creds.villager_uuid;
27
+
28
+ if (!villagerUuid && creds.villager_id) {
29
+ const result = await getVillagerByVillagerId(creds.villager_id);
30
+ villagerUuid = result?.data?.id || result?.id;
31
+ }
32
+
33
+ if (!villagerUuid) {
34
+ spinner.fail('Could not resolve villager profile.');
35
+ console.log(chalk.dim(' Try logging out and back in: myvillage logout && myvillage login'));
36
+ return null;
37
+ }
38
+
39
+ // 2. Get associated villages
40
+ const villagesResult = await getVillagerVillages(villagerUuid);
41
+ const villages = villagesResult?.data || [];
42
+
43
+ if (villages.length === 0) {
44
+ spinner.fail('You are not associated with any villages.');
45
+ console.log(chalk.dim(' Join a village at portal.myvillageproject.ai to get started.'));
46
+ return null;
47
+ }
48
+
49
+ spinner.stop();
50
+
51
+ // 3. Select village
52
+ let village;
53
+ if (villages.length === 1) {
54
+ village = villages[0];
55
+ console.log(brand.teal(` Village: ${village.name}`));
56
+ } else {
57
+ const { selected } = await inquirer.prompt([{
58
+ type: 'list',
59
+ name: 'selected',
60
+ message: 'Which village are you representing?',
61
+ choices: villages.map((v) => ({
62
+ name: `${v.name} (${v.city}, ${v.state})`,
63
+ value: v,
64
+ })),
65
+ }]);
66
+ village = selected;
67
+ }
68
+
69
+ // 4. Get village's dedicated community
70
+ const commSpinner = villageSpinner('Finding village community...').start();
71
+ const commResult = await getVillageCommunities(village.id);
72
+ const communities = commResult?.data || [];
73
+
74
+ // Find the community linked to this village
75
+ const community = communities.find((c) => c.villageId === village.id) || communities[0];
76
+
77
+ if (!community) {
78
+ commSpinner.fail('No MAN community found for this village.');
79
+ console.log(chalk.dim(' A community needs to be set up for your village first.'));
80
+ return null;
81
+ }
82
+
83
+ // 5. Auto-join community (ignore 409 = already member)
84
+ try {
85
+ await joinCommunity(community.slug);
86
+ } catch (err) {
87
+ if (err.response?.status !== 409 && err.response?.status !== 400) {
88
+ // Only throw if it's not "already a member"
89
+ const msg = err.response?.data?.error || '';
90
+ if (!msg.toLowerCase().includes('already')) {
91
+ throw err;
92
+ }
93
+ }
94
+ }
95
+
96
+ commSpinner.succeed(`Community: ${community.name}`);
97
+
98
+ return { villagerUuid, village, community };
99
+ } catch (err) {
100
+ spinner.stop();
101
+ console.log(chalk.red(` \u2717 Failed to resolve village context: ${err.message}`));
102
+ return null;
103
+ }
104
+ }