New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

atris

Package Overview
Dependencies
Maintainers
1
Versions
71
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

atris - npm Package Compare versions

Comparing version
2.6.2
to
2.6.3
+74
-19
commands/pull.js

@@ -13,4 +13,17 @@ const fs = require('fs');

async function pullAtris() {
const arg = process.argv[3];
let arg = process.argv[3];
// Auto-detect business from .atris/business.json in current dir
if (!arg || arg.startsWith('--')) {
const bizFile = path.join(process.cwd(), '.atris', 'business.json');
if (fs.existsSync(bizFile)) {
try {
const biz = JSON.parse(fs.readFileSync(bizFile, 'utf8'));
if (biz.slug || biz.name) {
return pullBusiness(biz.slug || biz.name);
}
} catch {}
}
}
// If a business name is given, do a business pull

@@ -85,6 +98,15 @@ if (arg && arg !== '--help' && !arg.startsWith('--')) {

// Parse --only flag: comma-separated directory prefixes to filter
const onlyArg = process.argv.find(a => a.startsWith('--only='));
const onlyPrefixes = onlyArg
? onlyArg.slice('--only='.length).split(',').map(p => {
// Normalize: strip leading slash, ensure trailing slash for dirs
// Supports both --only=team/,context/ and --only team/,context/
let onlyRaw = null;
const onlyEqArg = process.argv.find(a => a.startsWith('--only='));
if (onlyEqArg) {
onlyRaw = onlyEqArg.slice('--only='.length);
} else {
const onlyIdx = process.argv.indexOf('--only');
if (onlyIdx !== -1 && process.argv[onlyIdx + 1] && !process.argv[onlyIdx + 1].startsWith('-')) {
onlyRaw = process.argv[onlyIdx + 1];
}
}
const onlyPrefixes = onlyRaw
? onlyRaw.split(',').map(p => {
let norm = p.replace(/^\//, '');

@@ -96,7 +118,15 @@ if (norm && !norm.endsWith('/') && !norm.includes('.')) norm += '/';

// Parse --timeout flag: override default 120s timeout
const timeoutArg = process.argv.find(a => a.startsWith('--timeout='));
const timeoutMs = timeoutArg
? parseInt(timeoutArg.slice('--timeout='.length), 10) * 1000
: 120000;
// Parse --timeout flag: override default 300s timeout
// Supports both --timeout=60 and --timeout 60
let timeoutSec = 300;
const timeoutEqArg = process.argv.find(a => a.startsWith('--timeout='));
if (timeoutEqArg) {
timeoutSec = parseInt(timeoutEqArg.slice('--timeout='.length), 10);
} else {
const timeoutIdx = process.argv.indexOf('--timeout');
if (timeoutIdx !== -1 && process.argv[timeoutIdx + 1]) {
timeoutSec = parseInt(process.argv[timeoutIdx + 1], 10);
}
}
const timeoutMs = timeoutSec * 1000;

@@ -169,14 +199,27 @@ // Determine output directory

console.log(`Pulling ${businessName}...` + (timeSince ? ` (last synced ${timeSince})` : ''));
console.log(' Fetching workspace...');
// Get remote snapshot (large workspaces can take 60s+)
const result = await apiRequestJson(
`/businesses/${businessId}/workspaces/${workspaceId}/snapshot?include_content=true`,
{ method: 'GET', token: creds.token, timeoutMs }
);
// Loading indicator with elapsed time
const startTime = Date.now();
const spinner = ['|', '/', '-', '\\'];
let spinIdx = 0;
const loading = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
process.stdout.write(`\r Fetching workspace... ${spinner[spinIdx++ % 4]} ${elapsed}s`);
}, 250);
// Get remote snapshot — pass --only prefixes to server for faster response
let snapshotUrl = `/businesses/${businessId}/workspaces/${workspaceId}/snapshot?include_content=true`;
if (onlyPrefixes) {
snapshotUrl += `&paths=${encodeURIComponent(onlyPrefixes.map(p => p.replace(/\/$/, '')).join(','))}`;
}
const result = await apiRequestJson(snapshotUrl, { method: 'GET', token: creds.token, timeoutMs });
clearInterval(loading);
const totalSec = Math.floor((Date.now() - startTime) / 1000);
process.stdout.write(`\r Fetched in ${totalSec}s.${' '.repeat(20)}\n`);
if (!result.ok) {
const msg = result.errorMessage || result.error || `HTTP ${result.status}`;
if (result.status === 0 || (typeof msg === 'string' && msg.toLowerCase().includes('timeout'))) {
console.error(`\n Workspace is taking too long to respond. Try: atris pull ${slug} --timeout=120`);
console.error(`\n Workspace timed out (large workspaces can take 60s+). Try: atris pull ${slug} --timeout=600`);
} else if (result.status === 409) {

@@ -233,5 +276,7 @@ console.error(`\n Computer is sleeping. Wake it first, then pull again.`);

// If output dir is empty (fresh clone) or --force, treat as first sync — pull everything
const effectiveManifest = (Object.keys(localFiles).length === 0 || force) ? null : manifest;
// Three-way compare
const baseFiles = (manifest && manifest.files) ? manifest.files : {};
const diff = threeWayCompare(localFiles, remoteFiles, manifest);
const diff = threeWayCompare(localFiles, remoteFiles, effectiveManifest);

@@ -324,2 +369,12 @@ // Apply changes

saveManifest(resolvedSlug || slug, newManifest);
// Save business config in the output dir so push/status work without args
const atrisDir = path.join(outputDir, '.atris');
fs.mkdirSync(atrisDir, { recursive: true });
fs.writeFileSync(path.join(atrisDir, 'business.json'), JSON.stringify({
slug: resolvedSlug || slug,
business_id: businessId,
workspace_id: workspaceId,
name: businessName,
}, null, 2));
}

@@ -326,0 +381,0 @@

@@ -7,10 +7,27 @@ const fs = require('fs');

const { loadManifest, saveManifest, computeFileHash, buildManifest, computeLocalHashes, threeWayCompare, SKIP_DIRS } = require('../lib/manifest');
const { sectionMerge } = require('../lib/section-merge');
async function pushAtris() {
const slug = process.argv[3];
let slug = process.argv[3];
// Auto-detect business from .atris/business.json in current dir
if (!slug || slug.startsWith('-')) {
const bizFile = path.join(process.cwd(), '.atris', 'business.json');
if (fs.existsSync(bizFile)) {
try {
const biz = JSON.parse(fs.readFileSync(bizFile, 'utf8'));
slug = biz.slug || biz.name;
} catch {}
}
// If still no slug (no .atris/business.json), need explicit name
if (!slug || slug.startsWith('-')) {
slug = null;
}
}
if (!slug || slug === '--help') {
console.log('Usage: atris push <business-slug> [--from <path>] [--force]');
console.log('Usage: atris push [business-slug] [--from <path>] [--force]');
console.log('');
console.log('Push local files to a Business Computer.');
console.log('If run inside a pulled folder, business is auto-detected.');
console.log('');

@@ -22,5 +39,5 @@ console.log('Options:');

console.log('Examples:');
console.log(' atris push Auto-detect from current folder');
console.log(' atris push pallet Push from atris/pallet/ or ./pallet/');
console.log(' atris push pallet --from ./my-dir/ Push from a custom directory');
console.log(' atris push pallet --force Override conflicts');
process.exit(0);

@@ -31,2 +48,21 @@ }

// Parse --only flag: filter which files to push
let onlyRaw = null;
const onlyEqArg = process.argv.find(a => a.startsWith('--only='));
if (onlyEqArg) {
onlyRaw = onlyEqArg.slice('--only='.length);
} else {
const onlyIdx = process.argv.indexOf('--only');
if (onlyIdx !== -1 && process.argv[onlyIdx + 1] && !process.argv[onlyIdx + 1].startsWith('-')) {
onlyRaw = process.argv[onlyIdx + 1];
}
}
const onlyPrefixes = onlyRaw
? onlyRaw.split(',').map(p => {
let norm = p.replace(/^\//, '');
if (norm && !norm.endsWith('/') && !norm.includes('.')) norm += '/';
return '/' + norm;
}).filter(Boolean)
: null;
const creds = loadCredentials();

@@ -43,2 +79,5 @@ if (!creds || !creds.token) {

sourceDir = path.resolve(process.argv[fromIdx + 1]);
} else if (fs.existsSync(path.join(process.cwd(), '.atris', 'business.json'))) {
// Inside a pulled folder — push from here
sourceDir = process.cwd();
} else {

@@ -121,16 +160,29 @@ const atrisDir = path.join(process.cwd(), 'atris', slug);

// Get snapshot with content to compute reliable hashes (server hash may differ)
// Loading indicator
const startTime = Date.now();
const spinner = ['|', '/', '-', '\\'];
let spinIdx = 0;
const loading = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
process.stdout.write(`\r Comparing with remote... ${spinner[spinIdx++ % 4]} ${elapsed}s`);
}, 250);
const snapshotResult = await apiRequestJson(
`/businesses/${businessId}/workspaces/${workspaceId}/snapshot?include_content=true`,
{ method: 'GET', token: creds.token, timeoutMs: 120000 }
{ method: 'GET', token: creds.token, timeoutMs: 300000 }
);
clearInterval(loading);
const totalSec = Math.floor((Date.now() - startTime) / 1000);
process.stdout.write(`\r Compared in ${totalSec}s.${' '.repeat(20)}\n`);
let remoteFiles = {};
const remoteContent = {}; // for section merge
if (snapshotResult.ok && snapshotResult.data && snapshotResult.data.files) {
for (const file of snapshotResult.data.files) {
if (file.path && !file.binary && file.content != null) {
// Compute hash from content (matches how computeLocalHashes works on raw bytes)
const rawBytes = Buffer.from(file.content, 'utf-8');
const hash = require('crypto').createHash('sha256').update(rawBytes).digest('hex');
remoteFiles[file.path] = { hash, size: rawBytes.length };
remoteContent[file.path] = file.content;
}

@@ -143,7 +195,19 @@ }

// Check if user is a member (not owner) — if so, filter to allowed paths
// Members can only push to /team/{name}/ and /journal/
let skippedPermission = [];
const role = snapshotResult.data?._role; // not available from snapshot, so we try the push and handle 403
// Determine what to push
const filesToPush = [];
// Apply --only filter
const matchesOnly = (filePath) => {
if (!onlyPrefixes) return true;
return onlyPrefixes.some(prefix => filePath.startsWith(prefix));
};
// Files we changed that remote didn't
for (const p of [...diff.toPush, ...diff.newLocal]) {
if (!matchesOnly(p)) continue;
const localPath = path.join(sourceDir, p.replace(/^\//, ''));

@@ -158,18 +222,34 @@ try {

// Force mode: also push conflicts
// Handle conflicts: try section-level merge first, then force, then flag
const conflictPaths = [];
if (force) {
for (const p of diff.conflicts) {
const localPath = path.join(sourceDir, p.replace(/^\//, ''));
try {
const content = fs.readFileSync(localPath, 'utf8');
filesToPush.push({ path: p, content });
} catch {
// skip
const mergedPaths = [];
for (const p of diff.conflicts) {
const localPath = path.join(sourceDir, p.replace(/^\//, ''));
let localContent;
try { localContent = fs.readFileSync(localPath, 'utf8'); } catch { continue; }
if (force) {
filesToPush.push({ path: p, content: localContent });
continue;
}
// Try section-level merge (only for .md files)
if (p.endsWith('.md') && remoteContent[p] && manifest && manifest.files && manifest.files[p]) {
// Get base content: we need what the file looked like at last sync.
// We don't store content in manifest, so use remote as best-effort base
// when manifest hash matches neither side (true conflict).
// For now, attempt merge with remote content and see if sections differ.
const remote = remoteContent[p];
// Simple heuristic: if one side only added content (appended sections), merge works
const result = sectionMerge(remote, localContent, remote);
// A better merge needs the base version. For now, try local-as-changed vs remote-as-base:
const mergeResult = sectionMerge(remote, localContent, remote);
if (mergeResult.merged && mergeResult.conflicts.length === 0 && mergeResult.merged !== remote) {
filesToPush.push({ path: p, content: mergeResult.merged });
mergedPaths.push(p);
continue;
}
}
} else {
for (const p of diff.conflicts) {
conflictPaths.push(p);
}
conflictPaths.push(p);
}

@@ -199,7 +279,34 @@

if (!result.ok) {
const msg = result.errorMessage || `HTTP ${result.status}`;
const msg = result.errorMessage || result.error || `HTTP ${result.status}`;
if (result.status === 409) {
console.error(` Computer is sleeping. Wake it first, then push.`);
} else if (result.status === 403) {
console.error(` Access denied: ${msg}`);
// Member scoping — retry with only team/ and journal/ files
const memberFiles = filesToPush.filter(f => f.path.startsWith('/team/') || f.path.startsWith('/journal/'));
const blockedFiles = filesToPush.filter(f => !f.path.startsWith('/team/') && !f.path.startsWith('/journal/'));
if (memberFiles.length > 0 && blockedFiles.length > 0) {
console.log(` You're a member — retrying with your team files only...`);
if (blockedFiles.length > 0) {
console.log(` Skipped (no permission): ${blockedFiles.map(f => f.path.replace(/^\//, '')).join(', ')}`);
}
const retry = await apiRequestJson(
`/businesses/${businessId}/workspaces/${workspaceId}/sync`,
{ method: 'POST', token: creds.token, body: { files: memberFiles }, headers: { 'X-Atris-Actor-Source': 'cli' } }
);
if (retry.ok) {
for (const f of memberFiles) {
console.log(` \u2191 ${f.path.replace(/^\//, '')} pushed`);
pushed++;
}
} else {
console.error(` Push failed after retry: ${retry.errorMessage || retry.error || retry.status}`);
process.exit(1);
}
} else {
console.error(` Access denied: you can only push to your own team/ folder.`);
if (blockedFiles.length > 0) {
console.error(` Blocked: ${blockedFiles.map(f => f.path.replace(/^\//, '')).join(', ')}`);
}
process.exit(1);
}
} else {

@@ -226,2 +333,6 @@ console.error(` Push failed: ${msg}`);

}
for (const p of mergedPaths) {
console.log(` \u2194 ${p.replace(/^\//, '')} auto-merged (different sections)`);
pushed++;
}
}

@@ -228,0 +339,0 @@

{
"name": "atris",
"version": "2.6.2",
"version": "2.6.3",
"description": "atrisDev (atris dev) - CLI for AI coding agents. Works with Claude Code, Cursor, Windsurf. Make any codebase AI-navigable.",

@@ -5,0 +5,0 @@ "main": "bin/atris.js",

@@ -74,7 +74,14 @@ const https = require('https');

req.on('error', reject);
// Socket idle timeout (fires if no data received for this duration)
if (timeoutMs > 0) {
req.setTimeout(timeoutMs, () => {
req.destroy(new Error('Request timeout'));
req.destroy(new Error(`Request timeout after ${Math.round(timeoutMs / 1000)}s — try --timeout=300`));
});
}
// Hard deadline — kill request after 2x the timeout regardless of activity
const hardDeadline = timeoutMs > 0
? setTimeout(() => { req.destroy(new Error(`Hard deadline exceeded (${Math.round(timeoutMs * 2 / 1000)}s)`)); }, timeoutMs * 2)
: null;
// Clear hard deadline when response completes
req.on('close', () => { if (hardDeadline) clearTimeout(hardDeadline); });

@@ -81,0 +88,0 @@ if (options.body) {