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/now.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const NOW_PATH = path.join('atris', 'now.md');
|
|
5
|
+
|
|
6
|
+
function todayIso() {
|
|
7
|
+
return new Date().toISOString().split('T')[0];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ensureAtrisDir(root = process.cwd()) {
|
|
11
|
+
const atrisDir = path.join(root, 'atris');
|
|
12
|
+
if (!fs.existsSync(atrisDir)) {
|
|
13
|
+
throw new Error('atris/ folder not found. Run "atris init" first.');
|
|
14
|
+
}
|
|
15
|
+
return atrisDir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hasWorkspaceMarkers(atrisDir) {
|
|
19
|
+
return fs.existsSync(path.join(atrisDir, 'MAP.md')) || fs.existsSync(path.join(atrisDir, 'TODO.md'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findChildWorkspaces(root = process.cwd()) {
|
|
23
|
+
if (!fs.existsSync(root)) return [];
|
|
24
|
+
|
|
25
|
+
return fs
|
|
26
|
+
.readdirSync(root, { withFileTypes: true })
|
|
27
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
28
|
+
.map((entry) => {
|
|
29
|
+
const workspaceRoot = path.join(root, entry.name);
|
|
30
|
+
const atrisDir = path.join(workspaceRoot, 'atris');
|
|
31
|
+
if (!fs.existsSync(atrisDir) || !hasWorkspaceMarkers(atrisDir)) return null;
|
|
32
|
+
const mapPath = path.join(atrisDir, 'MAP.md');
|
|
33
|
+
const todoPath = path.join(atrisDir, 'TODO.md');
|
|
34
|
+
return {
|
|
35
|
+
slug: entry.name,
|
|
36
|
+
root: workspaceRoot,
|
|
37
|
+
atrisDir,
|
|
38
|
+
mapPath,
|
|
39
|
+
todoPath,
|
|
40
|
+
nowPath: path.join(atrisDir, 'now.md'),
|
|
41
|
+
};
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readFirstHeading(filePath) {
|
|
48
|
+
if (!fs.existsSync(filePath)) return null;
|
|
49
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
50
|
+
const line = content.split(/\r?\n/).find((l) => l.trim().startsWith('#'));
|
|
51
|
+
return line ? line.replace(/^#+\s*/, '').trim() : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function countMatches(filePath, pattern) {
|
|
55
|
+
if (!fs.existsSync(filePath)) return 0;
|
|
56
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
57
|
+
return (content.match(pattern) || []).length;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function currentJournalPath(root = process.cwd()) {
|
|
61
|
+
const now = new Date();
|
|
62
|
+
const year = String(now.getFullYear());
|
|
63
|
+
const date = todayIso();
|
|
64
|
+
return path.join(root, 'atris', 'logs', year, `${date}.md`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderDefaultNow(root = process.cwd()) {
|
|
68
|
+
const atrisDir = ensureAtrisDir(root);
|
|
69
|
+
const mapHeading = readFirstHeading(path.join(atrisDir, 'MAP.md')) || 'MAP not filled yet';
|
|
70
|
+
const todoPath = path.join(atrisDir, 'TODO.md');
|
|
71
|
+
const journalPath = currentJournalPath(root);
|
|
72
|
+
const backlogCount = countMatches(todoPath, /^-\s+\*\*.+?\*\*/gm);
|
|
73
|
+
const inboxCount = countMatches(journalPath, /^-\s+\*\*I\d+:/gm);
|
|
74
|
+
const completedCount = countMatches(journalPath, /^-\s+\*\*C\d+:/gm);
|
|
75
|
+
const generated = todayIso();
|
|
76
|
+
|
|
77
|
+
return `# now
|
|
78
|
+
|
|
79
|
+
> Current operating truth for this workspace.
|
|
80
|
+
> Read this first. Follow links only when needed.
|
|
81
|
+
|
|
82
|
+
Last updated: ${generated}
|
|
83
|
+
|
|
84
|
+
## What Matters Now
|
|
85
|
+
|
|
86
|
+
- Decide the next useful move before opening more context.
|
|
87
|
+
|
|
88
|
+
## Current Priority
|
|
89
|
+
|
|
90
|
+
- Keep the workspace coherent and useful for the next human or agent.
|
|
91
|
+
|
|
92
|
+
## Signals
|
|
93
|
+
|
|
94
|
+
- Map: ${mapHeading}
|
|
95
|
+
- TODO items visible: ${backlogCount}
|
|
96
|
+
- Inbox items today: ${inboxCount}
|
|
97
|
+
- Completed receipts today: ${completedCount}
|
|
98
|
+
|
|
99
|
+
## Watchouts
|
|
100
|
+
|
|
101
|
+
- Do not treat old logs as current truth unless this file links to them.
|
|
102
|
+
- Do not create motion for its own sake.
|
|
103
|
+
- If facts conflict, surface the conflict and cite the receipts.
|
|
104
|
+
|
|
105
|
+
## Next Move
|
|
106
|
+
|
|
107
|
+
- Read \`atris/MAP.md\`, \`atris/TODO.md\`, and today's journal only as needed for the task in front of you.
|
|
108
|
+
|
|
109
|
+
## Receipts
|
|
110
|
+
|
|
111
|
+
- \`atris/MAP.md\`
|
|
112
|
+
- \`atris/TODO.md\`
|
|
113
|
+
- \`${path.relative(root, journalPath)}\`
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderPortfolioNow(root = process.cwd()) {
|
|
118
|
+
const workspaces = findChildWorkspaces(root);
|
|
119
|
+
if (workspaces.length === 0) {
|
|
120
|
+
throw new Error('atris/ folder not found. Run "atris init" first.');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const generated = todayIso();
|
|
124
|
+
const lines = workspaces.map((workspace) => {
|
|
125
|
+
const heading = readFirstHeading(workspace.mapPath) || workspace.slug;
|
|
126
|
+
const todoCount = countMatches(workspace.todoPath, /^-\s+\*\*.+?\*\*/gm);
|
|
127
|
+
const nowState = fs.existsSync(workspace.nowPath) ? 'has now.md' : 'needs now.md';
|
|
128
|
+
return `- ${workspace.slug}: ${heading}; ${todoCount} visible TODO item${todoCount === 1 ? '' : 's'}; ${nowState}.`;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return `# now
|
|
132
|
+
|
|
133
|
+
> Current operating truth for this portfolio of Atris workspaces.
|
|
134
|
+
> Read this first. Then enter the specific workspace that matters.
|
|
135
|
+
|
|
136
|
+
Last updated: ${generated}
|
|
137
|
+
|
|
138
|
+
## What Matters Now
|
|
139
|
+
|
|
140
|
+
- Keep the active business workspaces easy to scan, update, and hand off.
|
|
141
|
+
|
|
142
|
+
## Current Priority
|
|
143
|
+
|
|
144
|
+
- Use the child workspace with the right slug; avoid creating duplicate business brains.
|
|
145
|
+
|
|
146
|
+
## Workspace Signals
|
|
147
|
+
|
|
148
|
+
${lines.join('\n')}
|
|
149
|
+
|
|
150
|
+
## Watchouts
|
|
151
|
+
|
|
152
|
+
- Parent status is a map, not the source of truth for each business.
|
|
153
|
+
- Each active workspace should own its own \`atris/now.md\`.
|
|
154
|
+
- If slugs conflict, resolve the workspace identity before pushing or pulling.
|
|
155
|
+
|
|
156
|
+
## Next Move
|
|
157
|
+
|
|
158
|
+
- Run \`atris now\` inside the workspace you are about to operate.
|
|
159
|
+
|
|
160
|
+
## Receipts
|
|
161
|
+
|
|
162
|
+
${workspaces.map((workspace) => `- \`${workspace.slug}/atris/MAP.md\``).join('\n')}
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ensureNowFile(root = process.cwd()) {
|
|
167
|
+
let atrisDir = path.join(root, 'atris');
|
|
168
|
+
const isWorkspace = fs.existsSync(atrisDir) && hasWorkspaceMarkers(atrisDir);
|
|
169
|
+
const childWorkspaces = isWorkspace ? [] : findChildWorkspaces(root);
|
|
170
|
+
if (!isWorkspace && childWorkspaces.length === 0) {
|
|
171
|
+
ensureAtrisDir(root);
|
|
172
|
+
}
|
|
173
|
+
if (!isWorkspace && childWorkspaces.length > 0) {
|
|
174
|
+
fs.mkdirSync(atrisDir, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
const nowPath = path.join(atrisDir, 'now.md');
|
|
177
|
+
if (!fs.existsSync(nowPath)) {
|
|
178
|
+
const content = isWorkspace ? renderDefaultNow(root) : renderPortfolioNow(root);
|
|
179
|
+
fs.writeFileSync(nowPath, content, 'utf8');
|
|
180
|
+
return { created: true, path: nowPath };
|
|
181
|
+
}
|
|
182
|
+
return { created: false, path: nowPath };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function refreshNowFile(root = process.cwd()) {
|
|
186
|
+
const atrisDir = path.join(root, 'atris');
|
|
187
|
+
const isWorkspace = fs.existsSync(atrisDir) && hasWorkspaceMarkers(atrisDir);
|
|
188
|
+
const childWorkspaces = isWorkspace ? [] : findChildWorkspaces(root);
|
|
189
|
+
if (!isWorkspace && childWorkspaces.length === 0) {
|
|
190
|
+
ensureAtrisDir(root);
|
|
191
|
+
}
|
|
192
|
+
if (!isWorkspace && childWorkspaces.length > 0) {
|
|
193
|
+
fs.mkdirSync(atrisDir, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
const nowPath = path.join(atrisDir, 'now.md');
|
|
196
|
+
const content = isWorkspace ? renderDefaultNow(root) : renderPortfolioNow(root);
|
|
197
|
+
fs.writeFileSync(nowPath, content, 'utf8');
|
|
198
|
+
return { path: nowPath };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
|
|
202
|
+
const help = args.includes('--help') || args.includes('-h');
|
|
203
|
+
if (help) {
|
|
204
|
+
console.log('Usage: atris now [--init|--refresh|--all|--path]');
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log('Show the current operating truth for this workspace.');
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(' atris now Show atris/now.md');
|
|
209
|
+
console.log(' atris now --init Create atris/now.md if missing');
|
|
210
|
+
console.log(' atris now --refresh Regenerate a small local now.md');
|
|
211
|
+
console.log(' atris now --all Refresh this parent and every child Atris workspace');
|
|
212
|
+
console.log(' atris now --path Print the file path only');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const init = args.includes('--init');
|
|
217
|
+
const refresh = args.includes('--refresh');
|
|
218
|
+
const all = args.includes('--all');
|
|
219
|
+
const pathOnly = args.includes('--path');
|
|
220
|
+
|
|
221
|
+
let result;
|
|
222
|
+
if (all) {
|
|
223
|
+
const workspaces = findChildWorkspaces(root);
|
|
224
|
+
for (const workspace of workspaces) {
|
|
225
|
+
refreshNowFile(workspace.root);
|
|
226
|
+
}
|
|
227
|
+
result = refreshNowFile(root);
|
|
228
|
+
if (!pathOnly) {
|
|
229
|
+
console.log(`Refreshed ${workspaces.length} child workspace${workspaces.length === 1 ? '' : 's'}.`);
|
|
230
|
+
console.log('');
|
|
231
|
+
}
|
|
232
|
+
} else if (refresh) {
|
|
233
|
+
result = refreshNowFile(root);
|
|
234
|
+
} else if (init) {
|
|
235
|
+
result = ensureNowFile(root);
|
|
236
|
+
} else {
|
|
237
|
+
result = ensureNowFile(root);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const rel = path.relative(root, result.path);
|
|
241
|
+
if (pathOnly) {
|
|
242
|
+
console.log(rel);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (result.created) {
|
|
247
|
+
console.log(`Created ${rel}`);
|
|
248
|
+
console.log('');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const content = fs.readFileSync(result.path, 'utf8').trimEnd();
|
|
252
|
+
console.log(content);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
NOW_PATH,
|
|
257
|
+
ensureNowFile,
|
|
258
|
+
findChildWorkspaces,
|
|
259
|
+
nowAtris,
|
|
260
|
+
refreshNowFile,
|
|
261
|
+
renderDefaultNow,
|
|
262
|
+
renderPortfolioNow,
|
|
263
|
+
};
|
package/commands/pull.js
CHANGED
|
@@ -11,6 +11,13 @@ const { loadManifest, saveManifest, computeFileHash, buildManifest, computeLocal
|
|
|
11
11
|
const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
|
|
12
12
|
const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
|
|
13
13
|
const { resolveSafeOutputDir } = require('../lib/workspace-safety');
|
|
14
|
+
const {
|
|
15
|
+
buildConflictReviewPacket,
|
|
16
|
+
readBaseContent,
|
|
17
|
+
removeBaseContents,
|
|
18
|
+
writeBaseContents,
|
|
19
|
+
writeConflictReviewPacket,
|
|
20
|
+
} = require('../lib/company-brain-sync');
|
|
14
21
|
|
|
15
22
|
function pruneEmptyParentDirs(filePath, stopDir) {
|
|
16
23
|
let current = path.dirname(filePath);
|
|
@@ -26,11 +33,50 @@ function pruneEmptyParentDirs(filePath, stopDir) {
|
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
function isBusinessWorkspaceRoot(dir) {
|
|
37
|
+
return fs.existsSync(path.join(dir, '.atris', 'business.json')) && fs.existsSync(path.join(dir, 'atris'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function syncTimestamp() {
|
|
41
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildPullConflictReviewPacket(outputDir, conflictChanges, remoteContents = {}, timestamp = syncTimestamp()) {
|
|
45
|
+
const baseContents = {};
|
|
46
|
+
const localContents = {};
|
|
47
|
+
const normalizedRemoteContents = {};
|
|
48
|
+
|
|
49
|
+
for (const change of conflictChanges) {
|
|
50
|
+
const p = change.path;
|
|
51
|
+
const localPath = path.join(outputDir, p.replace(/^\//, ''));
|
|
52
|
+
try {
|
|
53
|
+
localContents[p] = fs.readFileSync(localPath, 'utf8');
|
|
54
|
+
} catch {
|
|
55
|
+
localContents[p] = '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const baseContent = readBaseContent(outputDir, p);
|
|
59
|
+
if (baseContent !== null) baseContents[p] = baseContent;
|
|
60
|
+
|
|
61
|
+
normalizedRemoteContents[p] = Object.prototype.hasOwnProperty.call(remoteContents, p)
|
|
62
|
+
? remoteContents[p]
|
|
63
|
+
: '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return buildConflictReviewPacket({
|
|
67
|
+
plan: { changes: conflictChanges },
|
|
68
|
+
baseContents,
|
|
69
|
+
localContents,
|
|
70
|
+
remoteContents: normalizedRemoteContents,
|
|
71
|
+
timestamp,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
29
75
|
async function pullAtris() {
|
|
30
76
|
let arg = process.argv[3];
|
|
31
77
|
|
|
32
78
|
if (arg === '--help') {
|
|
33
|
-
console.log('Usage: atris pull [business] [--into <path>] [--only <prefix>] [--keep-local] [--timeout <seconds>]');
|
|
79
|
+
console.log('Usage: atris pull [business] [--into <path>] [--only <prefix>] [--keep-local] [--timeout <seconds>] [--dry-run]');
|
|
34
80
|
console.log('');
|
|
35
81
|
console.log(' Pull is force-overwrite by default. Cloud is the source of truth.');
|
|
36
82
|
console.log(' Local files that conflict with cloud are replaced by the cloud version.');
|
|
@@ -40,6 +86,7 @@ async function pullAtris() {
|
|
|
40
86
|
console.log(' atris pull doordash --into /tmp/doordash');
|
|
41
87
|
console.log(' atris pull doordash --only atris/wiki/');
|
|
42
88
|
console.log(' atris pull --keep-local Preserve conflicting local edits as .remote files (legacy)');
|
|
89
|
+
console.log(' atris pull --dry-run Preview pull changes without writing local files');
|
|
43
90
|
return;
|
|
44
91
|
}
|
|
45
92
|
|
|
@@ -127,6 +174,8 @@ async function pullBusiness(slug) {
|
|
|
127
174
|
// --keep-local opts back into the legacy three-way merge with .remote conflict files.
|
|
128
175
|
// --force is still accepted as an alias for the default for muscle-memory.
|
|
129
176
|
const force = !process.argv.includes('--keep-local');
|
|
177
|
+
const failOnConflict = process.argv.includes('--fail-on-conflict');
|
|
178
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
130
179
|
|
|
131
180
|
// Parse --only flag: comma-separated directory prefixes to filter
|
|
132
181
|
// Supports both --only=team/,context/ and --only team/,context/
|
|
@@ -140,7 +189,7 @@ async function pullBusiness(slug) {
|
|
|
140
189
|
onlyRaw = process.argv[onlyIdx + 1];
|
|
141
190
|
}
|
|
142
191
|
}
|
|
143
|
-
|
|
192
|
+
let onlyPrefixes = onlyRaw
|
|
144
193
|
? onlyRaw.split(',').map(p => {
|
|
145
194
|
let norm = p.replace(/^\//, '');
|
|
146
195
|
const wikiPrefix = normalizeWikiOnlyPrefix(norm);
|
|
@@ -202,6 +251,10 @@ async function pullBusiness(slug) {
|
|
|
202
251
|
// for a stray cwd to cause atris to delete user files.
|
|
203
252
|
({ dir: outputDir } = resolveSafeOutputDir(outputDir, { slug, op: 'pull into' }));
|
|
204
253
|
|
|
254
|
+
if (!onlyPrefixes && isBusinessWorkspaceRoot(outputDir)) {
|
|
255
|
+
onlyPrefixes = ['atris/'];
|
|
256
|
+
}
|
|
257
|
+
|
|
205
258
|
// Resolve business ID — always refresh from API to avoid stale workspace_id
|
|
206
259
|
let businessId, workspaceId, businessName, resolvedSlug;
|
|
207
260
|
let localSlug = slug;
|
|
@@ -283,6 +336,14 @@ async function pullBusiness(slug) {
|
|
|
283
336
|
// Load manifest (last sync state)
|
|
284
337
|
const manifest = loadManifest(resolvedSlug || slug);
|
|
285
338
|
const timeSince = manifest ? _timeSince(manifest.last_sync) : null;
|
|
339
|
+
const localFilesBeforePull = fs.existsSync(outputDir) ? computeLocalHashes(outputDir) : {};
|
|
340
|
+
const manifestRootMatchesOutput = !manifest || !manifest.workspace_root || (() => {
|
|
341
|
+
try {
|
|
342
|
+
return fs.realpathSync(manifest.workspace_root) === fs.realpathSync(outputDir);
|
|
343
|
+
} catch {
|
|
344
|
+
return path.resolve(manifest.workspace_root || '') === path.resolve(outputDir);
|
|
345
|
+
}
|
|
346
|
+
})();
|
|
286
347
|
|
|
287
348
|
console.log('');
|
|
288
349
|
console.log(`Pulling ${businessName}...` + (timeSince ? ` (last synced ${timeSince})` : ''));
|
|
@@ -297,7 +358,12 @@ async function pullBusiness(slug) {
|
|
|
297
358
|
}, 250);
|
|
298
359
|
|
|
299
360
|
// Smart pull: if we have a manifest (not first sync), fetch hashes first, then only changed content
|
|
300
|
-
const hasManifest = manifest
|
|
361
|
+
const hasManifest = manifest
|
|
362
|
+
&& manifest.files
|
|
363
|
+
&& Object.keys(manifest.files).length > 0
|
|
364
|
+
&& Object.keys(localFilesBeforePull).length > 0
|
|
365
|
+
&& manifestRootMatchesOutput
|
|
366
|
+
&& !force;
|
|
301
367
|
let result;
|
|
302
368
|
|
|
303
369
|
const pathsParam = onlyPrefixes ? `&paths=${encodeURIComponent(onlyPrefixes.map(p => p.replace(/\/$/, '')).join(','))}` : '';
|
|
@@ -483,7 +549,7 @@ async function pullBusiness(slug) {
|
|
|
483
549
|
}
|
|
484
550
|
|
|
485
551
|
// Compute local file hashes
|
|
486
|
-
const localFiles =
|
|
552
|
+
const localFiles = localFilesBeforePull;
|
|
487
553
|
|
|
488
554
|
// If output dir is empty (fresh clone) or --force, treat as first sync — pull everything
|
|
489
555
|
const effectiveManifest = (Object.keys(localFiles).length === 0 || force) ? null : manifest;
|
|
@@ -491,11 +557,38 @@ async function pullBusiness(slug) {
|
|
|
491
557
|
// Three-way compare
|
|
492
558
|
const diff = threeWayCompare(localFiles, remoteFiles, effectiveManifest);
|
|
493
559
|
|
|
560
|
+
if (dryRun) {
|
|
561
|
+
console.log('');
|
|
562
|
+
for (const p of [...diff.toPull, ...diff.newRemote]) {
|
|
563
|
+
const label = diff.newRemote.includes(p) ? 'new on computer' : 'updated on computer';
|
|
564
|
+
const icon = diff.newRemote.includes(p) ? '+' : '\u2193';
|
|
565
|
+
console.log(` ${icon} ${p.replace(/^\//, '')} ${label} (dry run)`);
|
|
566
|
+
}
|
|
567
|
+
for (const p of diff.conflicts) {
|
|
568
|
+
console.log(` ! ${p.replace(/^\//, '')} conflict (dry run)`);
|
|
569
|
+
}
|
|
570
|
+
for (const p of diff.deletedRemote) {
|
|
571
|
+
console.log(` - ${p.replace(/^\//, '')} deleted on computer (dry run)`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const parts = [];
|
|
575
|
+
const pullCount = diff.toPull.length + diff.newRemote.length;
|
|
576
|
+
if (pullCount > 0) parts.push(`${pullCount} would be pulled`);
|
|
577
|
+
if (diff.deletedRemote.length > 0) parts.push(`${diff.deletedRemote.length} local file${diff.deletedRemote.length === 1 ? '' : 's'} would be removed`);
|
|
578
|
+
if (diff.unchanged.length > 0) parts.push(`${diff.unchanged.length} unchanged`);
|
|
579
|
+
if (diff.conflicts.length > 0) parts.push(`${diff.conflicts.length} conflict${diff.conflicts.length === 1 ? '' : 's'}`);
|
|
580
|
+
if (parts.length === 0) parts.push('no changes');
|
|
581
|
+
console.log(`\n ${parts.join(', ')}. (--dry-run, nothing written)\n`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
494
585
|
// Apply changes
|
|
495
586
|
let pulled = 0;
|
|
496
587
|
let deleted = 0;
|
|
497
588
|
let conflictCount = 0;
|
|
498
589
|
let unchangedCount = diff.unchanged.length;
|
|
590
|
+
const conflictChanges = [];
|
|
591
|
+
const conflictRemoteContents = {};
|
|
499
592
|
|
|
500
593
|
console.log('');
|
|
501
594
|
|
|
@@ -526,6 +619,8 @@ async function pullBusiness(slug) {
|
|
|
526
619
|
} else {
|
|
527
620
|
// Save remote version alongside local
|
|
528
621
|
const content = remoteContent[p];
|
|
622
|
+
conflictRemoteContents[p] = content || '';
|
|
623
|
+
conflictChanges.push({ path: p, status: 'conflict_updated', action: 'review' });
|
|
529
624
|
if (content || content === '') {
|
|
530
625
|
const localPath = path.join(outputDir, p.replace(/^\//, '') + '.remote');
|
|
531
626
|
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
@@ -553,6 +648,8 @@ async function pullBusiness(slug) {
|
|
|
553
648
|
deleted++;
|
|
554
649
|
} else {
|
|
555
650
|
console.log(` \u26A0 ${p.replace(/^\//, '')} deleted on computer, but you changed it locally`);
|
|
651
|
+
conflictRemoteContents[p] = '';
|
|
652
|
+
conflictChanges.push({ path: p, status: 'conflict_remote_deleted_local_updated', action: 'review' });
|
|
556
653
|
conflictCount++;
|
|
557
654
|
}
|
|
558
655
|
}
|
|
@@ -640,6 +737,22 @@ async function pullBusiness(slug) {
|
|
|
640
737
|
if (unchangedCount > 0) parts.push(`${unchangedCount} unchanged`);
|
|
641
738
|
if (conflictCount > 0) parts.push(`${conflictCount} conflict${conflictCount > 1 ? 's' : ''}`);
|
|
642
739
|
if (parts.length > 0) console.log(` ${parts.join(', ')}.`);
|
|
740
|
+
if (failOnConflict && conflictCount > 0) {
|
|
741
|
+
const timestamp = syncTimestamp();
|
|
742
|
+
const packet = buildPullConflictReviewPacket(outputDir, conflictChanges, conflictRemoteContents, timestamp);
|
|
743
|
+
writeConflictReviewPacket(outputDir, packet);
|
|
744
|
+
console.log('');
|
|
745
|
+
console.log(` Sync paused: ${conflictCount} conflict${conflictCount === 1 ? '' : 's'} need review before publishing.`);
|
|
746
|
+
console.log(` Review packet: .atris/sync/conflicts/${timestamp}/summary.md`);
|
|
747
|
+
console.log(' Resolve the conflict, then run sync again.');
|
|
748
|
+
process.exit(2);
|
|
749
|
+
}
|
|
750
|
+
if (remoteFiles['atris/now.md'] || remoteFiles['/atris/now.md']) {
|
|
751
|
+
const nowLocal = path.join(outputDir, 'atris', 'now.md');
|
|
752
|
+
if (fs.existsSync(nowLocal)) {
|
|
753
|
+
console.log(' now: atris/now.md is current.');
|
|
754
|
+
}
|
|
755
|
+
}
|
|
643
756
|
|
|
644
757
|
// Get current commit hash from remote (for manifest)
|
|
645
758
|
let commitHash = null;
|
|
@@ -693,8 +806,10 @@ async function pullBusiness(slug) {
|
|
|
693
806
|
}
|
|
694
807
|
manifestFiles = merged;
|
|
695
808
|
}
|
|
696
|
-
const newManifest = buildManifest(manifestFiles, commitHash);
|
|
809
|
+
const newManifest = buildManifest(manifestFiles, commitHash, { workspaceRoot: outputDir });
|
|
697
810
|
saveManifest(resolvedSlug || slug, newManifest);
|
|
811
|
+
writeBaseContents(outputDir, remoteContent);
|
|
812
|
+
removeBaseContents(outputDir, diff.deletedRemote);
|
|
698
813
|
|
|
699
814
|
// Save business config in the output dir so push/status work without args
|
|
700
815
|
const atrisDir = path.join(outputDir, '.atris');
|
|
@@ -901,4 +1016,4 @@ async function pullMemberJournal(token, agentId, memberName, memberDir) {
|
|
|
901
1016
|
return synced;
|
|
902
1017
|
}
|
|
903
1018
|
|
|
904
|
-
module.exports = { pullAtris };
|
|
1019
|
+
module.exports = { buildPullConflictReviewPacket, pullAtris };
|