@stringsync/splates 0.0.0-stage → 0.0.45
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/LICENSE +21 -0
- package/README.md +308 -2
- package/acp-extensions.d.ts +82 -0
- package/add.d.ts +53 -0
- package/adherence.d.ts +49 -0
- package/age.d.ts +2 -0
- package/agent-connection.d.ts +171 -0
- package/agent-errors.d.ts +2 -0
- package/agent-process.d.ts +65 -0
- package/agent-stream.d.ts +91 -0
- package/agents.d.ts +15 -0
- package/artifact-room.d.ts +47 -0
- package/artifact-viewers.d.ts +6 -0
- package/artifact.d.ts +30 -0
- package/artifacts.d.ts +161 -0
- package/asked.d.ts +146 -0
- package/asked.js +7 -0
- package/assumed.d.ts +34 -0
- package/attach.d.ts +30 -0
- package/attachments.d.ts +43 -0
- package/baselines.d.ts +55 -0
- package/claim.d.ts +17 -0
- package/cloudflare-api.d.ts +25 -0
- package/cloudflare-error.d.ts +5 -0
- package/cloudflared-tunnel-provider.d.ts +27 -0
- package/codex-limits.d.ts +19 -0
- package/compaction.d.ts +17 -0
- package/config-source.d.ts +13 -0
- package/config.d.ts +98 -0
- package/config.js +7 -0
- package/conversation-archive.d.ts +6 -0
- package/conversation-background.d.ts +21 -0
- package/conversation-choice.d.ts +4 -0
- package/conversation-files.d.ts +51 -0
- package/conversation-given.d.ts +8 -0
- package/conversation-host.d.ts +102 -0
- package/conversation-listing.d.ts +15 -0
- package/conversation-log.d.ts +301 -0
- package/conversation-messages.d.ts +18 -0
- package/conversation-name.d.ts +28 -0
- package/conversation-options.d.ts +73 -0
- package/conversation-reference.d.ts +2 -0
- package/conversation-state.d.ts +472 -0
- package/conversation-turns.d.ts +115 -0
- package/conversation.d.ts +209 -0
- package/conversations.d.ts +140 -0
- package/coordinator-state.d.ts +28 -0
- package/creds.d.ts +123 -0
- package/darwin-temperature.d.ts +7 -0
- package/decided.d.ts +18 -0
- package/demo.d.ts +29 -0
- package/dev/api.d.ts +209 -0
- package/dev/assets.d.ts +24 -0
- package/dev/conversation-address.d.ts +47 -0
- package/dev/conversation-feed.d.ts +166 -0
- package/dev/live-stream.d.ts +29 -0
- package/dev/worker.d.ts +33 -0
- package/dev-access.d.ts +61 -0
- package/dev-artifacts.d.ts +22 -0
- package/dev-budgets.d.ts +18 -0
- package/dev-conversation-events.d.ts +39 -0
- package/dev-conversation-routes.d.ts +90 -0
- package/dev-creds.d.ts +17 -0
- package/dev-dictation-connection.d.ts +18 -0
- package/dev-dictation.d.ts +17 -0
- package/dev-http.d.ts +76 -0
- package/dev-open.d.ts +15 -0
- package/dev-options.d.ts +133 -0
- package/dev-outcomes.d.ts +21 -0
- package/dev-pages.d.ts +16 -0
- package/dev-picker.d.ts +57 -0
- package/dev-port-socket.d.ts +26 -0
- package/dev-ports.d.ts +11 -0
- package/dev-project-watch.d.ts +44 -0
- package/dev-project.d.ts +168 -0
- package/dev-push.d.ts +59 -0
- package/dev-restart.d.ts +141 -0
- package/dev-servers.d.ts +30 -0
- package/dev-setup-native.d.ts +23 -0
- package/dev-setup-opencode.d.ts +29 -0
- package/dev-setup.d.ts +16 -0
- package/dev-shell.d.ts +70 -0
- package/dev-slot.d.ts +28 -0
- package/dev-sockets.d.ts +13 -0
- package/dev-task-ports.d.ts +73 -0
- package/dev-tasks.d.ts +20 -0
- package/dev-terminal-connection.d.ts +19 -0
- package/dev-terminal.d.ts +42 -0
- package/dev-todos.d.ts +35 -0
- package/dev-transcriber.d.ts +15 -0
- package/dev-tunnel.d.ts +38 -0
- package/dev-versions.d.ts +53 -0
- package/dev-workers.d.ts +55 -0
- package/dev.d.ts +24 -0
- package/diff.d.ts +29 -0
- package/dispatched.d.ts +16 -0
- package/editors.d.ts +22 -0
- package/escalate.d.ts +27 -0
- package/events.d.ts +311 -0
- package/everywhere.d.ts +41 -0
- package/exhibit.d.ts +21 -0
- package/exit.d.ts +54 -0
- package/fact-todos.d.ts +18 -0
- package/fake-agent.d.ts +107 -0
- package/fake-agent.js +284 -0
- package/finding-facts.d.ts +38 -0
- package/findings.d.ts +84 -0
- package/fold.d.ts +43 -0
- package/follow.d.ts +41 -0
- package/format.d.ts +68 -0
- package/git.d.ts +112 -0
- package/handed-out.d.ts +79 -0
- package/handoff.d.ts +23 -0
- package/heavy-run.d.ts +12 -0
- package/heavy.d.ts +28 -0
- package/heavy.js +16 -0
- package/held.d.ts +22 -0
- package/index-0tfvy823.js +272 -0
- package/index-43pq33n4.js +157 -0
- package/index-4776671w.js +698 -0
- package/index-d4hjrzaq.js +464 -0
- package/index-gcfq7awr.js +740 -0
- package/index-k73tsw5g.js +101 -0
- package/index-kaa44k7q.js +59 -0
- package/index-vcv9m93e.js +1265 -0
- package/index-xmfmy729.js +249 -0
- package/index.d.ts +2 -0
- package/index.js +26928 -0
- package/init.d.ts +28 -0
- package/journal.d.ts +57 -0
- package/landed.d.ts +14 -0
- package/lane-todos.d.ts +44 -0
- package/lane.d.ts +55 -0
- package/lanes-from-tags.d.ts +17 -0
- package/lanes.d.ts +96 -0
- package/legacy-names.d.ts +70 -0
- package/legacy-ps.d.ts +31 -0
- package/limits.d.ts +32 -0
- package/list.d.ts +18 -0
- package/load-config.d.ts +44 -0
- package/log.d.ts +14 -0
- package/machine.d.ts +42 -0
- package/merge.d.ts +35 -0
- package/migrate-config.d.ts +31 -0
- package/models.d.ts +24 -0
- package/needs.d.ts +115 -0
- package/notices.d.ts +39 -0
- package/offered.d.ts +39 -0
- package/opencode-usage.d.ts +37 -0
- package/outcomes.d.ts +114 -0
- package/package.json +74 -5
- package/path.d.ts +13 -0
- package/plan.d.ts +236 -0
- package/port-forward.d.ts +22 -0
- package/port-pipe.d.ts +36 -0
- package/ports-list.d.ts +19 -0
- package/probe-runtime.d.ts +5 -0
- package/probe-runtime.js +13 -0
- package/project-reaches.d.ts +23 -0
- package/projects.d.ts +136 -0
- package/prompts.d.ts +128 -0
- package/pty.d.ts +39 -0
- package/pulse.d.ts +8 -0
- package/recommend.d.ts +31 -0
- package/release.d.ts +41 -0
- package/remote-process.d.ts +26 -0
- package/remote-pty.d.ts +29 -0
- package/remote-worker.d.ts +77 -0
- package/remove.d.ts +31 -0
- package/repository.d.ts +91 -0
- package/retry.d.ts +34 -0
- package/roster.d.ts +63 -0
- package/saved-ports-host.d.ts +25 -0
- package/saved-tunnel.d.ts +44 -0
- package/server-worker-socket.d.ts +21 -0
- package/setup-areas.d.ts +7 -0
- package/setup-opencode-model-error.d.ts +3 -0
- package/setup-opencode-models.d.ts +54 -0
- package/setup-opencode-recipe.d.ts +26 -0
- package/setup-opencode.d.ts +46 -0
- package/setup.d.ts +102 -0
- package/shared-browser.d.ts +50 -0
- package/shell.d.ts +17 -0
- package/show.d.ts +106 -0
- package/skill-checks.d.ts +73 -0
- package/skill-checks.js +10 -0
- package/skills.d.ts +16 -0
- package/slot-tunnel.d.ts +27 -0
- package/slots.d.ts +135 -0
- package/snapshot.d.ts +75 -0
- package/splates-artifact.d.ts +3 -0
- package/splates-creds.d.ts +3 -0
- package/splates-dev.d.ts +22 -0
- package/splates-flags.d.ts +34 -0
- package/splates-lane.d.ts +3 -0
- package/splates-projects.d.ts +3 -0
- package/splates-setup.d.ts +17 -0
- package/splates-slots.d.ts +3 -0
- package/splates-todo.d.ts +3 -0
- package/splates-tunnel.d.ts +3 -0
- package/splates-worker.d.ts +3 -0
- package/splates.d.ts +28 -0
- package/stats.d.ts +186 -0
- package/supervise.d.ts +50 -0
- package/system-changes.d.ts +67 -0
- package/system.d.ts +28 -0
- package/systems.d.ts +119 -0
- package/systems.js +21 -0
- package/table.d.ts +5 -0
- package/task-log.d.ts +35 -0
- package/task-ports.d.ts +25 -0
- package/tell.d.ts +47 -0
- package/todo-edit.d.ts +60 -0
- package/todo-file.d.ts +6 -0
- package/todo-lanes.d.ts +82 -0
- package/todo-list.d.ts +26 -0
- package/todo-take.d.ts +27 -0
- package/todo.d.ts +146 -0
- package/todos.d.ts +157 -0
- package/tool-calls.d.ts +18 -0
- package/tunnel-provider.d.ts +37 -0
- package/tunnel-provider.js +0 -0
- package/tunnel-remove.d.ts +28 -0
- package/tunnel-setup.d.ts +104 -0
- package/unix-worker-listener.d.ts +23 -0
- package/unix-worker-socket.d.ts +30 -0
- package/update.d.ts +29 -0
- package/update.js +10 -0
- package/vocabulary.d.ts +9 -0
- package/wait.d.ts +36 -0
- package/watch.d.ts +46 -0
- package/watched.d.ts +43 -0
- package/websocket-worker-socket.d.ts +18 -0
- package/worker-call-host.d.ts +51 -0
- package/worker-channel.d.ts +83 -0
- package/worker-connection.d.ts +24 -0
- package/worker-daemon.d.ts +64 -0
- package/worker-git.d.ts +18 -0
- package/worker-list.d.ts +49 -0
- package/worker-machine.d.ts +34 -0
- package/worker-port-host.d.ts +21 -0
- package/worker-process-host.d.ts +29 -0
- package/worker-protocol.d.ts +104 -0
- package/worker-pty-host.d.ts +34 -0
- package/worker-registry.d.ts +68 -0
- package/worker-replay.d.ts +25 -0
- package/worker-run.d.ts +20 -0
- package/worker-session.d.ts +189 -0
- package/worker-socket.d.ts +15 -0
- package/worktree-service.d.ts +113 -0
- package/worktree.d.ts +232 -0
|
@@ -0,0 +1,1265 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Attachments,
|
|
3
|
+
CONFIG_FILE,
|
|
4
|
+
ConfigFiles,
|
|
5
|
+
SYSTEMS_FILE2,
|
|
6
|
+
checkSystems2
|
|
7
|
+
} from "./index-4776671w.js";
|
|
8
|
+
import {
|
|
9
|
+
LEGACY,
|
|
10
|
+
renameLegacyFiles,
|
|
11
|
+
removeLegacySkill
|
|
12
|
+
} from "./index-k73tsw5g.js";
|
|
13
|
+
import {
|
|
14
|
+
fail
|
|
15
|
+
} from "./index-kaa44k7q.js";
|
|
16
|
+
|
|
17
|
+
// update.ts
|
|
18
|
+
import { color as color2 } from "webappwiz/log";
|
|
19
|
+
|
|
20
|
+
// lanes-from-tags.ts
|
|
21
|
+
import { isAbsolute, resolve } from "node:path";
|
|
22
|
+
import { ConsoleLogger } from "webappwiz/log";
|
|
23
|
+
import { FileLock, NodeFs as NodeFs2, NodePs } from "webappwiz/system";
|
|
24
|
+
|
|
25
|
+
// todos.ts
|
|
26
|
+
import { UuidProvider } from "webappwiz/id";
|
|
27
|
+
import { NodeFs } from "webappwiz/system";
|
|
28
|
+
|
|
29
|
+
// lanes.ts
|
|
30
|
+
function lanes(todos, records) {
|
|
31
|
+
return named(todos, records).map((record) => lane(todos, record));
|
|
32
|
+
}
|
|
33
|
+
function named(todos, records) {
|
|
34
|
+
const unnamed = [
|
|
35
|
+
...new Set(todos.flatMap((todo) => todo.lane === null || records.some((record) => record.id === todo.lane) ? [] : [todo.lane]))
|
|
36
|
+
].sort((left, right) => left - right);
|
|
37
|
+
return [...records, ...unnamed.map((id) => ({ id, name: `Lane ${id}` }))];
|
|
38
|
+
}
|
|
39
|
+
function lane(todos, { id, name }) {
|
|
40
|
+
const steps = todos.filter((todo) => todo.lane === id).sort((left, right) => left.position - right.position);
|
|
41
|
+
const next = steps.find((todo) => todo.takenBy === null) ?? null;
|
|
42
|
+
const agents = [
|
|
43
|
+
...new Set(steps.flatMap((todo) => todo.takenBy === null ? [] : [todo.takenBy]))
|
|
44
|
+
];
|
|
45
|
+
const blockers = next === null ? [] : blockersOf(todos, next).filter((blocker) => blocker.lane !== id);
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
name,
|
|
49
|
+
todos: steps,
|
|
50
|
+
agents,
|
|
51
|
+
next,
|
|
52
|
+
blockers,
|
|
53
|
+
ready: next !== null && blockersOf(todos, next).length === 0
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function blockersOf(todos, todo) {
|
|
57
|
+
return todos.filter((other) => todo.blockedBy.includes(other.id)).map(({ id, subject, lane, takenBy }) => ({ id, subject, lane, takenBy }));
|
|
58
|
+
}
|
|
59
|
+
function followers(todos, id) {
|
|
60
|
+
return todos.filter((todo) => todo.blockedBy.includes(id));
|
|
61
|
+
}
|
|
62
|
+
function isReady(todo) {
|
|
63
|
+
return todo.takenBy === null && todo.blockedBy.length === 0;
|
|
64
|
+
}
|
|
65
|
+
function cycle(todos, id, blocker) {
|
|
66
|
+
if (id === blocker) {
|
|
67
|
+
return [id, id];
|
|
68
|
+
}
|
|
69
|
+
const byId = new Map(todos.map((todo) => [todo.id, todo]));
|
|
70
|
+
const trail = (from, seen) => {
|
|
71
|
+
if (from === id) {
|
|
72
|
+
return [id];
|
|
73
|
+
}
|
|
74
|
+
if (seen.has(from)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
seen.add(from);
|
|
78
|
+
for (const before of byId.get(from)?.blockedBy ?? []) {
|
|
79
|
+
const found = trail(before, seen);
|
|
80
|
+
if (found) {
|
|
81
|
+
return [...found, from];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
const found = trail(blocker, new Set);
|
|
87
|
+
return found && [...found, id];
|
|
88
|
+
}
|
|
89
|
+
function settle(todos) {
|
|
90
|
+
const pending = [...todos];
|
|
91
|
+
const placed = new Set;
|
|
92
|
+
const byId = new Map(todos.map((todo) => [todo.id, todo]));
|
|
93
|
+
const free = (todo) => todo.lane === null || todo.blockedBy.every((id) => placed.has(id) || byId.get(id)?.lane !== todo.lane);
|
|
94
|
+
const order = [];
|
|
95
|
+
while (pending.length > 0) {
|
|
96
|
+
const at = Math.max(0, pending.findIndex(free));
|
|
97
|
+
const [todo] = pending.splice(at, 1);
|
|
98
|
+
placed.add(todo.id);
|
|
99
|
+
order.push(todo);
|
|
100
|
+
}
|
|
101
|
+
return order;
|
|
102
|
+
}
|
|
103
|
+
function misordered(todos) {
|
|
104
|
+
const at = new Map(todos.map((todo, i) => [todo.id, i]));
|
|
105
|
+
for (const [i, todo] of todos.entries()) {
|
|
106
|
+
if (todo.lane === null) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
for (const id of todo.blockedBy) {
|
|
110
|
+
const before = todos[at.get(id) ?? -1];
|
|
111
|
+
if (before && before.lane === todo.lane && (at.get(id) ?? -1) > i) {
|
|
112
|
+
return { todo, blockedBy: before };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function blockerLabel({ id, lane }, records) {
|
|
119
|
+
const name = records.find((record) => record.id === lane)?.name;
|
|
120
|
+
return `#${id}${lane === null ? "" : ` in ${name ?? `lane ${lane}`}`}`;
|
|
121
|
+
}
|
|
122
|
+
function aptName(todos, taken) {
|
|
123
|
+
const used = new Set(taken.map((name) => name.toLowerCase()));
|
|
124
|
+
const base = todos[0]?.subject ?? "New lane";
|
|
125
|
+
let name = base;
|
|
126
|
+
for (let count = 2;used.has(name.toLowerCase()); count++) {
|
|
127
|
+
name = `${base} ${count}`;
|
|
128
|
+
}
|
|
129
|
+
return name;
|
|
130
|
+
}
|
|
131
|
+
function mentioned(text) {
|
|
132
|
+
const prose = text.replace(/```[\s\S]*?(```|$)|`[^`\n]*`/g, " ");
|
|
133
|
+
return [
|
|
134
|
+
...new Set([...prose.matchAll(MENTION)].map((match) => Number(match[1])))
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
var MENTION = /(?<![\w/#&])#(\d+)\b/g;
|
|
138
|
+
|
|
139
|
+
// todo.ts
|
|
140
|
+
import { basename } from "node:path";
|
|
141
|
+
class Todo {
|
|
142
|
+
todos;
|
|
143
|
+
state;
|
|
144
|
+
constructor(todos, state) {
|
|
145
|
+
this.todos = todos;
|
|
146
|
+
this.state = state;
|
|
147
|
+
}
|
|
148
|
+
get id() {
|
|
149
|
+
return this.state.id;
|
|
150
|
+
}
|
|
151
|
+
get subject() {
|
|
152
|
+
return this.state.subject;
|
|
153
|
+
}
|
|
154
|
+
get text() {
|
|
155
|
+
return this.state.text;
|
|
156
|
+
}
|
|
157
|
+
get position() {
|
|
158
|
+
return this.state.position;
|
|
159
|
+
}
|
|
160
|
+
get from() {
|
|
161
|
+
return this.state.from;
|
|
162
|
+
}
|
|
163
|
+
get takenBy() {
|
|
164
|
+
return this.state.takenBy;
|
|
165
|
+
}
|
|
166
|
+
get createdAt() {
|
|
167
|
+
return new Date(this.state.createdAt);
|
|
168
|
+
}
|
|
169
|
+
get waited() {
|
|
170
|
+
return Date.now() - this.createdAt.getTime();
|
|
171
|
+
}
|
|
172
|
+
async blockers(task, together = []) {
|
|
173
|
+
if (this.takenBy === task) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
return (await this.todos.all()).filter((other) => this.blockedBy.includes(other.id) && other.takenBy !== task && !together.includes(other.id));
|
|
177
|
+
}
|
|
178
|
+
async take(task) {
|
|
179
|
+
return this.todos.revise(this.id, (state) => {
|
|
180
|
+
if (state.takenBy !== null && state.takenBy !== task) {
|
|
181
|
+
fail("exists", `todo ${this.id} is already taken by '${state.takenBy}': pick another from \`splates todo list\``, { todo: this.id, takenBy: state.takenBy });
|
|
182
|
+
}
|
|
183
|
+
return { ...state, takenBy: task };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
release() {
|
|
187
|
+
return this.todos.revise(this.id, (state) => ({
|
|
188
|
+
...state,
|
|
189
|
+
takenBy: null
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
get files() {
|
|
193
|
+
return this.state.files;
|
|
194
|
+
}
|
|
195
|
+
get blockedBy() {
|
|
196
|
+
return this.state.blockedBy;
|
|
197
|
+
}
|
|
198
|
+
get lane() {
|
|
199
|
+
return this.state.lane;
|
|
200
|
+
}
|
|
201
|
+
get attachments() {
|
|
202
|
+
return this.todos.attachments(this.id);
|
|
203
|
+
}
|
|
204
|
+
async update({
|
|
205
|
+
subject,
|
|
206
|
+
text,
|
|
207
|
+
position,
|
|
208
|
+
files = [],
|
|
209
|
+
keep,
|
|
210
|
+
blockedBy,
|
|
211
|
+
lane,
|
|
212
|
+
assignee
|
|
213
|
+
}) {
|
|
214
|
+
const words = wording(subject ?? this.subject, text ?? this.text);
|
|
215
|
+
if (words.subject === "") {
|
|
216
|
+
fail("usage", "a todo needs a subject: say what is left to do", {
|
|
217
|
+
todo: this.id
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const kept = keep === undefined ? this.files : this.files.filter((path) => keep.includes(path));
|
|
221
|
+
await this.attachments.remove(this.files.filter((path) => !kept.includes(path)));
|
|
222
|
+
const added = await this.attachments.store(files);
|
|
223
|
+
const updated = await this.todos.revise(this.id, (state) => assigned({ ...state, ...words, files: [...kept, ...added] }, assignee === undefined ? state.assignee : assignee));
|
|
224
|
+
let linked = updated;
|
|
225
|
+
if (blockedBy !== undefined) {
|
|
226
|
+
linked = await this.todos.link(this.id, {
|
|
227
|
+
add: blockedBy.filter((id) => !updated.blockedBy.includes(id)),
|
|
228
|
+
drop: updated.blockedBy.filter((id) => !blockedBy.includes(id))
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (lane !== undefined && lane !== linked.lane) {
|
|
232
|
+
linked = await this.todos.arrange(this.id, lane);
|
|
233
|
+
}
|
|
234
|
+
return position === undefined ? linked : this.todos.move(this.id, position);
|
|
235
|
+
}
|
|
236
|
+
async remove() {
|
|
237
|
+
await this.todos.delete(this.id);
|
|
238
|
+
await this.attachments.clear();
|
|
239
|
+
}
|
|
240
|
+
discard() {
|
|
241
|
+
return this.todos.discard(this.state);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function cameFrom({ from, origin }) {
|
|
245
|
+
if (origin === undefined) {
|
|
246
|
+
return from;
|
|
247
|
+
}
|
|
248
|
+
const project = basename(origin.project);
|
|
249
|
+
return origin.task === null ? project : `${project}/${origin.task}`;
|
|
250
|
+
}
|
|
251
|
+
function assigned(state, assignee) {
|
|
252
|
+
const { assignee: _, ...rest } = state;
|
|
253
|
+
return assignee == null ? rest : { ...rest, assignee };
|
|
254
|
+
}
|
|
255
|
+
function wording(subject, text) {
|
|
256
|
+
const [first = "", ...rest] = subject.trim().split(`
|
|
257
|
+
`);
|
|
258
|
+
return {
|
|
259
|
+
subject: first.trim(),
|
|
260
|
+
text: [rest.join(`
|
|
261
|
+
`).trim(), text.trim()].filter(Boolean).join(`
|
|
262
|
+
|
|
263
|
+
`)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async function recordUpdate(journal, todos, id, update) {
|
|
267
|
+
let before;
|
|
268
|
+
return journal.record("todo update", null, async () => {
|
|
269
|
+
before = (await todos.find(id)).state;
|
|
270
|
+
return update();
|
|
271
|
+
}, (after) => updated(before ?? after.state, after.state));
|
|
272
|
+
}
|
|
273
|
+
function updated(before, after) {
|
|
274
|
+
const named = (paths) => paths.map((path) => basename(path));
|
|
275
|
+
const attached = after.files.filter((path) => !before.files.includes(path));
|
|
276
|
+
const detached = before.files.filter((path) => !after.files.includes(path));
|
|
277
|
+
const blocked = after.blockedBy.filter((id) => !before.blockedBy.includes(id));
|
|
278
|
+
const unblocked = before.blockedBy.filter((id) => !after.blockedBy.includes(id));
|
|
279
|
+
const same = (old, now) => old?.agent === now?.agent && old?.model === now?.model;
|
|
280
|
+
const changed = {
|
|
281
|
+
...after.subject === before.subject ? {} : { subject: after.subject },
|
|
282
|
+
...after.text === before.text ? {} : { text: true },
|
|
283
|
+
...after.position === before.position ? {} : { position: after.position },
|
|
284
|
+
...after.lane === before.lane ? {} : { lane: after.lane },
|
|
285
|
+
...blocked.length === 0 ? {} : { blocked },
|
|
286
|
+
...unblocked.length === 0 ? {} : { unblocked },
|
|
287
|
+
...attached.length === 0 ? {} : { attached: named(attached) },
|
|
288
|
+
...detached.length === 0 ? {} : { detached: named(detached) },
|
|
289
|
+
...same(before.assignee, after.assignee) ? {} : { assignee: after.assignee ?? null }
|
|
290
|
+
};
|
|
291
|
+
return { todo: after.id, subject: after.subject, changed };
|
|
292
|
+
}
|
|
293
|
+
function todosNamed(todos) {
|
|
294
|
+
return todos.map(({ id, subject }) => ({ id, subject }));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// todo-file.ts
|
|
298
|
+
function parseTodoFile(raw) {
|
|
299
|
+
try {
|
|
300
|
+
const { tags = [], ...state } = JSON.parse(raw);
|
|
301
|
+
return {
|
|
302
|
+
state: {
|
|
303
|
+
...state,
|
|
304
|
+
...state.subject === undefined ? wording(state.text, "") : { subject: state.subject, text: state.text },
|
|
305
|
+
position: state.position ?? 0,
|
|
306
|
+
files: state.files ?? [],
|
|
307
|
+
blockedBy: state.blockedBy ?? [],
|
|
308
|
+
lane: state.lane ?? null
|
|
309
|
+
},
|
|
310
|
+
tags
|
|
311
|
+
};
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// todo-lanes.ts
|
|
318
|
+
class TodoLanes {
|
|
319
|
+
todos;
|
|
320
|
+
fs;
|
|
321
|
+
constructor(todos, fs) {
|
|
322
|
+
this.todos = todos;
|
|
323
|
+
this.fs = fs;
|
|
324
|
+
}
|
|
325
|
+
async joinLanes(from, into) {
|
|
326
|
+
await this.todos.locked(async () => {
|
|
327
|
+
const states = await this.todos.stored();
|
|
328
|
+
const records = await this.records(states);
|
|
329
|
+
for (const lane of [from, into]) {
|
|
330
|
+
lacking(records, lane);
|
|
331
|
+
}
|
|
332
|
+
const moving = states.filter((state) => state.lane === from).map((state) => ({ ...state, lane: into }));
|
|
333
|
+
const rest = states.filter((state) => state.lane !== from);
|
|
334
|
+
const tail = rest.findLastIndex((state) => state.lane === into);
|
|
335
|
+
const order = settle([
|
|
336
|
+
...rest.slice(0, tail + 1),
|
|
337
|
+
...moving,
|
|
338
|
+
...rest.slice(tail + 1)
|
|
339
|
+
]);
|
|
340
|
+
for (const state of moving) {
|
|
341
|
+
await this.todos.save(state);
|
|
342
|
+
}
|
|
343
|
+
await this.todos.renumber(order);
|
|
344
|
+
await this.writeLanes(records.filter((record) => record.id !== from));
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
async addLane(name) {
|
|
348
|
+
return this.todos.locked(async () => {
|
|
349
|
+
const states = await this.todos.stored();
|
|
350
|
+
const records = await this.records(states);
|
|
351
|
+
const called = laneName(name, records);
|
|
352
|
+
const record = { id: await this.nextLaneId(states), name: called };
|
|
353
|
+
await this.writeLanes([...records, record]);
|
|
354
|
+
return record;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
async renameLane(id, name) {
|
|
358
|
+
return this.todos.locked(async () => {
|
|
359
|
+
const records = await this.records(await this.todos.stored());
|
|
360
|
+
lacking(records, id);
|
|
361
|
+
const others = records.filter((record) => record.id !== id);
|
|
362
|
+
const renamed = {
|
|
363
|
+
...records.find((record) => record.id === id),
|
|
364
|
+
name: laneName(name, others)
|
|
365
|
+
};
|
|
366
|
+
await this.writeLanes(records.map((record) => record.id === id ? renamed : record));
|
|
367
|
+
return renamed;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async moveLane(id, position) {
|
|
371
|
+
if (!Number.isInteger(position) || position <= 0) {
|
|
372
|
+
fail("usage", `'${position}' is not a position: 1 is the first lane`, {
|
|
373
|
+
position
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return this.todos.locked(async () => {
|
|
377
|
+
const records = await this.records(await this.todos.stored());
|
|
378
|
+
lacking(records, id);
|
|
379
|
+
const moved = records.find((record) => record.id === id);
|
|
380
|
+
const rest = records.filter((record) => record.id !== id);
|
|
381
|
+
const at = Math.min(position - 1, rest.length);
|
|
382
|
+
const order = [...rest.slice(0, at), moved, ...rest.slice(at)];
|
|
383
|
+
await this.writeLanes(order);
|
|
384
|
+
return order;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
async dropLane(id) {
|
|
388
|
+
await this.todos.locked(async () => {
|
|
389
|
+
const states = await this.todos.stored();
|
|
390
|
+
const records = await this.records(states);
|
|
391
|
+
lacking(records, id);
|
|
392
|
+
for (const state of states.filter((state) => state.lane === id)) {
|
|
393
|
+
await this.todos.save({ ...state, lane: null });
|
|
394
|
+
}
|
|
395
|
+
await this.writeLanes(records.filter((record) => record.id !== id));
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
async endLanes(ids) {
|
|
399
|
+
await this.todos.locked(async () => {
|
|
400
|
+
const states = await this.todos.stored();
|
|
401
|
+
const records = await this.records(states);
|
|
402
|
+
const done = new Set(ids.filter((id) => !states.some((state) => state.lane === id)));
|
|
403
|
+
if (done.size > 0) {
|
|
404
|
+
await this.writeLanes(records.filter((record) => !done.has(record.id)));
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
async moveTagsToLanes() {
|
|
409
|
+
return this.todos.locked(async () => {
|
|
410
|
+
const entries = await this.fs.readdir(this.todos.dir).catch(() => []);
|
|
411
|
+
const parsed = [];
|
|
412
|
+
for (const entry of entries.filter((entry) => entry.endsWith(".json"))) {
|
|
413
|
+
const raw = await this.fs.read(`${this.todos.dir}/${entry}`).catch(() => "");
|
|
414
|
+
const found = parseTodoFile(raw);
|
|
415
|
+
if (found !== null && found.tags.length > 0) {
|
|
416
|
+
parsed.push(found);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (parsed.length === 0) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
const counts = new Map;
|
|
423
|
+
for (const { tags } of parsed) {
|
|
424
|
+
for (const tag of tags) {
|
|
425
|
+
counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const states = await this.todos.stored(parsed.map(({ state }) => state.id));
|
|
429
|
+
const records = await this.records(states);
|
|
430
|
+
const filled = new Map;
|
|
431
|
+
for (const state of states) {
|
|
432
|
+
const tags = parsed.find((each) => each.state.id === state.id)?.tags;
|
|
433
|
+
if (tags === undefined) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
let lane = state.lane;
|
|
437
|
+
if (lane === null) {
|
|
438
|
+
const [tag] = [...tags].sort((left, right) => (counts.get(right) ?? 0) - (counts.get(left) ?? 0) || left.localeCompare(right));
|
|
439
|
+
const name = tagName(tag);
|
|
440
|
+
let record = records.find((each) => each.name.toLowerCase() === name.toLowerCase());
|
|
441
|
+
if (record === undefined) {
|
|
442
|
+
record = { id: await this.nextLaneId(states), name };
|
|
443
|
+
records.push(record);
|
|
444
|
+
await this.writeLanes(records);
|
|
445
|
+
}
|
|
446
|
+
lane = record.id;
|
|
447
|
+
filled.set(lane, [...filled.get(lane) ?? [], state.id]);
|
|
448
|
+
}
|
|
449
|
+
await this.todos.save({ ...state, lane });
|
|
450
|
+
}
|
|
451
|
+
await this.todos.renumber(settle(await this.todos.stored()));
|
|
452
|
+
return records.filter((record) => filled.has(record.id)).map((lane) => ({ lane, todos: filled.get(lane.id) ?? [] }));
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
async laneId(lane, states, todos, name) {
|
|
456
|
+
if (lane === null) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
if (lane !== "new") {
|
|
460
|
+
lacking(await this.records(states), lane);
|
|
461
|
+
return lane;
|
|
462
|
+
}
|
|
463
|
+
return (await this.newLane(states, todos, name)).id;
|
|
464
|
+
}
|
|
465
|
+
async newLane(states, todos, name) {
|
|
466
|
+
const records = await this.records(states);
|
|
467
|
+
const called = name === undefined ? aptName(todos, records.map((each) => each.name)) : laneName(name, records);
|
|
468
|
+
const record = { id: await this.nextLaneId(states), name: called };
|
|
469
|
+
await this.writeLanes([...records, record]);
|
|
470
|
+
return record;
|
|
471
|
+
}
|
|
472
|
+
async nextLaneId(states) {
|
|
473
|
+
const last = Number(await this.fs.read(`${this.todos.dir}/last-lane`).catch(() => 0));
|
|
474
|
+
const records = await this.records(states);
|
|
475
|
+
const id = Math.max(Number.isInteger(last) ? last : 0, ...records.map((record) => record.id)) + 1;
|
|
476
|
+
await this.fs.mkdir(this.todos.dir);
|
|
477
|
+
await this.fs.write(`${this.todos.dir}/last-lane`, String(id));
|
|
478
|
+
return id;
|
|
479
|
+
}
|
|
480
|
+
async records(states) {
|
|
481
|
+
return named(states, await this.read("lanes"));
|
|
482
|
+
}
|
|
483
|
+
async names(states) {
|
|
484
|
+
const records = await this.records(states);
|
|
485
|
+
const ended = await this.read("ended-lanes");
|
|
486
|
+
return [
|
|
487
|
+
...records,
|
|
488
|
+
...ended.filter(({ id }) => !records.some((record) => record.id === id))
|
|
489
|
+
];
|
|
490
|
+
}
|
|
491
|
+
async read(file) {
|
|
492
|
+
const raw = await this.fs.read(`${this.todos.dir}/${file}`).catch(() => "[]");
|
|
493
|
+
try {
|
|
494
|
+
const parsed = JSON.parse(raw);
|
|
495
|
+
if (Array.isArray(parsed)) {
|
|
496
|
+
return parsed.filter((each) => Number.isInteger(each?.id) && typeof each?.name === "string").map(({ id, name }) => ({ id, name }));
|
|
497
|
+
}
|
|
498
|
+
} catch {}
|
|
499
|
+
return [];
|
|
500
|
+
}
|
|
501
|
+
async writeLanes(records) {
|
|
502
|
+
const dropped = (await this.read("lanes")).filter(({ id }) => !records.some((record) => record.id === id));
|
|
503
|
+
if (dropped.length > 0) {
|
|
504
|
+
const ended = await this.read("ended-lanes");
|
|
505
|
+
await this.write("ended-lanes", [
|
|
506
|
+
...ended.filter(({ id }) => !dropped.some((each) => each.id === id)),
|
|
507
|
+
...dropped
|
|
508
|
+
]);
|
|
509
|
+
}
|
|
510
|
+
await this.write("lanes", records);
|
|
511
|
+
}
|
|
512
|
+
async write(file, records) {
|
|
513
|
+
await this.fs.mkdir(this.todos.dir);
|
|
514
|
+
const path = `${this.todos.dir}/${file}`;
|
|
515
|
+
const tmp = `${path}.${crypto.randomUUID()}.tmp`;
|
|
516
|
+
await this.fs.write(tmp, `${JSON.stringify(records, null, "\t")}
|
|
517
|
+
`);
|
|
518
|
+
await this.fs.rename(tmp, path);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function tagName(tag) {
|
|
522
|
+
const words = tag.replaceAll("-", " ");
|
|
523
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
524
|
+
}
|
|
525
|
+
function lacking(records, id) {
|
|
526
|
+
if (!records.some((record) => record.id === id)) {
|
|
527
|
+
fail("not_found", `no lane ${id}: run \`splates lane list\`, or pass \`new\` to start one`, { lane: id });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function laneName(name, others) {
|
|
531
|
+
const trimmed = name.trim();
|
|
532
|
+
if (trimmed === "") {
|
|
533
|
+
fail("usage", "a lane needs a name: say what its todos are for", {});
|
|
534
|
+
}
|
|
535
|
+
const same = others.find((other) => other.name.toLowerCase() === trimmed.toLowerCase());
|
|
536
|
+
if (same) {
|
|
537
|
+
fail("exists", `lane ${same.id} is already called '${same.name}': pick another name`, { lane: same.id, name: same.name });
|
|
538
|
+
}
|
|
539
|
+
return trimmed;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// todos.ts
|
|
543
|
+
class Todos {
|
|
544
|
+
dir;
|
|
545
|
+
lock;
|
|
546
|
+
fs;
|
|
547
|
+
ids;
|
|
548
|
+
laneFiles;
|
|
549
|
+
constructor(dir, lock, opts = {}) {
|
|
550
|
+
this.dir = dir;
|
|
551
|
+
this.lock = lock;
|
|
552
|
+
this.fs = opts.fs ?? new NodeFs;
|
|
553
|
+
this.ids = opts.ids ?? new UuidProvider;
|
|
554
|
+
this.laneFiles = new TodoLanes(this, this.fs);
|
|
555
|
+
}
|
|
556
|
+
attachments(id) {
|
|
557
|
+
return new Attachments(`${this.dir}/${id}`, this.fs, this.ids);
|
|
558
|
+
}
|
|
559
|
+
owns(path) {
|
|
560
|
+
return new Attachments(this.dir, this.fs).owns(path);
|
|
561
|
+
}
|
|
562
|
+
async add(subject, from, {
|
|
563
|
+
text = "",
|
|
564
|
+
files = [],
|
|
565
|
+
position,
|
|
566
|
+
blockedBy = [],
|
|
567
|
+
lane = null,
|
|
568
|
+
assignee = null,
|
|
569
|
+
origin
|
|
570
|
+
} = {}) {
|
|
571
|
+
const words = wording(subject, text);
|
|
572
|
+
if (words.subject === "") {
|
|
573
|
+
fail("usage", "a todo needs a subject: say what is left to do", {});
|
|
574
|
+
}
|
|
575
|
+
await this.fs.mkdir(this.dir);
|
|
576
|
+
return this.locked(async () => {
|
|
577
|
+
const stored = await this.stored();
|
|
578
|
+
for (const id of blockedBy) {
|
|
579
|
+
if (!stored.some((other) => other.id === id)) {
|
|
580
|
+
missing(id);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const id = await this.lastId() + 1;
|
|
584
|
+
await this.fs.write(`${this.dir}/last`, String(id));
|
|
585
|
+
const state = {
|
|
586
|
+
id,
|
|
587
|
+
...words,
|
|
588
|
+
position: 0,
|
|
589
|
+
from,
|
|
590
|
+
createdAt: new Date().toISOString(),
|
|
591
|
+
takenBy: null,
|
|
592
|
+
files: await this.attachments(id).store(files),
|
|
593
|
+
blockedBy: [...new Set(blockedBy)].sort((left, right) => left - right),
|
|
594
|
+
lane: null,
|
|
595
|
+
...origin === undefined ? {} : { origin }
|
|
596
|
+
};
|
|
597
|
+
const todo = await this.save(assigned({
|
|
598
|
+
...state,
|
|
599
|
+
lane: await this.laneFiles.laneId(lane, stored, [state])
|
|
600
|
+
}, assignee));
|
|
601
|
+
await this.place(todo.state, position);
|
|
602
|
+
return this.settled(todo.id);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
async all() {
|
|
606
|
+
return (await this.stored()).map((state, i) => new Todo(this, { ...state, position: i + 1 }));
|
|
607
|
+
}
|
|
608
|
+
async find(id) {
|
|
609
|
+
return (await this.all()).find((todo) => todo.id === id) ?? missing(id);
|
|
610
|
+
}
|
|
611
|
+
async takenBy(task) {
|
|
612
|
+
return (await this.all()).filter((todo) => todo.takenBy === task);
|
|
613
|
+
}
|
|
614
|
+
async move(id, position) {
|
|
615
|
+
return this.locked(async () => {
|
|
616
|
+
const state = await this.state(id);
|
|
617
|
+
this.ordered(slot(await this.stored(), state, position));
|
|
618
|
+
return this.place(state, position);
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
async link(id, { add = [], drop = [] }) {
|
|
622
|
+
return this.locked(async () => {
|
|
623
|
+
const states = await this.stored();
|
|
624
|
+
const state = states.find((other) => other.id === id) ?? missing(id);
|
|
625
|
+
for (const before of add) {
|
|
626
|
+
if (!states.some((other) => other.id === before)) {
|
|
627
|
+
missing(before);
|
|
628
|
+
}
|
|
629
|
+
const loop = cycle(states, id, before);
|
|
630
|
+
if (loop) {
|
|
631
|
+
fail("cycle", loop.length === 2 ? `todo ${id} cannot come after itself` : `todo ${id} cannot come after todo ${before}: that makes a loop, ${loop.map((each) => `#${each}`).join(" → ")}, where each has to merge before the next`, { todo: id, blockedBy: before, cycle: loop });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const unknown = drop.find((before) => !state.blockedBy.includes(before));
|
|
635
|
+
if (unknown !== undefined) {
|
|
636
|
+
fail("not_found", `todo ${id} does not come after todo ${unknown}: nothing was changed`, { todo: id, blockedBy: unknown });
|
|
637
|
+
}
|
|
638
|
+
await this.save({
|
|
639
|
+
...state,
|
|
640
|
+
blockedBy: [...new Set([...state.blockedBy, ...add])].filter((before) => !drop.includes(before)).sort((left, right) => left - right)
|
|
641
|
+
});
|
|
642
|
+
return this.settled(id);
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
async arrange(id, lane, { before, name } = {}) {
|
|
646
|
+
return this.locked(async () => {
|
|
647
|
+
const states = await this.stored();
|
|
648
|
+
const state = states.find((other) => other.id === id) ?? missing(id);
|
|
649
|
+
const moved = {
|
|
650
|
+
...state,
|
|
651
|
+
lane: await this.laneFiles.laneId(lane, states, [state], name)
|
|
652
|
+
};
|
|
653
|
+
const rest = states.filter((other) => other.id !== id);
|
|
654
|
+
let at;
|
|
655
|
+
if (before !== undefined) {
|
|
656
|
+
at = rest.findIndex((other) => other.id === before);
|
|
657
|
+
if (at === -1) {
|
|
658
|
+
missing(before);
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
const tail = rest.findLastIndex((other) => moved.lane !== null && other.lane === moved.lane);
|
|
662
|
+
at = tail === -1 ? states.findIndex((other) => other.id === id) : tail + 1;
|
|
663
|
+
}
|
|
664
|
+
const order = [...rest.slice(0, at), moved, ...rest.slice(at)];
|
|
665
|
+
this.ordered(order);
|
|
666
|
+
await this.save(moved);
|
|
667
|
+
await this.renumber(order);
|
|
668
|
+
return this.find(id);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
async lanes() {
|
|
672
|
+
return this.laneFiles.records(await this.stored());
|
|
673
|
+
}
|
|
674
|
+
async laneNames() {
|
|
675
|
+
return this.laneFiles.names(await this.stored());
|
|
676
|
+
}
|
|
677
|
+
joinLanes(from, into) {
|
|
678
|
+
return this.laneFiles.joinLanes(from, into);
|
|
679
|
+
}
|
|
680
|
+
addLane(name) {
|
|
681
|
+
return this.laneFiles.addLane(name);
|
|
682
|
+
}
|
|
683
|
+
renameLane(id, name) {
|
|
684
|
+
return this.laneFiles.renameLane(id, name);
|
|
685
|
+
}
|
|
686
|
+
moveLane(id, position) {
|
|
687
|
+
return this.laneFiles.moveLane(id, position);
|
|
688
|
+
}
|
|
689
|
+
dropLane(id) {
|
|
690
|
+
return this.laneFiles.dropLane(id);
|
|
691
|
+
}
|
|
692
|
+
endLanes(ids) {
|
|
693
|
+
return this.laneFiles.endLanes(ids);
|
|
694
|
+
}
|
|
695
|
+
moveTagsToLanes() {
|
|
696
|
+
return this.laneFiles.moveTagsToLanes();
|
|
697
|
+
}
|
|
698
|
+
async revise(id, change) {
|
|
699
|
+
return this.locked(async () => this.save(change(await this.state(id))));
|
|
700
|
+
}
|
|
701
|
+
async save(state) {
|
|
702
|
+
const path = this.path(state.id);
|
|
703
|
+
const tmp = `${path}.${crypto.randomUUID()}.tmp`;
|
|
704
|
+
await this.fs.write(tmp, `${JSON.stringify(state, null, "\t")}
|
|
705
|
+
`);
|
|
706
|
+
await this.fs.rename(tmp, path);
|
|
707
|
+
return new Todo(this, state);
|
|
708
|
+
}
|
|
709
|
+
async delete(id) {
|
|
710
|
+
await this.locked(async () => {
|
|
711
|
+
await this.fs.rm(this.path(id), { force: true });
|
|
712
|
+
const order = [];
|
|
713
|
+
for (const state of await this.stored()) {
|
|
714
|
+
if (state.blockedBy.includes(id)) {
|
|
715
|
+
const freed = {
|
|
716
|
+
...state,
|
|
717
|
+
blockedBy: state.blockedBy.filter((before) => before !== id)
|
|
718
|
+
};
|
|
719
|
+
await this.save(freed);
|
|
720
|
+
order.push(freed);
|
|
721
|
+
} else {
|
|
722
|
+
order.push(state);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
await this.renumber(order);
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
async discard(state) {
|
|
729
|
+
await this.fs.mkdir(this.removedDir);
|
|
730
|
+
const kept = { state, removedAt: new Date().toISOString() };
|
|
731
|
+
await this.fs.write(this.removedPath(state.id), `${JSON.stringify(kept, null, "\t")}
|
|
732
|
+
`);
|
|
733
|
+
await this.delete(state.id);
|
|
734
|
+
const removed = await this.removed();
|
|
735
|
+
for (const { state: old } of removed.slice(0, -REMOVED_KEPT)) {
|
|
736
|
+
await this.fs.rm(this.removedPath(old.id), { force: true });
|
|
737
|
+
await this.attachments(old.id).clear();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async restore(id) {
|
|
741
|
+
return this.locked(async () => {
|
|
742
|
+
const raw = await this.fs.read(this.removedPath(id)).catch(() => null);
|
|
743
|
+
if (raw === null) {
|
|
744
|
+
fail("not_found", `no removed todo ${id} to put back: only the last ${REMOVED_KEPT} removed are kept`, { todo: id });
|
|
745
|
+
}
|
|
746
|
+
const { state } = JSON.parse(raw);
|
|
747
|
+
const stored = await this.stored();
|
|
748
|
+
if (stored.some((other) => other.id === id)) {
|
|
749
|
+
fail("exists", `todo ${id} is on the list already`, { todo: id });
|
|
750
|
+
}
|
|
751
|
+
const lanes = await this.laneFiles.records(stored);
|
|
752
|
+
const restored = await this.save({
|
|
753
|
+
...state,
|
|
754
|
+
takenBy: null,
|
|
755
|
+
lane: lanes.some((lane) => lane.id === state.lane) ? state.lane : null,
|
|
756
|
+
blockedBy: state.blockedBy.filter((before) => stored.some((other) => other.id === before))
|
|
757
|
+
});
|
|
758
|
+
await this.fs.rm(this.removedPath(id), { force: true });
|
|
759
|
+
await this.place(restored.state, state.position);
|
|
760
|
+
return this.settled(id);
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
async locked(run) {
|
|
764
|
+
await this.lock.acquire();
|
|
765
|
+
try {
|
|
766
|
+
return await run();
|
|
767
|
+
} finally {
|
|
768
|
+
await this.lock.release();
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async settled(id) {
|
|
772
|
+
await this.renumber(settle(await this.stored()));
|
|
773
|
+
return this.find(id);
|
|
774
|
+
}
|
|
775
|
+
ordered(order) {
|
|
776
|
+
const wrong = misordered(order);
|
|
777
|
+
if (wrong) {
|
|
778
|
+
fail("usage", `todo ${wrong.todo.id} comes after todo ${wrong.blockedBy.id} in lane ${wrong.todo.lane}, so it cannot go above it: \`splates todo update ${wrong.todo.id} --remove-blocked-by ${wrong.blockedBy.id}\` first, or move it to another lane`, {
|
|
779
|
+
todo: wrong.todo.id,
|
|
780
|
+
blockedBy: wrong.blockedBy.id,
|
|
781
|
+
lane: wrong.todo.lane
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
async state(id) {
|
|
786
|
+
const state = await this.read(this.path(id));
|
|
787
|
+
return state ?? missing(id);
|
|
788
|
+
}
|
|
789
|
+
async place(state, position) {
|
|
790
|
+
if (position !== undefined && (!Number.isInteger(position) || position <= 0)) {
|
|
791
|
+
fail("usage", `'${position}' is not a position: 1 is the top`, {
|
|
792
|
+
todo: state.id,
|
|
793
|
+
position
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
await this.renumber(slot(await this.stored(), state, position));
|
|
797
|
+
return this.find(state.id);
|
|
798
|
+
}
|
|
799
|
+
async stored(tagged = []) {
|
|
800
|
+
const entries = await this.fs.readdir(this.dir).catch(() => []);
|
|
801
|
+
const states = [];
|
|
802
|
+
for (const entry of entries.filter((entry) => entry.endsWith(".json"))) {
|
|
803
|
+
const state = await this.read(`${this.dir}/${entry}`, tagged);
|
|
804
|
+
if (state) {
|
|
805
|
+
states.push(state);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return states.sort((left, right) => (left.position || Infinity) - (right.position || Infinity) || left.id - right.id);
|
|
809
|
+
}
|
|
810
|
+
async renumber(order) {
|
|
811
|
+
for (const [i, state] of order.entries()) {
|
|
812
|
+
if (state.position !== i + 1) {
|
|
813
|
+
await this.save({ ...state, position: i + 1 });
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
get removedDir() {
|
|
818
|
+
return `${this.dir}/removed`;
|
|
819
|
+
}
|
|
820
|
+
removedPath(id) {
|
|
821
|
+
return `${this.removedDir}/${id}.json`;
|
|
822
|
+
}
|
|
823
|
+
async removed() {
|
|
824
|
+
const entries = await this.fs.readdir(this.removedDir).catch(() => []);
|
|
825
|
+
const kept = [];
|
|
826
|
+
for (const entry of entries.filter((each) => each.endsWith(".json"))) {
|
|
827
|
+
const raw = await this.fs.read(`${this.removedDir}/${entry}`).catch(() => null);
|
|
828
|
+
if (raw !== null) {
|
|
829
|
+
kept.push(JSON.parse(raw));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return kept.sort((left, right) => left.removedAt.localeCompare(right.removedAt));
|
|
833
|
+
}
|
|
834
|
+
path(id) {
|
|
835
|
+
return `${this.dir}/${id}.json`;
|
|
836
|
+
}
|
|
837
|
+
async lastId() {
|
|
838
|
+
const last = Number(await this.fs.read(`${this.dir}/last`).catch(() => 0));
|
|
839
|
+
const ids = (await this.all()).map((todo) => todo.id);
|
|
840
|
+
return Math.max(Number.isInteger(last) ? last : 0, ...ids);
|
|
841
|
+
}
|
|
842
|
+
async read(path, tagged = []) {
|
|
843
|
+
const raw = await this.fs.read(path).catch(() => null);
|
|
844
|
+
const found = raw === null ? null : parseTodoFile(raw);
|
|
845
|
+
if (found !== null && found.tags.length > 0 && !tagged.includes(found.state.id)) {
|
|
846
|
+
fail("outdated", [
|
|
847
|
+
`todo ${found.state.id} was saved with tags, and splates now groups todos in lanes instead, so it reads no todos until their tags are moved.`,
|
|
848
|
+
"Run `splates update` in this repository: it puts each tag's todos in a lane named after the tag, then splates works as before."
|
|
849
|
+
].join(`
|
|
850
|
+
`), { todo: found.state.id });
|
|
851
|
+
}
|
|
852
|
+
return found?.state ?? null;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
var REMOVED_KEPT = 50;
|
|
856
|
+
function slot(states, state, position) {
|
|
857
|
+
const rest = states.filter((other) => other.id !== state.id);
|
|
858
|
+
const at = Math.min((position ?? Infinity) - 1, rest.length);
|
|
859
|
+
return [...rest.slice(0, at), state, ...rest.slice(at)];
|
|
860
|
+
}
|
|
861
|
+
function missing(id) {
|
|
862
|
+
fail("not_found", `no todo ${id}: run \`splates todo list\``, { todo: id });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// lanes-from-tags.ts
|
|
866
|
+
async function lanesFromTags(opts) {
|
|
867
|
+
const log = opts.log ?? new ConsoleLogger;
|
|
868
|
+
const fs = opts.fs ?? new NodeFs2;
|
|
869
|
+
const git = await commonDir(opts.dir, fs);
|
|
870
|
+
if (git === null || !await fs.exists(`${git}/splates/todos`)) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const todos = new Todos(`${git}/splates/todos`, new FileLock(`${git}/splates/todos.lock`, {
|
|
874
|
+
fs,
|
|
875
|
+
ps: opts.ps ?? new NodePs,
|
|
876
|
+
log
|
|
877
|
+
}), { fs });
|
|
878
|
+
for (const { lane, todos: moved } of await todos.moveTagsToLanes()) {
|
|
879
|
+
log.info(`moved ${moved.map((id) => `#${id}`).join(", ")} into lane ${lane.id} ${lane.name}, from their tag`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async function commonDir(dir, fs) {
|
|
883
|
+
const dotGit = `${dir}/.git`;
|
|
884
|
+
if (!await fs.exists(dotGit)) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
if ((await fs.stat(dotGit)).isDirectory()) {
|
|
888
|
+
return dotGit;
|
|
889
|
+
}
|
|
890
|
+
const own = /^gitdir:\s*(.+)$/m.exec(await fs.read(dotGit))?.[1]?.trim();
|
|
891
|
+
if (own === undefined) {
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
const worktree = isAbsolute(own) ? own : resolve(dir, own);
|
|
895
|
+
const common = await fs.read(`${worktree}/commondir`).then((text) => text.trim()).catch(() => ".");
|
|
896
|
+
return isAbsolute(common) ? common : resolve(worktree, common);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// migrate-config.ts
|
|
900
|
+
import { color } from "webappwiz/log";
|
|
901
|
+
|
|
902
|
+
// config-source.ts
|
|
903
|
+
function typedConfig(source) {
|
|
904
|
+
const imported = source.match(DEFINE_CONFIG_IMPORT);
|
|
905
|
+
const code = [...codeOf(source)];
|
|
906
|
+
const calls = code.filter(({ index }, at) => source.startsWith("defineConfig(", index) && !/[\w$.]/.test(source[code[at - 1]?.index ?? -1] ?? ""));
|
|
907
|
+
if (imported?.index === undefined || calls.length !== 1) {
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
const from = imported.index;
|
|
911
|
+
const [call] = calls;
|
|
912
|
+
const open = call.index + "defineConfig".length;
|
|
913
|
+
const close = code.find(({ index, depth }) => index > open && source[index] === ")" && depth === call.depth);
|
|
914
|
+
if (close === undefined) {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
return source.slice(0, from) + 'import type { ConfigFile } from "@stringsync/splates/config";' + source.slice(from + imported[0].length, call.index) + source.slice(open + 1, close.index).trim() + " satisfies ConfigFile" + source.slice(close.index + 1);
|
|
918
|
+
}
|
|
919
|
+
var DEFINE_CONFIG_IMPORT = /import\s*\{\s*defineConfig\s*\}\s*from\s*["'](?:@stringsync\/)?splates\/config["'];?/;
|
|
920
|
+
function withoutSystems(source) {
|
|
921
|
+
const code = [...codeOf(source)];
|
|
922
|
+
const keys = code.filter(({ index, objects }, at) => objects === 1 && KEY.test(source.slice(index)) && !/[\w$.]/.test(source[code[at - 1]?.index ?? -1] ?? ""));
|
|
923
|
+
if (keys.length !== 1) {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
const [key] = keys;
|
|
927
|
+
const colon = source.indexOf(":", key.index);
|
|
928
|
+
const end = code.find(({ index, depth }) => index > colon && (source[index] === "," && depth === key.depth || source[index] === "}" && depth === key.depth - 1));
|
|
929
|
+
if (end === undefined) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
let from = key.index;
|
|
933
|
+
let to = source[end.index] === "," ? end.index + 1 : end.index;
|
|
934
|
+
const lineStart = source.lastIndexOf(`
|
|
935
|
+
`, from - 1) + 1;
|
|
936
|
+
const rest = source.slice(to).match(/^[ \t]*(\/\/[^\n]*)?\n/);
|
|
937
|
+
if (/^\s*$/.test(source.slice(lineStart, from)) && rest !== null) {
|
|
938
|
+
from = lineStart;
|
|
939
|
+
to += rest[0].length;
|
|
940
|
+
} else if (source[end.index] === ",") {
|
|
941
|
+
to += (source.slice(to).match(/^[ \t]*/)?.[0] ?? "").length;
|
|
942
|
+
}
|
|
943
|
+
return source.slice(0, from) + source.slice(to);
|
|
944
|
+
}
|
|
945
|
+
var KEY = /^(systems|"systems"|'systems')\s*:/;
|
|
946
|
+
function* codeOf(source) {
|
|
947
|
+
const open = [];
|
|
948
|
+
const objects = () => open.filter((each) => each === "{").length;
|
|
949
|
+
let i = 0;
|
|
950
|
+
let template = false;
|
|
951
|
+
while (i < source.length) {
|
|
952
|
+
const char = source[i];
|
|
953
|
+
if (template) {
|
|
954
|
+
if (char === "\\") {
|
|
955
|
+
i += 2;
|
|
956
|
+
} else if (char === "`") {
|
|
957
|
+
template = false;
|
|
958
|
+
i++;
|
|
959
|
+
} else if (source.startsWith("${", i)) {
|
|
960
|
+
open.push("${");
|
|
961
|
+
template = false;
|
|
962
|
+
i += 2;
|
|
963
|
+
} else {
|
|
964
|
+
i++;
|
|
965
|
+
}
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (source.startsWith("//", i)) {
|
|
969
|
+
const end = source.indexOf(`
|
|
970
|
+
`, i);
|
|
971
|
+
i = end === -1 ? source.length : end;
|
|
972
|
+
} else if (source.startsWith("/*", i)) {
|
|
973
|
+
const end = source.indexOf("*/", i + 2);
|
|
974
|
+
i = end === -1 ? source.length : end + 2;
|
|
975
|
+
} else if (char === '"' || char === "'") {
|
|
976
|
+
yield { index: i, depth: open.length, objects: objects() };
|
|
977
|
+
i++;
|
|
978
|
+
while (i < source.length && source[i] !== char) {
|
|
979
|
+
i += source[i] === "\\" ? 2 : 1;
|
|
980
|
+
}
|
|
981
|
+
i++;
|
|
982
|
+
} else if (char === "`") {
|
|
983
|
+
template = true;
|
|
984
|
+
i++;
|
|
985
|
+
} else if (char === "}" && open.at(-1) === "${") {
|
|
986
|
+
open.pop();
|
|
987
|
+
template = true;
|
|
988
|
+
i++;
|
|
989
|
+
} else {
|
|
990
|
+
if ("{([".includes(char)) {
|
|
991
|
+
open.push(char);
|
|
992
|
+
} else if ("})]".includes(char)) {
|
|
993
|
+
open.pop();
|
|
994
|
+
}
|
|
995
|
+
yield { index: i, depth: open.length, objects: objects() };
|
|
996
|
+
i++;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// migrate-config.ts
|
|
1002
|
+
async function moveSystems({
|
|
1003
|
+
root,
|
|
1004
|
+
fs,
|
|
1005
|
+
log
|
|
1006
|
+
}) {
|
|
1007
|
+
const config = await new ConfigFiles(fs).importConfig(root);
|
|
1008
|
+
if (config.systems === undefined) {
|
|
1009
|
+
return 0;
|
|
1010
|
+
}
|
|
1011
|
+
const configPath = `${root}/${CONFIG_FILE}`;
|
|
1012
|
+
const systemsPath = `${root}/${SYSTEMS_FILE2}`;
|
|
1013
|
+
const byHand = `move \`systems\` from ${CONFIG_FILE} into ${SYSTEMS_FILE2} by hand`;
|
|
1014
|
+
if (await fs.exists(systemsPath)) {
|
|
1015
|
+
log.error(`both ${CONFIG_FILE} and ${SYSTEMS_FILE2} name systems: ${byHand}`);
|
|
1016
|
+
return 1;
|
|
1017
|
+
}
|
|
1018
|
+
const systems = checkSystems2(`${configPath}: systems`, config.systems);
|
|
1019
|
+
const before = await fs.read(configPath);
|
|
1020
|
+
const after = withoutSystems(before);
|
|
1021
|
+
if (after === null) {
|
|
1022
|
+
log.error(`could not find \`systems\` to take out of ${CONFIG_FILE}: ${byHand}`);
|
|
1023
|
+
return 1;
|
|
1024
|
+
}
|
|
1025
|
+
await fs.write(configPath, after);
|
|
1026
|
+
const { systems: _, ...rest } = config;
|
|
1027
|
+
const rewritten = await new ConfigFiles(fs).importConfig(root).catch(() => null);
|
|
1028
|
+
if (rewritten === null || JSON.stringify(rewritten) !== JSON.stringify(rest)) {
|
|
1029
|
+
await fs.write(configPath, before);
|
|
1030
|
+
log.error(`taking \`systems\` out of ${CONFIG_FILE} changed the rest of it: ${byHand}`);
|
|
1031
|
+
return 1;
|
|
1032
|
+
}
|
|
1033
|
+
await fs.write(systemsPath, systemsSource(systems));
|
|
1034
|
+
const count = Object.keys(systems).length;
|
|
1035
|
+
log.info(`${color.green("moved")} ${count} ${count === 1 ? "system" : "systems"} from ${CONFIG_FILE} to ${SYSTEMS_FILE2}`);
|
|
1036
|
+
return 0;
|
|
1037
|
+
}
|
|
1038
|
+
async function typeConfig({
|
|
1039
|
+
root,
|
|
1040
|
+
fs,
|
|
1041
|
+
log
|
|
1042
|
+
}) {
|
|
1043
|
+
const path = `${root}/${CONFIG_FILE}`;
|
|
1044
|
+
if (!await fs.exists(path)) {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const before = await fs.read(path);
|
|
1048
|
+
if (!before.includes("defineConfig")) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const after = typedConfig(before);
|
|
1052
|
+
if (after === null) {
|
|
1053
|
+
log.info(`left ${CONFIG_FILE} calling \`defineConfig\`: type it with \`satisfies ConfigFile\` by hand`);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const config = await new ConfigFiles(fs).importConfig(root);
|
|
1057
|
+
await fs.write(path, after);
|
|
1058
|
+
const rewritten = await new ConfigFiles(fs).importConfig(root).catch(() => null);
|
|
1059
|
+
if (JSON.stringify(rewritten) !== JSON.stringify(config)) {
|
|
1060
|
+
await fs.write(path, before);
|
|
1061
|
+
log.info(`left ${CONFIG_FILE} calling \`defineConfig\`: type it with \`satisfies ConfigFile\` by hand`);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
log.info(`${color.green("typed")} ${CONFIG_FILE} with \`satisfies ConfigFile\``);
|
|
1065
|
+
}
|
|
1066
|
+
function systemsSource(systems) {
|
|
1067
|
+
const literal = JSON.stringify(systems, null, "\t").replace(/^(\s*)"([A-Za-z_$][\w$]*)":/gm, "$1$2:");
|
|
1068
|
+
return `import type { SystemsFile } from "@stringsync/splates/systems";
|
|
1069
|
+
|
|
1070
|
+
export default ${literal} satisfies SystemsFile;
|
|
1071
|
+
`;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// release.ts
|
|
1075
|
+
import { dirname } from "node:path";
|
|
1076
|
+
import { Duration } from "webappwiz/time";
|
|
1077
|
+
// package.json
|
|
1078
|
+
var package_default = {
|
|
1079
|
+
name: "@stringsync/splates",
|
|
1080
|
+
version: "0.0.45",
|
|
1081
|
+
description: "Runs several AI coding agents on one repository at once, each in its own git worktree",
|
|
1082
|
+
license: "MIT",
|
|
1083
|
+
author: "stringsync LLC",
|
|
1084
|
+
repository: {
|
|
1085
|
+
type: "git",
|
|
1086
|
+
url: "git+https://github.com/stringsync/splates.git",
|
|
1087
|
+
directory: "packages/splates"
|
|
1088
|
+
},
|
|
1089
|
+
type: "module",
|
|
1090
|
+
module: "index.ts",
|
|
1091
|
+
bin: {
|
|
1092
|
+
splates: "./index.ts",
|
|
1093
|
+
arbor: "./index.ts"
|
|
1094
|
+
},
|
|
1095
|
+
exports: {
|
|
1096
|
+
".": "./index.ts",
|
|
1097
|
+
"./asked": "./asked.ts",
|
|
1098
|
+
"./probe-runtime": "./probe-runtime.ts",
|
|
1099
|
+
"./testing/agent": "./fake-agent.ts",
|
|
1100
|
+
"./config": "./config.ts",
|
|
1101
|
+
"./heavy": "./heavy.ts",
|
|
1102
|
+
"./skill-checks": "./skill-checks.ts",
|
|
1103
|
+
"./systems": "./systems.ts",
|
|
1104
|
+
"./tunnel-provider": "./tunnel-provider.ts",
|
|
1105
|
+
"./update": "./update.ts"
|
|
1106
|
+
},
|
|
1107
|
+
imports: {
|
|
1108
|
+
"#dev/*": "./dev/*"
|
|
1109
|
+
},
|
|
1110
|
+
engines: {
|
|
1111
|
+
bun: ">=1.3"
|
|
1112
|
+
},
|
|
1113
|
+
publishConfig: {
|
|
1114
|
+
access: "public"
|
|
1115
|
+
},
|
|
1116
|
+
dependencies: {
|
|
1117
|
+
"@agentclientprotocol/sdk": "^1.7.0",
|
|
1118
|
+
webappwiz: "^0.0.45",
|
|
1119
|
+
zod: "^4.4.3"
|
|
1120
|
+
},
|
|
1121
|
+
devDependencies: {
|
|
1122
|
+
"@assistant-ui/core": "^0.3.28",
|
|
1123
|
+
"@assistant-ui/react": "^0.15.28",
|
|
1124
|
+
"@assistant-ui/react-lexical": "^0.2.17",
|
|
1125
|
+
"@assistant-ui/react-markdown": "^0.14.22",
|
|
1126
|
+
"@assistant-ui/store": "^0.3.20",
|
|
1127
|
+
"@base-ui/react": "^1.8.0",
|
|
1128
|
+
"@dnd-kit/core": "^6.3.1",
|
|
1129
|
+
"@dnd-kit/modifiers": "^9.0.0",
|
|
1130
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
1131
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
1132
|
+
"@happy-dom/global-registrator": "^20.3.1",
|
|
1133
|
+
"@lexical/history": "^0.52.0",
|
|
1134
|
+
"@lexical/plain-text": "^0.52.0",
|
|
1135
|
+
"@lexical/react": "^0.52.0",
|
|
1136
|
+
"@lexical/utils": "^0.52.0",
|
|
1137
|
+
"@tailwindcss/oxide": "^4.3.3",
|
|
1138
|
+
"@testing-library/react": "^16.3.1",
|
|
1139
|
+
"@types/bun": "1.3.14",
|
|
1140
|
+
"@types/react": "^19.2.18",
|
|
1141
|
+
"@types/react-dom": "^19.2.4",
|
|
1142
|
+
"@webappwiz/react": "^0.0.45",
|
|
1143
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
1144
|
+
"@xterm/addon-webgl": "^0.19.0",
|
|
1145
|
+
"@xterm/xterm": "^6.0.0",
|
|
1146
|
+
"class-variance-authority": "^0.7.1",
|
|
1147
|
+
clsx: "^2.1.1",
|
|
1148
|
+
lexical: "^0.52.0",
|
|
1149
|
+
"lucide-react": "^1.49.0",
|
|
1150
|
+
"markdown-to-jsx": "^9.10.3",
|
|
1151
|
+
"radix-ui": "^1.7.0",
|
|
1152
|
+
react: "^19.2.8",
|
|
1153
|
+
"react-dom": "^19.2.8",
|
|
1154
|
+
"remark-gfm": "^4.0.1",
|
|
1155
|
+
"tailwind-merge": "^3.7.0",
|
|
1156
|
+
tailwindcss: "^4.3.3",
|
|
1157
|
+
"tw-animate-css": "^1.4.0",
|
|
1158
|
+
"tw-shimmer": "^0.4.13",
|
|
1159
|
+
zustand: "^5.0.15"
|
|
1160
|
+
},
|
|
1161
|
+
scripts: {
|
|
1162
|
+
build: "bun run build.ts"
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
// release.ts
|
|
1167
|
+
var RUNNING = package_default.version;
|
|
1168
|
+
var REGISTRY = "https://registry.npmjs.org/@stringsync%2fsplates/latest";
|
|
1169
|
+
var PATIENCE = Duration.secs(10);
|
|
1170
|
+
var SOURCE = [
|
|
1171
|
+
import.meta.dirname,
|
|
1172
|
+
dirname(dirname(Bun.fileURLToPath(import.meta.resolve("webappwiz/time"))))
|
|
1173
|
+
];
|
|
1174
|
+
async function sourceCommit(ps, source = SOURCE) {
|
|
1175
|
+
const [here = "", ...rest] = source;
|
|
1176
|
+
try {
|
|
1177
|
+
const { exitCode, stdout } = await ps.spawnCapture([
|
|
1178
|
+
"git",
|
|
1179
|
+
"-C",
|
|
1180
|
+
here,
|
|
1181
|
+
"log",
|
|
1182
|
+
"-1",
|
|
1183
|
+
"--format=%H",
|
|
1184
|
+
"--",
|
|
1185
|
+
here,
|
|
1186
|
+
...rest
|
|
1187
|
+
]);
|
|
1188
|
+
const commit = stdout.trim();
|
|
1189
|
+
return exitCode === 0 && /^[0-9a-f]{40,64}$/.test(commit) ? commit : null;
|
|
1190
|
+
} catch {
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
async function newest(registry = REGISTRY) {
|
|
1195
|
+
try {
|
|
1196
|
+
const response = await fetch(registry, {
|
|
1197
|
+
signal: AbortSignal.timeout(PATIENCE.ms)
|
|
1198
|
+
});
|
|
1199
|
+
if (!response.ok) {
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
const { version } = await response.json();
|
|
1203
|
+
return typeof version === "string" && parse(version) !== null ? version : null;
|
|
1204
|
+
} catch {
|
|
1205
|
+
return null;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function isNewer(version, than) {
|
|
1209
|
+
const [mine, theirs] = [parse(version), parse(than)];
|
|
1210
|
+
if (mine === null || theirs === null) {
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
for (let i = 0;i < 3; i += 1) {
|
|
1214
|
+
if (mine[i] !== theirs[i]) {
|
|
1215
|
+
return mine[i] > theirs[i];
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
function parse(version) {
|
|
1221
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
1222
|
+
return match === null ? null : match.slice(1).map(Number);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// skills.ts
|
|
1226
|
+
import { ProjectSkills, SKILLS_DIR, versionOf } from "webappwiz/skills";
|
|
1227
|
+
var SOURCE2 = "stringsync/splates";
|
|
1228
|
+
function projectSkills(root, deps) {
|
|
1229
|
+
return new ProjectSkills(root, { ...deps, source: SOURCE2 });
|
|
1230
|
+
}
|
|
1231
|
+
function installedVersion(fs, root) {
|
|
1232
|
+
return fs.read(`${root}/${SKILLS_DIR}/splates/SKILL.md`).then(versionOf).catch(() => null);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// update.ts
|
|
1236
|
+
var UPDATING2 = "SPLATES_UPDATING";
|
|
1237
|
+
async function update2({
|
|
1238
|
+
root,
|
|
1239
|
+
fs,
|
|
1240
|
+
ps,
|
|
1241
|
+
log,
|
|
1242
|
+
newest,
|
|
1243
|
+
release
|
|
1244
|
+
}) {
|
|
1245
|
+
if (release === undefined && newest && ps.env(UPDATING2) === undefined) {
|
|
1246
|
+
const latest = await newest();
|
|
1247
|
+
if (latest === null) {
|
|
1248
|
+
log.info(`could not reach npm: refreshing from splates ${RUNNING}`);
|
|
1249
|
+
} else if (isNewer(latest, RUNNING)) {
|
|
1250
|
+
log.info(`${color2.green("updating")} splates ${RUNNING} to ${latest}`);
|
|
1251
|
+
const { exitCode } = await ps.spawn(["bunx", `@stringsync/splates@${latest}`, "update"], { env: { [UPDATING2]: "1" } });
|
|
1252
|
+
return exitCode;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
await lanesFromTags({ dir: root, fs, ps, log });
|
|
1256
|
+
await renameLegacyFiles({ root, fs, ps, log });
|
|
1257
|
+
const skills = projectSkills(root, { fs, ps, log });
|
|
1258
|
+
await skills.add(["splates"], release ?? RUNNING);
|
|
1259
|
+
await removeLegacySkill(`${root}/${skills.path(LEGACY)}`, { fs, log });
|
|
1260
|
+
const code = await moveSystems({ root, fs, log });
|
|
1261
|
+
await typeConfig({ root, fs, log });
|
|
1262
|
+
return code;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
export { RUNNING, sourceCommit, newest, isNewer, projectSkills, installedVersion, lanes, lane, blockersOf, followers, isReady, blockerLabel, mentioned, cameFrom, wording, recordUpdate, todosNamed, Todos, UPDATING2, update2 };
|