atris 3.14.0 → 3.15.11
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/AGENTS.md +24 -4
- package/README.md +4 -3
- package/atris/atris.md +38 -13
- package/atris/features/company-brain-sync/build.md +140 -0
- package/atris/features/company-brain-sync/idea.md +52 -0
- package/atris/features/company-brain-sync/validate.md +229 -0
- package/atris/skills/imessage/SKILL.md +44 -0
- package/bin/atris.js +56 -6
- package/commands/aeo.js +197 -0
- package/commands/align.js +1 -1
- package/commands/brain.js +840 -0
- package/commands/business-sync.js +716 -0
- package/commands/init.js +15 -3
- package/commands/integrations.js +128 -0
- package/commands/live.js +311 -0
- package/commands/now.js +263 -0
- package/commands/pull.js +121 -6
- package/commands/push.js +146 -48
- package/commands/task.js +1658 -18
- package/lib/company-brain-sync.js +178 -0
- package/lib/manifest.js +2 -1
- package/lib/task-db.js +271 -4
- package/package.json +12 -2
package/commands/push.js
CHANGED
|
@@ -9,6 +9,86 @@ const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
|
|
|
9
9
|
const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
|
|
10
10
|
const { assertSafeWorkspaceRoot } = require('../lib/workspace-safety');
|
|
11
11
|
|
|
12
|
+
function resolvePushSourceDir({ slug, argv = process.argv, cwd = process.cwd() }) {
|
|
13
|
+
const fromIdx = argv.indexOf('--from');
|
|
14
|
+
if (fromIdx !== -1 && argv[fromIdx + 1]) {
|
|
15
|
+
return path.resolve(cwd, argv[fromIdx + 1]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (fs.existsSync(path.join(cwd, '.atris', 'business.json'))) {
|
|
19
|
+
// A pulled business folder is the workspace root. Its cloud paths include
|
|
20
|
+
// the top-level `atris/` directory, so pushing from `<business>/atris`
|
|
21
|
+
// strips that prefix and makes a force push look like "delete atris/* and
|
|
22
|
+
// recreate everything at root". Keep the business folder itself as root.
|
|
23
|
+
return cwd;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const atrisDir = path.join(cwd, 'atris', slug);
|
|
27
|
+
const cwdDir = path.join(cwd, slug);
|
|
28
|
+
if (fs.existsSync(atrisDir)) return atrisDir;
|
|
29
|
+
if (fs.existsSync(cwdDir)) return cwdDir;
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function canonicalWorkspaceRoot(dir) {
|
|
35
|
+
try {
|
|
36
|
+
return fs.realpathSync(dir);
|
|
37
|
+
} catch {
|
|
38
|
+
return path.resolve(dir);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function basenameOfManifestPath(filePath) {
|
|
43
|
+
const cleaned = String(filePath || '').replace(/\/+$/, '');
|
|
44
|
+
const idx = cleaned.lastIndexOf('/');
|
|
45
|
+
return idx === -1 ? cleaned : cleaned.slice(idx + 1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isBusinessWorkspaceRoot(dir) {
|
|
49
|
+
return fs.existsSync(path.join(dir, '.atris', 'business.json')) && fs.existsSync(path.join(dir, 'atris'));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pathInScope(filePath, onlyPrefixes) {
|
|
53
|
+
if (!onlyPrefixes) return true;
|
|
54
|
+
return onlyPrefixes.some(prefix => filePath.startsWith(prefix));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildPushChangePlan({
|
|
58
|
+
localFiles = {},
|
|
59
|
+
baseFiles = {},
|
|
60
|
+
onlyPrefixes = null,
|
|
61
|
+
readFileContent,
|
|
62
|
+
} = {}) {
|
|
63
|
+
const filesToPush = [];
|
|
64
|
+
const deletedPaths = [];
|
|
65
|
+
|
|
66
|
+
for (const [filePath, fileInfo] of Object.entries(localFiles)) {
|
|
67
|
+
if (!pathInScope(filePath, onlyPrefixes)) continue;
|
|
68
|
+
const baseHash = baseFiles[filePath] ? baseFiles[filePath].hash : null;
|
|
69
|
+
if (!baseHash || fileInfo.hash !== baseHash) {
|
|
70
|
+
try {
|
|
71
|
+
const content = readFileContent ? readFileContent(filePath) : '';
|
|
72
|
+
filesToPush.push({ path: filePath, content });
|
|
73
|
+
} catch {
|
|
74
|
+
// File moved or became unreadable while planning; skip this cycle.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const filePath of Object.keys(baseFiles)) {
|
|
80
|
+
if (!pathInScope(filePath, onlyPrefixes)) continue;
|
|
81
|
+
if (basenameOfManifestPath(filePath).startsWith('.')) continue;
|
|
82
|
+
if (!localFiles[filePath]) {
|
|
83
|
+
deletedPaths.push(filePath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const filteredLocalCount = Object.keys(localFiles).filter(filePath => pathInScope(filePath, onlyPrefixes)).length;
|
|
88
|
+
const unchangedCount = Math.max(0, filteredLocalCount - filesToPush.length);
|
|
89
|
+
return { filesToPush, deletedPaths, unchangedCount };
|
|
90
|
+
}
|
|
91
|
+
|
|
12
92
|
async function pushAtris() {
|
|
13
93
|
const elapsedMs = startTimer();
|
|
14
94
|
let slug = process.argv[3];
|
|
@@ -27,7 +107,7 @@ async function pushAtris() {
|
|
|
27
107
|
}
|
|
28
108
|
|
|
29
109
|
if (!slug || slug === '--help') {
|
|
30
|
-
console.log('Usage: atris push [business] [--from <path>] [--only <prefix>] [--force]');
|
|
110
|
+
console.log('Usage: atris push [business] [--from <path>] [--only <prefix>] [--force] [--delete]');
|
|
31
111
|
console.log('');
|
|
32
112
|
console.log(' Push requires a fresh pull. If cloud has changed since your last pull,');
|
|
33
113
|
console.log(' the push will be blocked until you run `atris pull`. Use --force to override.');
|
|
@@ -36,11 +116,14 @@ async function pushAtris() {
|
|
|
36
116
|
console.log(' atris push pallet Push pallet/ or atris/pallet/');
|
|
37
117
|
console.log(' atris push pallet --only team/nate Only push files in team/nate/');
|
|
38
118
|
console.log(' atris push --force Bypass freshness check (force-push, may overwrite cloud changes)');
|
|
119
|
+
console.log(' atris push --delete Allow cloud deletes shown by --dry-run');
|
|
39
120
|
process.exit(0);
|
|
40
121
|
}
|
|
41
122
|
|
|
42
123
|
const force = process.argv.includes('--force');
|
|
43
124
|
const dryRun = process.argv.includes('--dry-run');
|
|
125
|
+
const allowDelete = process.argv.includes('--delete');
|
|
126
|
+
const allowCrossRootManifest = process.argv.includes('--allow-cross-root-manifest');
|
|
44
127
|
|
|
45
128
|
// Parse --only
|
|
46
129
|
let onlyRaw = null;
|
|
@@ -50,7 +133,7 @@ async function pushAtris() {
|
|
|
50
133
|
const oi = process.argv.indexOf('--only');
|
|
51
134
|
if (oi !== -1 && process.argv[oi + 1] && !process.argv[oi + 1].startsWith('-')) onlyRaw = process.argv[oi + 1];
|
|
52
135
|
}
|
|
53
|
-
|
|
136
|
+
let onlyPrefixes = onlyRaw
|
|
54
137
|
? onlyRaw.split(',').map(p => {
|
|
55
138
|
const wikiPrefix = normalizeWikiOnlyPrefix(p);
|
|
56
139
|
if (wikiPrefix) return `/${wikiPrefix.replace(/^\//, '')}`;
|
|
@@ -64,22 +147,11 @@ async function pushAtris() {
|
|
|
64
147
|
if (!creds || !creds.token) { console.error('Not logged in. Run: atris login'); process.exit(1); }
|
|
65
148
|
|
|
66
149
|
// Determine source directory
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
sourceDir = process.cwd();
|
|
73
|
-
} else {
|
|
74
|
-
const atrisDir = path.join(process.cwd(), 'atris', slug);
|
|
75
|
-
const cwdDir = path.join(process.cwd(), slug);
|
|
76
|
-
if (fs.existsSync(atrisDir)) sourceDir = atrisDir;
|
|
77
|
-
else if (fs.existsSync(cwdDir)) sourceDir = cwdDir;
|
|
78
|
-
else {
|
|
79
|
-
console.error(`No local folder found for "${slug}".`);
|
|
80
|
-
console.error('Run from inside a pulled folder, or: atris push pallet --from ./path');
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
150
|
+
const sourceDir = resolvePushSourceDir({ slug });
|
|
151
|
+
if (!sourceDir) {
|
|
152
|
+
console.error(`No local folder found for "${slug}".`);
|
|
153
|
+
console.error('Run from inside a pulled folder, or: atris push pallet --from ./path');
|
|
154
|
+
process.exit(1);
|
|
83
155
|
}
|
|
84
156
|
|
|
85
157
|
if (!fs.existsSync(sourceDir)) { console.error(`Source not found: ${sourceDir}`); process.exit(1); }
|
|
@@ -87,6 +159,10 @@ async function pushAtris() {
|
|
|
87
159
|
// Refuse to walk/upload dangerous paths ($HOME, /, /Users, system dirs).
|
|
88
160
|
assertSafeWorkspaceRoot(sourceDir, { slug, op: 'push from' });
|
|
89
161
|
|
|
162
|
+
if (!onlyPrefixes && isBusinessWorkspaceRoot(sourceDir)) {
|
|
163
|
+
onlyPrefixes = ['/atris/'];
|
|
164
|
+
}
|
|
165
|
+
|
|
90
166
|
// Resolve business — always refresh from API
|
|
91
167
|
let businessId, workspaceId, businessName, resolvedSlug;
|
|
92
168
|
const businesses = loadBusinesses();
|
|
@@ -145,6 +221,24 @@ async function pushAtris() {
|
|
|
145
221
|
const manifest = loadManifest(resolvedSlug || slug);
|
|
146
222
|
const localFiles = computeLocalHashes(sourceDir);
|
|
147
223
|
|
|
224
|
+
if (manifest && manifest.workspace_root && !allowCrossRootManifest) {
|
|
225
|
+
const manifestRoot = canonicalWorkspaceRoot(manifest.workspace_root);
|
|
226
|
+
const currentRoot = canonicalWorkspaceRoot(sourceDir);
|
|
227
|
+
if (manifestRoot !== currentRoot) {
|
|
228
|
+
console.log('');
|
|
229
|
+
console.log(' ✗ This folder has not been pulled since the current sync manifest was created.');
|
|
230
|
+
console.log('');
|
|
231
|
+
console.log(` Manifest folder: ${manifest.workspace_root}`);
|
|
232
|
+
console.log(` Current folder: ${sourceDir}`);
|
|
233
|
+
console.log('');
|
|
234
|
+
console.log(' To sync safely, run these from the folder you want to push:');
|
|
235
|
+
console.log(` atris pull ${resolvedSlug || slug} --keep-local --timeout 120`);
|
|
236
|
+
console.log(` atris push ${resolvedSlug || slug} --dry-run`);
|
|
237
|
+
console.log(` atris push ${resolvedSlug || slug}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
148
242
|
if (Object.keys(localFiles).length === 0) {
|
|
149
243
|
console.log(`\nNo files to push from ${sourceDir}`);
|
|
150
244
|
return;
|
|
@@ -240,34 +334,12 @@ async function pushAtris() {
|
|
|
240
334
|
// Compare local hashes to manifest — NO server call needed
|
|
241
335
|
// Files where local hash differs from manifest = changed locally
|
|
242
336
|
const baseFiles = (manifest && manifest.files) ? manifest.files : {};
|
|
243
|
-
const filesToPush =
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (!baseHash || fileInfo.hash !== baseHash) {
|
|
250
|
-
// Changed or new — push it
|
|
251
|
-
const localPath = path.join(sourceDir, filePath.replace(/^\//, ''));
|
|
252
|
-
try {
|
|
253
|
-
const content = fs.readFileSync(localPath, 'utf8');
|
|
254
|
-
filesToPush.push({ path: filePath, content });
|
|
255
|
-
} catch {}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
for (const filePath of Object.keys(baseFiles)) {
|
|
260
|
-
if (onlyPrefixes && !onlyPrefixes.some(p => filePath.startsWith(p))) continue;
|
|
261
|
-
if (!localFiles[filePath]) {
|
|
262
|
-
deletedPaths.push(filePath);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const filteredLocalCount = Object.keys(localFiles).filter(filePath => {
|
|
267
|
-
if (!onlyPrefixes) return true;
|
|
268
|
-
return onlyPrefixes.some(prefix => filePath.startsWith(prefix));
|
|
269
|
-
}).length;
|
|
270
|
-
const unchangedCount = Math.max(0, filteredLocalCount - filesToPush.length);
|
|
337
|
+
const { filesToPush, deletedPaths, unchangedCount } = buildPushChangePlan({
|
|
338
|
+
localFiles,
|
|
339
|
+
baseFiles,
|
|
340
|
+
onlyPrefixes,
|
|
341
|
+
readFileContent: (filePath) => fs.readFileSync(path.join(sourceDir, filePath.replace(/^\//, '')), 'utf8'),
|
|
342
|
+
});
|
|
271
343
|
|
|
272
344
|
if (filesToPush.length === 0 && deletedPaths.length === 0) {
|
|
273
345
|
console.log('\n Already up to date.\n');
|
|
@@ -290,9 +362,28 @@ async function pushAtris() {
|
|
|
290
362
|
if (deletedPaths.length > 0) parts.push(`${deletedPaths.length} would be deleted`);
|
|
291
363
|
if (unchangedCount > 0) parts.push(`${unchangedCount} unchanged`);
|
|
292
364
|
console.log(`\n ${parts.join(', ')}. (--dry-run, nothing sent)\n`);
|
|
365
|
+
if (deletedPaths.length > 0) {
|
|
366
|
+
console.log(' Deletes require an explicit real push with --delete.');
|
|
367
|
+
console.log(' If these deletes are unexpected, run `atris pull --keep-local` first.\n');
|
|
368
|
+
}
|
|
293
369
|
return;
|
|
294
370
|
}
|
|
295
371
|
|
|
372
|
+
if (deletedPaths.length > 0 && !allowDelete) {
|
|
373
|
+
console.log('');
|
|
374
|
+
console.log(` ✗ Refusing to delete ${deletedPaths.length} cloud file${deletedPaths.length === 1 ? '' : 's'} without --delete.`);
|
|
375
|
+
console.log('');
|
|
376
|
+
console.log(' Preview first:');
|
|
377
|
+
console.log(` atris push ${resolvedSlug || slug} --dry-run`);
|
|
378
|
+
console.log('');
|
|
379
|
+
console.log(' If the deletes are intentional:');
|
|
380
|
+
console.log(` atris push ${resolvedSlug || slug} --delete`);
|
|
381
|
+
console.log('');
|
|
382
|
+
console.log(' If the deletes are surprising, pull cloud truth first:');
|
|
383
|
+
console.log(` atris pull ${resolvedSlug || slug} --keep-local --timeout 120`);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
|
|
296
387
|
let pushed = 0;
|
|
297
388
|
let deleted = 0;
|
|
298
389
|
let skipped = [];
|
|
@@ -526,7 +617,7 @@ async function pushAtris() {
|
|
|
526
617
|
for (const filePath of deletedConfirmed) {
|
|
527
618
|
delete updatedFiles[filePath];
|
|
528
619
|
}
|
|
529
|
-
saveManifest(resolvedSlug || slug, buildManifest(updatedFiles, null));
|
|
620
|
+
saveManifest(resolvedSlug || slug, buildManifest(updatedFiles, null, { workspaceRoot: sourceDir }));
|
|
530
621
|
|
|
531
622
|
// Telemetry — outcome reflects actual run quality, not just exit-code-zero.
|
|
532
623
|
// Partial delete failures or rate-limit retries mean the run was NOT a clean win;
|
|
@@ -558,4 +649,11 @@ async function pushAtris() {
|
|
|
558
649
|
});
|
|
559
650
|
}
|
|
560
651
|
|
|
561
|
-
module.exports = {
|
|
652
|
+
module.exports = {
|
|
653
|
+
pushAtris,
|
|
654
|
+
buildPushChangePlan,
|
|
655
|
+
resolvePushSourceDir,
|
|
656
|
+
canonicalWorkspaceRoot,
|
|
657
|
+
basenameOfManifestPath,
|
|
658
|
+
isBusinessWorkspaceRoot,
|
|
659
|
+
};
|