atris 2.6.0 → 2.6.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/GETTING_STARTED.md +2 -2
- package/atris/GETTING_STARTED.md +2 -2
- package/bin/atris.js +35 -4
- package/commands/business.js +244 -2
- package/commands/context-sync.js +228 -0
- package/commands/pull.js +176 -50
- package/commands/push.js +154 -61
- package/commands/setup.js +178 -0
- package/commands/workspace-clean.js +249 -0
- package/lib/manifest.js +224 -0
- package/lib/section-merge.js +196 -0
- package/package.json +9 -4
- package/utils/api.js +9 -1
- package/utils/update-check.js +11 -11
- package/AGENT.md +0 -35
- package/atris/experiments/README.md +0 -118
- package/atris/experiments/_examples/smoke-keep-revert/README.md +0 -45
- package/atris/experiments/_examples/smoke-keep-revert/candidate.py +0 -8
- package/atris/experiments/_examples/smoke-keep-revert/loop.py +0 -129
- package/atris/experiments/_examples/smoke-keep-revert/measure.py +0 -47
- package/atris/experiments/_examples/smoke-keep-revert/program.md +0 -3
- package/atris/experiments/_examples/smoke-keep-revert/proposals/bad_patch.py +0 -19
- package/atris/experiments/_examples/smoke-keep-revert/proposals/fix_patch.py +0 -22
- package/atris/experiments/_examples/smoke-keep-revert/reset.py +0 -21
- package/atris/experiments/_examples/smoke-keep-revert/results.tsv +0 -5
- package/atris/experiments/_examples/smoke-keep-revert/visual.svg +0 -52
- package/atris/experiments/_fixtures/invalid/BadName/loop.py +0 -1
- package/atris/experiments/_fixtures/invalid/BadName/program.md +0 -3
- package/atris/experiments/_fixtures/invalid/BadName/results.tsv +0 -1
- package/atris/experiments/_fixtures/invalid/bloated-context/loop.py +0 -1
- package/atris/experiments/_fixtures/invalid/bloated-context/measure.py +0 -1
- package/atris/experiments/_fixtures/invalid/bloated-context/program.md +0 -6
- package/atris/experiments/_fixtures/invalid/bloated-context/results.tsv +0 -1
- package/atris/experiments/_fixtures/valid/good-experiment/loop.py +0 -1
- package/atris/experiments/_fixtures/valid/good-experiment/measure.py +0 -1
- package/atris/experiments/_fixtures/valid/good-experiment/program.md +0 -3
- package/atris/experiments/_fixtures/valid/good-experiment/results.tsv +0 -1
- package/atris/experiments/_template/pack/loop.py +0 -3
- package/atris/experiments/_template/pack/measure.py +0 -13
- package/atris/experiments/_template/pack/program.md +0 -3
- package/atris/experiments/_template/pack/reset.py +0 -3
- package/atris/experiments/_template/pack/results.tsv +0 -1
- package/atris/experiments/benchmark_runtime.py +0 -81
- package/atris/experiments/benchmark_validate.py +0 -70
- package/atris/experiments/validate.py +0 -92
- package/atris/team/navigator/journal/2026-02-23.md +0 -6
package/GETTING_STARTED.md
CHANGED
|
@@ -164,8 +164,8 @@ This syncs your local `atris.md` and agent templates to the latest version. Re-r
|
|
|
164
164
|
## Need Help?
|
|
165
165
|
|
|
166
166
|
- **Full spec:** Read `atris.md` for technical details
|
|
167
|
-
- **Issues:** https://github.com/atrislabs/atris
|
|
168
|
-
- **Docs:** https://github.com/atrislabs/atris
|
|
167
|
+
- **Issues:** https://github.com/atrislabs/atris/issues
|
|
168
|
+
- **Docs:** https://github.com/atrislabs/atris
|
|
169
169
|
|
|
170
170
|
---
|
|
171
171
|
|
package/atris/GETTING_STARTED.md
CHANGED
|
@@ -164,8 +164,8 @@ This syncs your local `atris.md` and agent templates to the latest version. Re-r
|
|
|
164
164
|
## Need Help?
|
|
165
165
|
|
|
166
166
|
- **Full spec:** Read `atris.md` for technical details
|
|
167
|
-
- **Issues:** https://github.com/atrislabs/atris
|
|
168
|
-
- **Docs:** https://github.com/atrislabs/atris
|
|
167
|
+
- **Issues:** https://github.com/atrislabs/atris/issues
|
|
168
|
+
- **Docs:** https://github.com/atrislabs/atris
|
|
169
169
|
|
|
170
170
|
---
|
|
171
171
|
|
package/bin/atris.js
CHANGED
|
@@ -211,6 +211,7 @@ function showHelp() {
|
|
|
211
211
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
212
212
|
console.log('');
|
|
213
213
|
console.log('Setup:');
|
|
214
|
+
console.log(' setup - Guided first-time setup (login, pick business, pull)');
|
|
214
215
|
console.log(' init - Initialize Atris in current project');
|
|
215
216
|
console.log(' update - Update local files to latest version');
|
|
216
217
|
console.log(' upgrade - Install latest Atris from npm');
|
|
@@ -246,6 +247,14 @@ function showHelp() {
|
|
|
246
247
|
console.log('');
|
|
247
248
|
console.log('Sync:');
|
|
248
249
|
console.log(' pull - Pull journals + member data from cloud');
|
|
250
|
+
console.log(' clean-workspace <slug> - Analyze & remove junk files from a workspace (alias: cw)');
|
|
251
|
+
console.log('');
|
|
252
|
+
console.log('Business:');
|
|
253
|
+
console.log(' business add <slug> - Connect a business');
|
|
254
|
+
console.log(' business list - Show connected businesses');
|
|
255
|
+
console.log(' business remove <slug> - Disconnect a business');
|
|
256
|
+
console.log(' business health <slug> - Health report (members, workspace, issues)');
|
|
257
|
+
console.log(' business audit - One-line health summary of all businesses');
|
|
249
258
|
console.log('');
|
|
250
259
|
console.log('Cloud & agents:');
|
|
251
260
|
console.log(' console - Start/attach always-on coding console (tmux daemon)');
|
|
@@ -387,7 +396,7 @@ const { planAtris: planCmd, doAtris: doCmd, reviewAtris: reviewCmd } = require('
|
|
|
387
396
|
const knownCommands = ['init', 'log', 'status', 'analytics', 'visualize', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review',
|
|
388
397
|
'activate', 'agent', 'chat', 'console', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
389
398
|
'clean', 'verify', 'search', 'skill', 'member', 'plugin', 'experiments', 'pull', 'push', 'business', 'sync',
|
|
390
|
-
'gmail', 'calendar', 'twitter', 'slack', 'integrations'];
|
|
399
|
+
'gmail', 'calendar', 'twitter', 'slack', 'integrations', 'setup', 'clean-workspace', 'cw'];
|
|
391
400
|
|
|
392
401
|
// Check if command is an atris.md spec file - triggers welcome visualization
|
|
393
402
|
function isSpecFile(cmd) {
|
|
@@ -698,6 +707,11 @@ if (command === 'init') {
|
|
|
698
707
|
console.error(`✗ Log sync failed: ${error.message || error}`);
|
|
699
708
|
process.exit(1);
|
|
700
709
|
});
|
|
710
|
+
} else if (subcommand && subcommand !== '--help' && !subcommand.startsWith('-')) {
|
|
711
|
+
// Business log: atris log <business-slug>
|
|
712
|
+
require('../commands/context-sync').businessLog(subcommand)
|
|
713
|
+
.then(() => process.exit(0))
|
|
714
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
701
715
|
} else {
|
|
702
716
|
logCmd();
|
|
703
717
|
}
|
|
@@ -876,9 +890,17 @@ if (command === 'init') {
|
|
|
876
890
|
process.exit(1);
|
|
877
891
|
});
|
|
878
892
|
} else if (command === 'status') {
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
893
|
+
const subcommand = process.argv[3];
|
|
894
|
+
if (subcommand && !subcommand.startsWith('-')) {
|
|
895
|
+
// Business status: atris status <business-slug>
|
|
896
|
+
require('../commands/context-sync').businessStatus(subcommand)
|
|
897
|
+
.then(() => process.exit(0))
|
|
898
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
899
|
+
} else {
|
|
900
|
+
const isQuick = process.argv.includes('--quick') || process.argv.includes('-q');
|
|
901
|
+
const isJson = process.argv.includes('--json');
|
|
902
|
+
statusCmd(isQuick, isJson);
|
|
903
|
+
}
|
|
882
904
|
} else if (command === 'analytics') {
|
|
883
905
|
require('../commands/analytics').analyticsAtris();
|
|
884
906
|
} else if (command === 'clean') {
|
|
@@ -945,6 +967,11 @@ if (command === 'init') {
|
|
|
945
967
|
require('../commands/business').businessCommand(subcommand, ...args)
|
|
946
968
|
.then(() => process.exit(0))
|
|
947
969
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
970
|
+
} else if (command === 'clean-workspace' || command === 'cw') {
|
|
971
|
+
const { cleanWorkspace } = require('../commands/workspace-clean');
|
|
972
|
+
cleanWorkspace()
|
|
973
|
+
.then(() => process.exit(0))
|
|
974
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
948
975
|
} else if (command === 'plugin') {
|
|
949
976
|
const subcommand = process.argv[3] || 'build';
|
|
950
977
|
const args = process.argv.slice(4);
|
|
@@ -953,6 +980,10 @@ if (command === 'init') {
|
|
|
953
980
|
const subcommand = process.argv[3];
|
|
954
981
|
const args = process.argv.slice(4);
|
|
955
982
|
require('../commands/experiments').experimentsCommand(subcommand, ...args);
|
|
983
|
+
} else if (command === 'setup') {
|
|
984
|
+
require('../commands/setup').setupAtris()
|
|
985
|
+
.then(() => process.exit(0))
|
|
986
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
956
987
|
} else {
|
|
957
988
|
console.log(`Unknown command: ${command}`);
|
|
958
989
|
console.log('Run "atris help" to see available commands');
|
package/commands/business.js
CHANGED
|
@@ -111,6 +111,242 @@ async function removeBusiness(slug) {
|
|
|
111
111
|
console.log(`\nRemoved "${name}"`);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Resolve a slug to a business ID using local cache or API lookup
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
async function resolveSlug(slug, creds) {
|
|
118
|
+
// Check local cache first
|
|
119
|
+
const businesses = loadBusinesses();
|
|
120
|
+
if (businesses[slug]) {
|
|
121
|
+
return businesses[slug];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Try by-slug endpoint
|
|
125
|
+
const result = await apiRequestJson(`/businesses/by-slug/${slug}/`, {
|
|
126
|
+
method: 'GET',
|
|
127
|
+
token: creds.token,
|
|
128
|
+
});
|
|
129
|
+
if (result.ok && result.data) {
|
|
130
|
+
return { business_id: result.data.id, workspace_id: result.data.workspace_id, name: result.data.name, slug: result.data.slug };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Fallback: list all and match
|
|
134
|
+
const listResult = await apiRequestJson('/businesses/', { method: 'GET', token: creds.token });
|
|
135
|
+
if (listResult.ok && Array.isArray(listResult.data)) {
|
|
136
|
+
const match = listResult.data.find(b => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase());
|
|
137
|
+
if (match) {
|
|
138
|
+
return { business_id: match.id, workspace_id: match.workspace_id, name: match.name, slug: match.slug };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Helper: format relative time
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
function relativeTime(dateStr) {
|
|
149
|
+
if (!dateStr) return 'unknown';
|
|
150
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
151
|
+
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
152
|
+
if (days <= 0) return 'today';
|
|
153
|
+
if (days === 1) return '1d ago';
|
|
154
|
+
return `${days}d ago`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Helper: activity bar
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
function activityBar(daysSinceActive, width = 10) {
|
|
161
|
+
const filled = Math.max(0, Math.min(width, width - Math.floor(daysSinceActive / 3)));
|
|
162
|
+
return '\u2501'.repeat(filled) + '\u2591'.repeat(width - filled);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// atris business health <slug>
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
async function businessHealth(slug) {
|
|
169
|
+
if (!slug) {
|
|
170
|
+
console.error('Usage: atris business health <slug>');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const creds = loadCredentials();
|
|
175
|
+
if (!creds || !creds.token) {
|
|
176
|
+
console.error('Not logged in. Run: atris login');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const biz = await resolveSlug(slug, creds);
|
|
181
|
+
if (!biz) {
|
|
182
|
+
console.error(`Business "${slug}" not found.`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const bizId = biz.business_id;
|
|
187
|
+
const wsId = biz.workspace_id;
|
|
188
|
+
|
|
189
|
+
// Fetch dashboard and workspace snapshot in parallel
|
|
190
|
+
const fetchOpts = { method: 'GET', token: creds.token, timeoutMs: 120000 };
|
|
191
|
+
const [dashResult, wsResult] = await Promise.all([
|
|
192
|
+
apiRequestJson(`/businesses/${bizId}/dashboard/`, fetchOpts),
|
|
193
|
+
wsId
|
|
194
|
+
? apiRequestJson(`/businesses/${bizId}/workspaces/${wsId}/snapshot?include_content=false`, fetchOpts)
|
|
195
|
+
: Promise.resolve({ ok: false }),
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
const dashboard = dashResult.ok ? dashResult.data : null;
|
|
199
|
+
const workspace = wsResult.ok ? wsResult.data : null;
|
|
200
|
+
|
|
201
|
+
const name = dashboard?.business?.name || biz.name || slug;
|
|
202
|
+
|
|
203
|
+
console.log('');
|
|
204
|
+
console.log(`Business Health: ${name}`);
|
|
205
|
+
console.log('\u2501'.repeat(26 + name.length));
|
|
206
|
+
console.log('');
|
|
207
|
+
|
|
208
|
+
// Workspace stats
|
|
209
|
+
const files = workspace?.files || [];
|
|
210
|
+
const totalSize = files.reduce((sum, f) => sum + (f.size || 0), 0);
|
|
211
|
+
const fileSizeStr = totalSize > 1024 ? `${Math.round(totalSize / 1024)}KB` : `${totalSize}B`;
|
|
212
|
+
console.log(` Workspace: ${files.length} files, ${fileSizeStr}`);
|
|
213
|
+
|
|
214
|
+
// Members
|
|
215
|
+
const members = dashboard?.roster?.members || dashboard?.members || dashboard?.business?.members || [];
|
|
216
|
+
const humanMembers = members.filter(m => !m.is_agent && m.role !== 'agent');
|
|
217
|
+
const agentMembers = members.filter(m => m.is_agent || m.role === 'agent');
|
|
218
|
+
const memberCountStr = members.length > 0
|
|
219
|
+
? `${members.length} (${humanMembers.length} human, ${agentMembers.length} agent)`
|
|
220
|
+
: `${members.length}`;
|
|
221
|
+
console.log(` Members: ${memberCountStr}`);
|
|
222
|
+
|
|
223
|
+
// Apps
|
|
224
|
+
const apps = dashboard?.business?.apps || dashboard?.apps || [];
|
|
225
|
+
console.log(` Apps: ${Array.isArray(apps) ? apps.length : 0}`);
|
|
226
|
+
|
|
227
|
+
// Status
|
|
228
|
+
const status = dashboard?.business?.status || dashboard?.status || 'unknown';
|
|
229
|
+
console.log(` Status: ${status}`);
|
|
230
|
+
|
|
231
|
+
// Member activity
|
|
232
|
+
if (members.length > 0) {
|
|
233
|
+
console.log('');
|
|
234
|
+
console.log(' Member Activity:');
|
|
235
|
+
for (const m of members) {
|
|
236
|
+
const memberName = m.display_name || m.name || m.email || 'Unknown';
|
|
237
|
+
const role = m.role || 'member';
|
|
238
|
+
const lastActive = m.atris?.last_active || m.last_active || m.last_login || m.joined_at || m.created_at;
|
|
239
|
+
const daysSince = lastActive ? Math.floor((Date.now() - new Date(lastActive).getTime()) / (1000 * 60 * 60 * 24)) : 999;
|
|
240
|
+
const bar = activityBar(daysSince);
|
|
241
|
+
const label = daysSince <= 1 ? 'active' : `last active ${relativeTime(lastActive)}`;
|
|
242
|
+
console.log(` ${memberName.padEnd(18)} ${role.padEnd(8)} ${bar} ${label}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Workspace breakdown by directory
|
|
247
|
+
if (files.length > 0) {
|
|
248
|
+
console.log('');
|
|
249
|
+
console.log(' Workspace Breakdown:');
|
|
250
|
+
const dirSizes = {};
|
|
251
|
+
for (const f of files) {
|
|
252
|
+
const filePath = f.path || f.name || '';
|
|
253
|
+
const dir = filePath.includes('/') ? filePath.split('/')[0] + '/' : '/';
|
|
254
|
+
dirSizes[dir] = (dirSizes[dir] || 0) + (f.size || 0);
|
|
255
|
+
}
|
|
256
|
+
const maxDirSize = Math.max(...Object.values(dirSizes), 1);
|
|
257
|
+
const sortedDirs = Object.entries(dirSizes).sort((a, b) => b[1] - a[1]);
|
|
258
|
+
for (const [dir, size] of sortedDirs) {
|
|
259
|
+
const sizeStr = size > 1024 ? `${Math.round(size / 1024)}KB` : `${size}B`;
|
|
260
|
+
const barLen = Math.max(1, Math.round((size / maxDirSize) * 10));
|
|
261
|
+
console.log(` ${dir.padEnd(12)} ${sizeStr.padStart(5)} ${'█'.repeat(barLen)}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Issues
|
|
266
|
+
console.log('');
|
|
267
|
+
console.log(' Issues:');
|
|
268
|
+
let hasIssues = false;
|
|
269
|
+
const humanMembers2 = members.filter(m => m.role !== 'agent');
|
|
270
|
+
for (const m of humanMembers2) {
|
|
271
|
+
const lastActive = m.atris?.last_active || m.last_active || m.last_login || m.joined_at || m.created_at;
|
|
272
|
+
const daysSince = lastActive ? Math.floor((Date.now() - new Date(lastActive).getTime()) / (1000 * 60 * 60 * 24)) : 999;
|
|
273
|
+
if (daysSince >= 30) {
|
|
274
|
+
const memberName = m.display_name || m.name || m.email || 'Unknown';
|
|
275
|
+
console.log(` \u26A0 ${memberName} inactive for ${daysSince}+ days`);
|
|
276
|
+
hasIssues = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check for workspace bloat (arbitrary threshold: >500KB or >100 files)
|
|
281
|
+
if (totalSize > 500 * 1024) {
|
|
282
|
+
console.log(` \u26A0 Workspace large (${fileSizeStr})`);
|
|
283
|
+
hasIssues = true;
|
|
284
|
+
}
|
|
285
|
+
if (files.length > 100) {
|
|
286
|
+
console.log(` \u26A0 Workspace has ${files.length} files (consider cleanup)`);
|
|
287
|
+
hasIssues = true;
|
|
288
|
+
}
|
|
289
|
+
if (!hasIssues) {
|
|
290
|
+
console.log(' \u2713 Workspace clean (no bloat detected)');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log('');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// atris business audit
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
async function businessAudit() {
|
|
300
|
+
const creds = loadCredentials();
|
|
301
|
+
if (!creds || !creds.token) {
|
|
302
|
+
console.error('Not logged in. Run: atris login');
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const listResult = await apiRequestJson('/businesses/', { method: 'GET', token: creds.token });
|
|
307
|
+
if (!listResult.ok || !Array.isArray(listResult.data)) {
|
|
308
|
+
console.error(`Failed to fetch businesses: ${listResult.error || 'unknown error'}`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const businesses = listResult.data;
|
|
313
|
+
|
|
314
|
+
console.log('');
|
|
315
|
+
console.log('Business Audit');
|
|
316
|
+
console.log('\u2501'.repeat(14));
|
|
317
|
+
console.log('');
|
|
318
|
+
|
|
319
|
+
for (const biz of businesses) {
|
|
320
|
+
const name = biz.name || biz.slug || 'Unknown';
|
|
321
|
+
const memberCount = typeof biz.member_count === 'number' ? biz.member_count : (Array.isArray(biz.members) ? biz.members.length : 0);
|
|
322
|
+
const appCount = typeof biz.app_count === 'number' ? biz.app_count : (Array.isArray(biz.apps) ? biz.apps.length : 0);
|
|
323
|
+
|
|
324
|
+
// Determine activity status
|
|
325
|
+
const status = biz.status || 'unknown';
|
|
326
|
+
const isActive = status === 'active' || (memberCount > 1 && appCount > 0);
|
|
327
|
+
const hasContent = memberCount > 1 || appCount > 0;
|
|
328
|
+
|
|
329
|
+
let icon, activityLabel;
|
|
330
|
+
if (isActive) {
|
|
331
|
+
icon = '\u2713';
|
|
332
|
+
activityLabel = appCount > 0 ? 'active' : 'idle';
|
|
333
|
+
} else if (hasContent) {
|
|
334
|
+
icon = '\u26A0';
|
|
335
|
+
activityLabel = 'inactive';
|
|
336
|
+
} else {
|
|
337
|
+
icon = '\u25CB';
|
|
338
|
+
activityLabel = 'inactive';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const memberStr = memberCount === 1 ? '1 member' : `${memberCount} members`;
|
|
342
|
+
const appStr = appCount === 1 ? '1 app' : `${appCount} apps`;
|
|
343
|
+
|
|
344
|
+
console.log(` ${icon} ${name.padEnd(16)} ${memberStr.padEnd(12)} ${appStr.padEnd(8)} ${activityLabel}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log('');
|
|
348
|
+
}
|
|
349
|
+
|
|
114
350
|
async function businessCommand(subcommand, ...args) {
|
|
115
351
|
switch (subcommand) {
|
|
116
352
|
case 'add':
|
|
@@ -124,9 +360,15 @@ async function businessCommand(subcommand, ...args) {
|
|
|
124
360
|
case 'rm':
|
|
125
361
|
await removeBusiness(args[0]);
|
|
126
362
|
break;
|
|
363
|
+
case 'health':
|
|
364
|
+
await businessHealth(args[0]);
|
|
365
|
+
break;
|
|
366
|
+
case 'audit':
|
|
367
|
+
await businessAudit();
|
|
368
|
+
break;
|
|
127
369
|
default:
|
|
128
|
-
console.log('Usage: atris business <add|list|remove> [slug]');
|
|
370
|
+
console.log('Usage: atris business <add|list|remove|health|audit> [slug]');
|
|
129
371
|
}
|
|
130
372
|
}
|
|
131
373
|
|
|
132
|
-
module.exports = { businessCommand, loadBusinesses, saveBusinesses, getBusinessConfigPath };
|
|
374
|
+
module.exports = { businessCommand, businessHealth, businessAudit, loadBusinesses, saveBusinesses, getBusinessConfigPath };
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { loadCredentials } = require('../utils/auth');
|
|
4
|
+
const { apiRequestJson } = require('../utils/api');
|
|
5
|
+
const { loadBusinesses } = require('./business');
|
|
6
|
+
const { loadManifest, computeLocalHashes, threeWayCompare } = require('../lib/manifest');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a business slug to its IDs. Shared helper.
|
|
10
|
+
*/
|
|
11
|
+
async function resolveBusiness(slug) {
|
|
12
|
+
const creds = loadCredentials();
|
|
13
|
+
if (!creds || !creds.token) {
|
|
14
|
+
console.error('Not logged in. Run: atris login');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const businesses = loadBusinesses();
|
|
19
|
+
if (businesses[slug]) {
|
|
20
|
+
return {
|
|
21
|
+
businessId: businesses[slug].business_id,
|
|
22
|
+
workspaceId: businesses[slug].workspace_id,
|
|
23
|
+
name: businesses[slug].name || slug,
|
|
24
|
+
slug: businesses[slug].slug || slug,
|
|
25
|
+
token: creds.token,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const listResult = await apiRequestJson('/businesses/', { method: 'GET', token: creds.token });
|
|
30
|
+
if (!listResult.ok) {
|
|
31
|
+
console.error(`Failed to fetch businesses: ${listResult.errorMessage || listResult.status}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const match = (listResult.data || []).find(
|
|
35
|
+
b => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase()
|
|
36
|
+
);
|
|
37
|
+
if (!match) {
|
|
38
|
+
console.error(`Business "${slug}" not found.`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
businessId: match.id,
|
|
43
|
+
workspaceId: match.workspace_id,
|
|
44
|
+
name: match.name,
|
|
45
|
+
slug: match.slug,
|
|
46
|
+
token: creds.token,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* atris status <business-slug>
|
|
53
|
+
* Shows what's different locally vs remote without transferring.
|
|
54
|
+
*/
|
|
55
|
+
async function businessStatus(slug) {
|
|
56
|
+
const biz = await resolveBusiness(slug);
|
|
57
|
+
|
|
58
|
+
if (!biz.workspaceId) {
|
|
59
|
+
console.error(`Business "${slug}" has no workspace.`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const manifest = loadManifest(biz.slug);
|
|
64
|
+
const timeSince = manifest ? _timeSince(manifest.last_sync) : null;
|
|
65
|
+
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(`${biz.name}` + (timeSince ? ` \u2014 last synced ${timeSince}` : ' \u2014 never synced'));
|
|
68
|
+
|
|
69
|
+
// Determine local directory
|
|
70
|
+
const atrisDir = path.join(process.cwd(), 'atris', slug);
|
|
71
|
+
const cwdDir = path.join(process.cwd(), slug);
|
|
72
|
+
let localDir = null;
|
|
73
|
+
if (fs.existsSync(atrisDir)) localDir = atrisDir;
|
|
74
|
+
else if (fs.existsSync(cwdDir)) localDir = cwdDir;
|
|
75
|
+
|
|
76
|
+
// Get remote snapshot with content (needed for reliable hash computation)
|
|
77
|
+
const result = await apiRequestJson(
|
|
78
|
+
`/businesses/${biz.businessId}/workspaces/${biz.workspaceId}/snapshot?include_content=true`,
|
|
79
|
+
{ method: 'GET', token: biz.token, timeoutMs: 120000 }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
if (result.status === 409) {
|
|
84
|
+
console.log(' Computer is sleeping. Wake it first.');
|
|
85
|
+
} else {
|
|
86
|
+
console.error(` Failed to get remote state: ${result.errorMessage || result.status}`);
|
|
87
|
+
}
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const remoteFiles = {};
|
|
92
|
+
const crypto = require('crypto');
|
|
93
|
+
for (const file of (result.data.files || [])) {
|
|
94
|
+
if (file.path && !file.binary && file.content != null) {
|
|
95
|
+
const rawBytes = Buffer.from(file.content, 'utf-8');
|
|
96
|
+
remoteFiles[file.path] = { hash: crypto.createHash('sha256').update(rawBytes).digest('hex'), size: rawBytes.length };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const localFiles = localDir ? computeLocalHashes(localDir) : {};
|
|
101
|
+
const diff = threeWayCompare(localFiles, remoteFiles, manifest);
|
|
102
|
+
|
|
103
|
+
console.log('');
|
|
104
|
+
|
|
105
|
+
// You changed
|
|
106
|
+
const youChanged = [...diff.toPush, ...diff.newLocal];
|
|
107
|
+
if (youChanged.length > 0) {
|
|
108
|
+
console.log(' You changed:');
|
|
109
|
+
for (const p of youChanged) {
|
|
110
|
+
const label = diff.newLocal.includes(p) ? '(new)' : '';
|
|
111
|
+
console.log(` ${p.replace(/^\//, '')} ${label}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Computer changed
|
|
116
|
+
const computerChanged = [...diff.toPull, ...diff.newRemote];
|
|
117
|
+
if (computerChanged.length > 0) {
|
|
118
|
+
console.log(' Computer changed:');
|
|
119
|
+
for (const p of computerChanged) {
|
|
120
|
+
const label = diff.newRemote.includes(p) ? '(new)' : '';
|
|
121
|
+
console.log(` ${p.replace(/^\//, '')} ${label}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Conflicts
|
|
126
|
+
if (diff.conflicts.length > 0) {
|
|
127
|
+
console.log(' Conflicts:');
|
|
128
|
+
for (const p of diff.conflicts) {
|
|
129
|
+
console.log(` ${p.replace(/^\//, '')}`);
|
|
130
|
+
}
|
|
131
|
+
} else if (youChanged.length > 0 || computerChanged.length > 0) {
|
|
132
|
+
console.log(' Conflicts: (none)');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Remote deletions
|
|
136
|
+
if (diff.deletedRemote.length > 0) {
|
|
137
|
+
console.log(' Deleted on computer:');
|
|
138
|
+
for (const p of diff.deletedRemote) {
|
|
139
|
+
console.log(` ${p.replace(/^\//, '')}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (youChanged.length === 0 && computerChanged.length === 0 && diff.conflicts.length === 0) {
|
|
144
|
+
if (!localDir) {
|
|
145
|
+
console.log(' No local copy found. Run: atris pull ' + slug);
|
|
146
|
+
} else {
|
|
147
|
+
console.log(' Everything up to date.');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log('');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* atris log <business-slug>
|
|
157
|
+
* Shows human-readable commit history.
|
|
158
|
+
*/
|
|
159
|
+
async function businessLog(slug) {
|
|
160
|
+
const biz = await resolveBusiness(slug);
|
|
161
|
+
|
|
162
|
+
if (!biz.workspaceId) {
|
|
163
|
+
console.error(`Business "${slug}" has no workspace.`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const limit = 20;
|
|
168
|
+
const pathFilter = process.argv.includes('--path') ? process.argv[process.argv.indexOf('--path') + 1] : null;
|
|
169
|
+
|
|
170
|
+
const params = `limit=${limit}` + (pathFilter ? `&path=${encodeURIComponent(pathFilter)}` : '');
|
|
171
|
+
const result = await apiRequestJson(
|
|
172
|
+
`/businesses/${biz.businessId}/workspaces/${biz.workspaceId}/git/log?${params}`,
|
|
173
|
+
{ method: 'GET', token: biz.token }
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (!result.ok) {
|
|
177
|
+
if (result.status === 409) {
|
|
178
|
+
console.log('\n Computer is sleeping. Wake it first.\n');
|
|
179
|
+
} else {
|
|
180
|
+
console.error(`\n Failed to get history: ${result.errorMessage || result.status}\n`);
|
|
181
|
+
}
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const commits = result.data.commits || [];
|
|
186
|
+
if (commits.length === 0) {
|
|
187
|
+
console.log(`\n ${biz.name} \u2014 no history yet.\n`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log(`\n ${biz.name} \u2014 history\n`);
|
|
192
|
+
|
|
193
|
+
for (const commit of commits) {
|
|
194
|
+
const date = _timeSince(commit.date) || commit.date;
|
|
195
|
+
const msg = commit.message || '';
|
|
196
|
+
|
|
197
|
+
// Parse actor from message format "actor/name: message"
|
|
198
|
+
const colonIdx = msg.indexOf(': ');
|
|
199
|
+
let actor = '';
|
|
200
|
+
let description = msg;
|
|
201
|
+
if (colonIdx > 0 && colonIdx < 40) {
|
|
202
|
+
actor = msg.substring(0, colonIdx);
|
|
203
|
+
description = msg.substring(colonIdx + 2);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const actorDisplay = actor ? ` ${actor}` : '';
|
|
207
|
+
console.log(` ${date.padEnd(12)} ${actorDisplay}`);
|
|
208
|
+
console.log(` ${description}`);
|
|
209
|
+
console.log('');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
function _timeSince(isoString) {
|
|
215
|
+
if (!isoString) return null;
|
|
216
|
+
const diff = Date.now() - new Date(isoString).getTime();
|
|
217
|
+
if (diff < 0) return 'just now';
|
|
218
|
+
const mins = Math.floor(diff / 60000);
|
|
219
|
+
if (mins < 1) return 'just now';
|
|
220
|
+
if (mins < 60) return `${mins}m ago`;
|
|
221
|
+
const hours = Math.floor(mins / 60);
|
|
222
|
+
if (hours < 24) return `${hours}h ago`;
|
|
223
|
+
const days = Math.floor(hours / 24);
|
|
224
|
+
return `${days}d ago`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
module.exports = { businessStatus, businessLog };
|