djournal 0.4.0 → 0.4.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/.agents/adapters/shared/journal-hook.js +57 -21
- package/bin/journal.js +5 -2
- package/docs/remote-sync.md +3 -0
- package/package.json +1 -1
|
@@ -62,12 +62,22 @@ function activeWork(root) {
|
|
|
62
62
|
const context = projectContext(root);
|
|
63
63
|
const state = JSON.parse(fs.readFileSync(path.join(context.journalRoot, "state.json"), "utf8"));
|
|
64
64
|
if (typeof state.active_work_name !== "string" || !state.active_work_name) return null;
|
|
65
|
-
|
|
65
|
+
return workBySlug(root, state.active_work_name);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function workBySlug(root, slug) {
|
|
72
|
+
try {
|
|
73
|
+
if (typeof slug !== "string" || !slug || slug.includes("/") || slug.includes("\\") || slug === "." || slug === "..") return null;
|
|
74
|
+
const context = projectContext(root);
|
|
75
|
+
const work = path.join(context.journalRoot, "work", slug, "work.md");
|
|
66
76
|
if (!fs.existsSync(work)) return null;
|
|
67
77
|
const metadata = parseFrontmatter(fs.readFileSync(work, "utf8"));
|
|
68
78
|
const config = readConfig(context);
|
|
69
|
-
const shared = config.sharing?.sharedWorkItems && Object.prototype.hasOwnProperty.call(config.sharing.sharedWorkItems,
|
|
70
|
-
return { slug
|
|
79
|
+
const shared = config.sharing?.sharedWorkItems && Object.prototype.hasOwnProperty.call(config.sharing.sharedWorkItems, slug);
|
|
80
|
+
return { slug, visibility: metadata.visibility || "local_only", shared: !!shared, path: work };
|
|
71
81
|
} catch {
|
|
72
82
|
return null;
|
|
73
83
|
}
|
|
@@ -96,26 +106,50 @@ function context(event, message) {
|
|
|
96
106
|
};
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
function
|
|
100
|
-
if (!value) return
|
|
109
|
+
function resolveClosedEntry(root, value) {
|
|
110
|
+
if (!value) return null;
|
|
101
111
|
const relative = value.trim().replaceAll("\\", "/");
|
|
102
|
-
if (!relative.endsWith(".md") || path.isAbsolute(relative)) return
|
|
112
|
+
if (!relative.endsWith(".md") || path.isAbsolute(relative)) return null;
|
|
103
113
|
|
|
104
114
|
const validSpineEntry = (resolved, journalRoot) => {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
const resolvedJournalRoot = path.resolve(journalRoot);
|
|
116
|
+
const workRoot = path.join(resolvedJournalRoot, "work");
|
|
117
|
+
const workRelative = path.relative(workRoot, resolved);
|
|
118
|
+
if (!workRelative || workRelative.startsWith("..") || path.isAbsolute(workRelative)) return null;
|
|
119
|
+
const parts = workRelative.split(path.sep);
|
|
120
|
+
if (parts.length < 3 || !parts[0] || parts[1] !== "journal") return null;
|
|
121
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) return null;
|
|
122
|
+
return { relative, resolved, journalRoot: resolvedJournalRoot, workSlug: parts[0] };
|
|
109
123
|
};
|
|
110
124
|
|
|
111
|
-
const repoResolved = path.resolve(root, relative);
|
|
112
|
-
if (validSpineEntry(repoResolved, path.join(root, ".journal"))) return true;
|
|
113
|
-
|
|
114
125
|
const context = projectContext(root);
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
const candidates = [];
|
|
127
|
+
if (context.global && relative.startsWith(".journal/")) {
|
|
128
|
+
candidates.push({
|
|
129
|
+
resolved: path.resolve(context.journalRoot, relative.slice(".journal/".length)),
|
|
130
|
+
journalRoot: context.journalRoot,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
candidates.push({
|
|
134
|
+
resolved: path.resolve(root, relative),
|
|
135
|
+
journalRoot: path.join(root, ".journal"),
|
|
136
|
+
});
|
|
137
|
+
if (!context.global && relative.startsWith(".journal/")) {
|
|
138
|
+
candidates.push({
|
|
139
|
+
resolved: path.resolve(context.journalRoot, relative.slice(".journal/".length)),
|
|
140
|
+
journalRoot: context.journalRoot,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const candidate of candidates) {
|
|
145
|
+
const entry = validSpineEntry(candidate.resolved, candidate.journalRoot);
|
|
146
|
+
if (entry) return entry;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validateClosedPath(root, value) {
|
|
152
|
+
return !!resolveClosedEntry(root, value);
|
|
119
153
|
}
|
|
120
154
|
|
|
121
155
|
function syncSharedWork(root, work, runner) {
|
|
@@ -166,14 +200,16 @@ function handle(payload, options = {}) {
|
|
|
166
200
|
};
|
|
167
201
|
}
|
|
168
202
|
|
|
169
|
-
|
|
203
|
+
const status = match[1].toLowerCase();
|
|
204
|
+
const closedEntry = status === "closed" ? resolveClosedEntry(root, match[2]) : null;
|
|
205
|
+
if (status === "closed" && !closedEntry) {
|
|
170
206
|
return {
|
|
171
207
|
decision: "block",
|
|
172
208
|
reason: "The journal-status closed marker must reference an existing Markdown spine entry under the resolved journal root. In global-store projects, .journal/... paths resolve through .djournal.json.",
|
|
173
209
|
};
|
|
174
210
|
}
|
|
175
|
-
if (
|
|
176
|
-
const work =
|
|
211
|
+
if (status === "closed") {
|
|
212
|
+
const work = workBySlug(root, closedEntry.workSlug);
|
|
177
213
|
if ((work?.shared || work?.visibility === "team_shared") && shouldAutoSync(root)) {
|
|
178
214
|
const result = syncSharedWork(root, work, options.syncRunner);
|
|
179
215
|
if (!result.ok) {
|
|
@@ -200,4 +236,4 @@ if (require.main === module) {
|
|
|
200
236
|
main();
|
|
201
237
|
}
|
|
202
238
|
|
|
203
|
-
module.exports = { handle, parseFrontmatter, shouldAutoSync };
|
|
239
|
+
module.exports = { handle, parseFrontmatter, resolveClosedEntry, shouldAutoSync };
|
package/bin/journal.js
CHANGED
|
@@ -104,7 +104,10 @@ function print(value, json) {
|
|
|
104
104
|
}
|
|
105
105
|
if (value.conflicts?.length) process.stdout.write(`conflicts: ${value.conflicts.join(", ")}\n`);
|
|
106
106
|
if (value.files) {
|
|
107
|
-
for (const file of value.files)
|
|
107
|
+
for (const file of value.files) {
|
|
108
|
+
if (typeof file === "string") process.stdout.write(`${file}\n`);
|
|
109
|
+
else process.stdout.write(`${file.status.padEnd(8)} ${file.path}\n`);
|
|
110
|
+
}
|
|
108
111
|
}
|
|
109
112
|
if (value.checks) {
|
|
110
113
|
for (const check of value.checks) process.stdout.write(`${check.ok ? "ok" : "fail"} ${check.name}: ${check.detail}\n`);
|
|
@@ -144,4 +147,4 @@ async function main() {
|
|
|
144
147
|
|
|
145
148
|
if (require.main === module) main();
|
|
146
149
|
|
|
147
|
-
module.exports = { parseArgs };
|
|
150
|
+
module.exports = { parseArgs, print };
|
package/docs/remote-sync.md
CHANGED
|
@@ -124,6 +124,9 @@ Current behavior:
|
|
|
124
124
|
|
|
125
125
|
Hook-triggered sync uses the same command path with `--auto` only when
|
|
126
126
|
global config enables standalone automatic sync and the closed work is shared.
|
|
127
|
+
The hook derives the work item from the validated closed marker path, so a
|
|
128
|
+
response that closes `.journal/work/<slug>/journal/...` synchronizes that
|
|
129
|
+
`<slug>` even if `state.json` currently selects a different active work item.
|
|
127
130
|
|
|
128
131
|
## Keep local state local
|
|
129
132
|
|