isotone 1.1.3 → 1.2.0
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 +79 -5
- package/CHANGELOG.md +34 -0
- package/README.md +64 -14
- package/dist/{IterationPanel-phJRYtUO.d.ts → IterationPanel-Bq77d5NT.d.ts} +3 -1
- package/dist/{chunk-K5RAC2BT.js → chunk-2FY2JNPG.js} +3 -5
- package/dist/chunk-2FY2JNPG.js.map +1 -0
- package/dist/{chunk-RKZWO37H.js → chunk-BTJHQXF7.js} +85 -1
- package/dist/chunk-BTJHQXF7.js.map +1 -0
- package/dist/chunk-JCTWHR7U.js +13 -0
- package/dist/chunk-JCTWHR7U.js.map +1 -0
- package/dist/{chunk-YTEUMO3O.js → chunk-KRS57POF.js} +17 -33
- package/dist/chunk-KRS57POF.js.map +1 -0
- package/dist/chunk-L6O4HYGP.js +1150 -0
- package/dist/chunk-L6O4HYGP.js.map +1 -0
- package/dist/chunk-MX4RQJFV.js +9 -0
- package/dist/chunk-MX4RQJFV.js.map +1 -0
- package/dist/chunk-YPDV3PX4.js +142 -0
- package/dist/chunk-YPDV3PX4.js.map +1 -0
- package/dist/cli.js +1040 -148
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +92 -2
- package/dist/core.js +10 -3
- package/dist/index.d.ts +13 -28
- package/dist/index.js +586 -257
- package/dist/index.js.map +1 -1
- package/dist/isotone.global.js +98 -42
- package/dist/serve.d.ts +5 -3
- package/dist/serve.js +60 -23
- package/dist/serve.js.map +1 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.js +24 -11
- package/dist/server.js.map +1 -1
- package/dist/vite-plugin.d.ts +4 -3
- package/dist/vite-plugin.js +23 -19
- package/dist/vite-plugin.js.map +1 -1
- package/docs/blast.md +164 -0
- package/docs/integrations.md +75 -15
- package/docs/migration.md +19 -0
- package/docs/updates.md +110 -4
- package/docs/variants.md +65 -7
- package/package.json +2 -1
- package/dist/chunk-A4DX2V4C.js +0 -7
- package/dist/chunk-A4DX2V4C.js.map +0 -1
- package/dist/chunk-BRBXIGZP.js +0 -593
- package/dist/chunk-BRBXIGZP.js.map +0 -1
- package/dist/chunk-K5RAC2BT.js.map +0 -1
- package/dist/chunk-LXW6WFOC.js +0 -402
- package/dist/chunk-LXW6WFOC.js.map +0 -1
- package/dist/chunk-RKZWO37H.js.map +0 -1
- package/dist/chunk-RQ2YI5KS.js +0 -24
- package/dist/chunk-RQ2YI5KS.js.map +0 -1
- package/dist/chunk-YTEUMO3O.js.map +0 -1
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
updateNoteText
|
|
3
|
+
} from "./chunk-JCTWHR7U.js";
|
|
4
|
+
import {
|
|
5
|
+
MAX_FEEDBACK_LENGTH,
|
|
6
|
+
OVERLAY_GROUP_ID,
|
|
7
|
+
overlayGroups
|
|
8
|
+
} from "./chunk-BTJHQXF7.js";
|
|
9
|
+
|
|
10
|
+
// src/store.ts
|
|
11
|
+
import { execFileSync } from "child_process";
|
|
12
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
13
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
14
|
+
import { basename, dirname as dirname3, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve, sep as sep2 } from "path";
|
|
15
|
+
|
|
16
|
+
// src/file-store.ts
|
|
17
|
+
import { randomUUID } from "crypto";
|
|
18
|
+
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
19
|
+
import { dirname } from "path";
|
|
20
|
+
function writeJson(file, value) {
|
|
21
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
22
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
23
|
+
try {
|
|
24
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}
|
|
25
|
+
`, { flag: "wx", mode: 384 });
|
|
26
|
+
renameSync(temporary, file);
|
|
27
|
+
} finally {
|
|
28
|
+
rmSync(temporary, { force: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function withFileLock(file, work) {
|
|
32
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
33
|
+
const lock = `${file}.lock`;
|
|
34
|
+
const deadline = Date.now() + 2e3;
|
|
35
|
+
while (true) {
|
|
36
|
+
try {
|
|
37
|
+
const fd = openSync(lock, "wx", 384);
|
|
38
|
+
try {
|
|
39
|
+
writeFileSync(fd, String(process.pid));
|
|
40
|
+
} finally {
|
|
41
|
+
closeSync(fd);
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error.code !== "EEXIST") throw error;
|
|
46
|
+
try {
|
|
47
|
+
const owner = Number(readFileSync(lock, "utf8"));
|
|
48
|
+
if (Number.isSafeInteger(owner) && owner > 0) {
|
|
49
|
+
try {
|
|
50
|
+
process.kill(owner, 0);
|
|
51
|
+
} catch (cause) {
|
|
52
|
+
if (cause.code === "ESRCH") {
|
|
53
|
+
rmSync(lock, { force: true });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
if (Date.now() >= deadline) throw new Error(`Local store is busy: ${file}. Retry after the other writer finishes.`);
|
|
61
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return work();
|
|
66
|
+
} finally {
|
|
67
|
+
rmSync(lock, { force: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/update-request.ts
|
|
72
|
+
import { existsSync, realpathSync, statSync } from "fs";
|
|
73
|
+
import { dirname as dirname2, isAbsolute, join, relative, sep } from "path";
|
|
74
|
+
function parseVersion(value) {
|
|
75
|
+
if (value.length > 256) throw new Error("Invalid version.");
|
|
76
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?(?:\+([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?$/.exec(value);
|
|
77
|
+
if (!match) throw new Error("Invalid version.");
|
|
78
|
+
const prerelease = match[4]?.split(".") ?? [];
|
|
79
|
+
if (prerelease.some((part) => /^0\d+$/.test(part))) throw new Error("Invalid version.");
|
|
80
|
+
return { parts: match.slice(1, 4).map(BigInt), prerelease };
|
|
81
|
+
}
|
|
82
|
+
function compareVersions(a, b) {
|
|
83
|
+
const left = parseVersion(a), right = parseVersion(b);
|
|
84
|
+
for (let i = 0; i < 3; i++) {
|
|
85
|
+
if (left.parts[i] !== right.parts[i]) return left.parts[i] > right.parts[i] ? 1 : -1;
|
|
86
|
+
}
|
|
87
|
+
if (!left.prerelease.length || !right.prerelease.length) {
|
|
88
|
+
return left.prerelease.length === right.prerelease.length ? 0 : left.prerelease.length ? -1 : 1;
|
|
89
|
+
}
|
|
90
|
+
for (let i = 0; i < Math.max(left.prerelease.length, right.prerelease.length); i++) {
|
|
91
|
+
const x = left.prerelease[i], y = right.prerelease[i];
|
|
92
|
+
if (x === y) continue;
|
|
93
|
+
if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
|
|
94
|
+
const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y);
|
|
95
|
+
if (nx && ny) return BigInt(x) > BigInt(y) ? 1 : -1;
|
|
96
|
+
if (nx !== ny) return nx ? -1 : 1;
|
|
97
|
+
return x > y ? 1 : -1;
|
|
98
|
+
}
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
function isVersion(value) {
|
|
102
|
+
if (typeof value !== "string") return false;
|
|
103
|
+
try {
|
|
104
|
+
parseVersion(value);
|
|
105
|
+
return true;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
var plainText = (value, max) => typeof value === "string" && value !== "" && value.length <= max && ![...value].some((char) => {
|
|
111
|
+
const code = char.charCodeAt(0);
|
|
112
|
+
return code < 32 || code === 127;
|
|
113
|
+
});
|
|
114
|
+
var PACKAGE_NAME = "isotone";
|
|
115
|
+
var STRICT_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?$/;
|
|
116
|
+
function isStrictVersion(value) {
|
|
117
|
+
return typeof value === "string" && value.length <= 256 && STRICT_VERSION.test(value) && isVersion(value);
|
|
118
|
+
}
|
|
119
|
+
function upgradeCommand(manager, version, flags) {
|
|
120
|
+
if (!isStrictVersion(version)) throw new Error("Invalid version.");
|
|
121
|
+
const dev = flags.dev ? " -D" : "";
|
|
122
|
+
if (manager === "npm") return `npm install${dev} --save-exact ${PACKAGE_NAME}@${version}`;
|
|
123
|
+
const workspace = flags.workspace ? manager === "pnpm" ? " -w" : " -W" : "";
|
|
124
|
+
return `${manager} add${dev}${workspace} --exact ${PACKAGE_NAME}@${version}`;
|
|
125
|
+
}
|
|
126
|
+
function canonicalUpgradeCommands(manager, version) {
|
|
127
|
+
if (!isStrictVersion(version)) return [];
|
|
128
|
+
const commands = [];
|
|
129
|
+
for (const dev of [true, false]) {
|
|
130
|
+
for (const workspace of manager === "npm" ? [false] : [false, true]) commands.push(upgradeCommand(manager, version, { dev, workspace }));
|
|
131
|
+
}
|
|
132
|
+
return commands;
|
|
133
|
+
}
|
|
134
|
+
function notesProjectRoot(file) {
|
|
135
|
+
const start = dirname2(file);
|
|
136
|
+
for (let directory = start; ; directory = dirname2(directory)) {
|
|
137
|
+
if (existsSync(join(directory, ".git"))) return directory;
|
|
138
|
+
if (dirname2(directory) === directory) return start;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function isInside(cwd, root) {
|
|
142
|
+
try {
|
|
143
|
+
const target = realpathSync(cwd);
|
|
144
|
+
if (!statSync(target).isDirectory()) return false;
|
|
145
|
+
const path = relative(realpathSync(root), target);
|
|
146
|
+
return path === "" || path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path);
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function updateRequestOf(value, context = {}) {
|
|
152
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
153
|
+
const v = value;
|
|
154
|
+
if (!isVersion(v.from) || !isStrictVersion(v.to)) return null;
|
|
155
|
+
if (v.manager !== "npm" && v.manager !== "pnpm" && v.manager !== "yarn") return null;
|
|
156
|
+
if (!plainText(v.command, 500) || !plainText(v.cwd, 4096)) return null;
|
|
157
|
+
if (!canonicalUpgradeCommands(v.manager, v.to).includes(v.command)) return null;
|
|
158
|
+
if (!isAbsolute(v.cwd)) return null;
|
|
159
|
+
if (context.root !== void 0 && !isInside(v.cwd, context.root)) return null;
|
|
160
|
+
if (v.changelog !== void 0 && !(plainText(v.changelog, 500) && v.changelog.startsWith("https://"))) return null;
|
|
161
|
+
return {
|
|
162
|
+
from: v.from,
|
|
163
|
+
to: v.to,
|
|
164
|
+
command: v.command,
|
|
165
|
+
cwd: v.cwd,
|
|
166
|
+
manager: v.manager,
|
|
167
|
+
...v.changelog !== void 0 ? { changelog: v.changelog } : {}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function changelogUrl(version) {
|
|
171
|
+
parseVersion(version);
|
|
172
|
+
return `https://github.com/aaroi/isotone/blob/v${version}/CHANGELOG.md`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/store.ts
|
|
176
|
+
var PAYLOAD_MAX = 65536;
|
|
177
|
+
function readNotes(file) {
|
|
178
|
+
if (!existsSync2(file)) return [];
|
|
179
|
+
try {
|
|
180
|
+
const raw = readFileSync2(file, "utf8").trim();
|
|
181
|
+
if (!raw) return [];
|
|
182
|
+
const data = JSON.parse(raw);
|
|
183
|
+
if (!Array.isArray(data) || !data.every((note) => note && typeof note === "object" && typeof note.id === "string" && typeof note.route === "string" && typeof note.feedback === "string" && typeof note.createdAt === "string")) {
|
|
184
|
+
throw new Error("expected an array of notes");
|
|
185
|
+
}
|
|
186
|
+
return data;
|
|
187
|
+
} catch (cause) {
|
|
188
|
+
throw new Error(`Could not read notes from ${file}; the file has been left untouched.`, { cause });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function writeNotes(file, notes) {
|
|
192
|
+
writeJson(file, notes);
|
|
193
|
+
}
|
|
194
|
+
function updateNotes(file, update) {
|
|
195
|
+
withFileLock(file, () => writeNotes(file, update(readNotes(file))));
|
|
196
|
+
}
|
|
197
|
+
function ensureFileIgnored(file) {
|
|
198
|
+
try {
|
|
199
|
+
const dir = realpathSync2(dirname3(resolve(file)));
|
|
200
|
+
const absolute = join2(dir, basename(file));
|
|
201
|
+
const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
202
|
+
cwd: dir,
|
|
203
|
+
encoding: "utf8",
|
|
204
|
+
// Outside a repo git writes "fatal: not a git repository" to stderr, and
|
|
205
|
+
// inheriting it prints that over the server's own startup banner. The
|
|
206
|
+
// throw is the signal we act on; the message is noise.
|
|
207
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
208
|
+
}).trim();
|
|
209
|
+
const pattern = excludePattern(gitRoot, absolute);
|
|
210
|
+
if (!pattern) return;
|
|
211
|
+
const infoExclude = excludeFile(dir);
|
|
212
|
+
const existing = existsSync2(infoExclude) ? readFileSync2(infoExclude, "utf8") : "";
|
|
213
|
+
if (hasExcludePattern(existing, pattern)) return;
|
|
214
|
+
const marker = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
215
|
+
const heading = "# isotone (local dev feedback loop)";
|
|
216
|
+
const comment = existing.includes(heading) ? "" : `${heading}
|
|
217
|
+
`;
|
|
218
|
+
mkdirSync2(dirname3(infoExclude), { recursive: true });
|
|
219
|
+
writeFileSync2(infoExclude, `${existing}${marker}${comment}${pattern}
|
|
220
|
+
`);
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function ensureDirIgnored(dir) {
|
|
225
|
+
try {
|
|
226
|
+
const absolute = realpathSync2(resolve(dir));
|
|
227
|
+
const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
228
|
+
cwd: absolute,
|
|
229
|
+
encoding: "utf8",
|
|
230
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
231
|
+
}).trim();
|
|
232
|
+
const base = excludePattern(gitRoot, absolute);
|
|
233
|
+
if (!base) return;
|
|
234
|
+
const pattern = `${base}/`;
|
|
235
|
+
const infoExclude = excludeFile(absolute);
|
|
236
|
+
const existing = existsSync2(infoExclude) ? readFileSync2(infoExclude, "utf8") : "";
|
|
237
|
+
if (hasExcludePattern(existing, pattern) || hasExcludePattern(existing, base)) return;
|
|
238
|
+
const marker = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
239
|
+
mkdirSync2(dirname3(infoExclude), { recursive: true });
|
|
240
|
+
writeFileSync2(
|
|
241
|
+
infoExclude,
|
|
242
|
+
`${existing}${marker}# isotone (local overlay variants)
|
|
243
|
+
${pattern}
|
|
244
|
+
`
|
|
245
|
+
);
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function excludeFile(cwd) {
|
|
250
|
+
const reported = execFileSync("git", ["rev-parse", "--git-path", "info/exclude"], {
|
|
251
|
+
cwd,
|
|
252
|
+
encoding: "utf8",
|
|
253
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
254
|
+
}).trim();
|
|
255
|
+
return resolve(cwd, reported);
|
|
256
|
+
}
|
|
257
|
+
function excludePattern(gitRoot, absolute) {
|
|
258
|
+
const rel = relative2(gitRoot, absolute);
|
|
259
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute2(rel)) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return `/${rel.split(sep2).join("/")}`;
|
|
263
|
+
}
|
|
264
|
+
function hasExcludePattern(exclude, pattern) {
|
|
265
|
+
const bare = pattern.replace(/^\//, "");
|
|
266
|
+
return exclude.split(/\r?\n/).some((line) => line.trim() === pattern || line.trim() === bare);
|
|
267
|
+
}
|
|
268
|
+
function toItem(n, root) {
|
|
269
|
+
const update = updateRequestOf(n.update, { root });
|
|
270
|
+
const item = {
|
|
271
|
+
id: n.id,
|
|
272
|
+
route: n.route,
|
|
273
|
+
// An update note says what the server's request says, whatever the file holds.
|
|
274
|
+
feedback: update ? updateNoteText(update) : n.feedback,
|
|
275
|
+
element: n.element,
|
|
276
|
+
done: n.done,
|
|
277
|
+
createdAt: n.createdAt
|
|
278
|
+
};
|
|
279
|
+
if (n.workingAt && !n.done) item.workingAt = n.workingAt;
|
|
280
|
+
if (n.viewport !== void 0) item.viewport = n.viewport;
|
|
281
|
+
if (n.payload !== void 0) item.payload = n.payload;
|
|
282
|
+
return {
|
|
283
|
+
...item,
|
|
284
|
+
...n.elements !== void 0 ? { elements: n.elements } : {},
|
|
285
|
+
...n.variant !== void 0 ? { variant: n.variant } : {},
|
|
286
|
+
...n.variantAction !== void 0 ? { variantAction: n.variantAction } : {},
|
|
287
|
+
// Only the server writes an update request; a malformed one, or one whose
|
|
288
|
+
// directory is outside the project, is not shown.
|
|
289
|
+
...update ? { update } : {},
|
|
290
|
+
// Only the CLI writes a reply; a hand-edited, malformed one is not shown.
|
|
291
|
+
...replyView(n) ? { reply: replyView(n) } : {}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function notesResponse(file, req) {
|
|
295
|
+
if (req.method === "POST" || req.method === "DELETE") {
|
|
296
|
+
return withFileLock(file, () => handleNotesRequest(file, req));
|
|
297
|
+
}
|
|
298
|
+
return handleNotesRequest(file, req);
|
|
299
|
+
}
|
|
300
|
+
function handleNotesRequest(file, req) {
|
|
301
|
+
const method = req.method ?? "";
|
|
302
|
+
const query = Object.fromEntries(
|
|
303
|
+
new URL(req.url ?? "/", "http://localhost").searchParams
|
|
304
|
+
);
|
|
305
|
+
const json = (status, body) => ({
|
|
306
|
+
status,
|
|
307
|
+
headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
|
|
308
|
+
body
|
|
309
|
+
});
|
|
310
|
+
if (method === "GET") {
|
|
311
|
+
const polled = query.rev !== void 0;
|
|
312
|
+
const rev = polled ? notesRevision(file) : "";
|
|
313
|
+
if (polled && rev === String(query.rev)) return json(200, { rev, unchanged: true });
|
|
314
|
+
const route = String(query.route ?? "");
|
|
315
|
+
const root = notesProjectRoot(file);
|
|
316
|
+
const all = query.all !== void 0 && String(query.all) !== "" && String(query.all) !== "0";
|
|
317
|
+
const notes = polled ? readNotesAt(file, rev) : readNotes(file);
|
|
318
|
+
const rows = (all ? [...notes] : route ? notes.filter((n) => n.route === route) : notes.filter((n) => !n.done)).sort((a, b) => b.createdAt.localeCompare(a.createdAt)).map((note) => toItem(note, root));
|
|
319
|
+
return json(200, polled ? { rev, notes: rows } : rows);
|
|
320
|
+
}
|
|
321
|
+
if (method === "DELETE") {
|
|
322
|
+
const id = String(query.id ?? "");
|
|
323
|
+
if (!id) return json(400, { error: "id is required" });
|
|
324
|
+
writeNotes(file, readNotes(file).filter((n) => n.id !== id));
|
|
325
|
+
return json(200, { ok: true });
|
|
326
|
+
}
|
|
327
|
+
if (method === "POST") {
|
|
328
|
+
const body = req.body ?? {};
|
|
329
|
+
const { id, route, feedback, element, payload, viewport } = body;
|
|
330
|
+
if (id !== void 0 && id !== null && (typeof id !== "string" || !id || id.length > 200)) {
|
|
331
|
+
return json(400, { error: "id must be a non-empty string of at most 200 characters" });
|
|
332
|
+
}
|
|
333
|
+
if (typeof route !== "string" || route === "") {
|
|
334
|
+
return json(400, { error: "route is required" });
|
|
335
|
+
}
|
|
336
|
+
const cleanFeedback = typeof feedback === "string" ? feedback.trim() : "";
|
|
337
|
+
if (cleanFeedback === "") {
|
|
338
|
+
return json(400, { error: "feedback is required" });
|
|
339
|
+
}
|
|
340
|
+
if (cleanFeedback.length > MAX_FEEDBACK_LENGTH) return json(400, { error: `feedback must be at most ${MAX_FEEDBACK_LENGTH} characters` });
|
|
341
|
+
if (payload !== void 0 && !withinPayloadCap(payload)) {
|
|
342
|
+
return json(400, {
|
|
343
|
+
error: `payload exceeds ${PAYLOAD_MAX} bytes or is not serializable`
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
const cleanViewport = viewportOf(viewport);
|
|
347
|
+
if (body.elements !== void 0 && (!Array.isArray(body.elements) || body.elements.length > 50)) {
|
|
348
|
+
return json(400, { error: "elements must be an array of at most 50 elements" });
|
|
349
|
+
}
|
|
350
|
+
const elements = body.elements === void 0 ? void 0 : body.elements.map(elementOf).filter((el) => el !== null);
|
|
351
|
+
const variant = body.variant === void 0 ? void 0 : variantOf(body.variant);
|
|
352
|
+
const variantAction = removalOf(body.variantAction);
|
|
353
|
+
if (body.variantAction !== void 0 && !variantAction) return json(400, { error: "Invalid variant action" });
|
|
354
|
+
let cleanElement = null;
|
|
355
|
+
if (element && typeof element === "object" && !Array.isArray(element)) {
|
|
356
|
+
cleanElement = {
|
|
357
|
+
tag: cap(String(element.tag ?? ""), 30),
|
|
358
|
+
text: cap(String(element.text ?? ""), 120),
|
|
359
|
+
selector: cap(String(element.selector ?? ""), MAX_SELECTOR),
|
|
360
|
+
...elementContext(element)
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const notes = readNotes(file);
|
|
364
|
+
const defaultsRequest = cleanFeedback.startsWith("[set-defaults]");
|
|
365
|
+
const previousDefaults = defaultsRequest && !notes.some((note) => note.id === id) ? notes.filter((n) => !n.done && n.route === route && n.feedback.startsWith("[set-defaults]")).sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0] : void 0;
|
|
366
|
+
const updateId = previousDefaults?.id ?? id;
|
|
367
|
+
if (typeof updateId === "string" && updateId !== "") {
|
|
368
|
+
const existing = notes.find((n) => n.id === updateId && n.route === route);
|
|
369
|
+
if (existing?.update !== void 0) {
|
|
370
|
+
return json(409, { error: "An update note is written by Isotone and cannot be edited. Delete it to cancel the update." });
|
|
371
|
+
}
|
|
372
|
+
if (existing) {
|
|
373
|
+
if (previousDefaults) {
|
|
374
|
+
delete existing.elements;
|
|
375
|
+
delete existing.variant;
|
|
376
|
+
delete existing.variantAction;
|
|
377
|
+
delete existing.payload;
|
|
378
|
+
}
|
|
379
|
+
existing.feedback = cleanFeedback;
|
|
380
|
+
existing.element = cleanElement;
|
|
381
|
+
if (elements !== void 0) {
|
|
382
|
+
existing.elements = elements;
|
|
383
|
+
existing.element = elements[0] ?? null;
|
|
384
|
+
} else if (existing.elements) existing.element = existing.elements[0] ?? null;
|
|
385
|
+
if (variant !== void 0) existing.variant = variant;
|
|
386
|
+
if (variantAction) existing.variantAction = variantAction;
|
|
387
|
+
if (payload !== void 0) existing.payload = payload;
|
|
388
|
+
if (defaultsRequest && !existing.done) {
|
|
389
|
+
existing.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
390
|
+
if (cleanViewport) existing.viewport = cleanViewport;
|
|
391
|
+
writeNotes(file, notes.filter((n) => n.id === existing.id || n.done || n.route !== route || !n.feedback.startsWith("[set-defaults]")));
|
|
392
|
+
} else writeNotes(file, notes);
|
|
393
|
+
return json(200, { id: existing.id });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (typeof id === "string" && notes.some((note) => note.id === id)) {
|
|
397
|
+
return json(409, { error: "This note belongs to a different route" });
|
|
398
|
+
}
|
|
399
|
+
const newId = typeof id === "string" ? id : randomUUID2();
|
|
400
|
+
const createdAt = typeof body.createdAt === "string" && Number.isFinite(Date.parse(body.createdAt)) ? new Date(body.createdAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
401
|
+
notes.push({
|
|
402
|
+
id: newId,
|
|
403
|
+
route,
|
|
404
|
+
feedback: cleanFeedback,
|
|
405
|
+
element: cleanElement,
|
|
406
|
+
done: false,
|
|
407
|
+
doneAt: null,
|
|
408
|
+
createdAt,
|
|
409
|
+
...cleanViewport ? { viewport: cleanViewport } : {},
|
|
410
|
+
...payload !== void 0 ? { payload } : {},
|
|
411
|
+
...elements !== void 0 ? { elements, element: elements[0] ?? null } : {},
|
|
412
|
+
...variant !== void 0 ? { variant } : {},
|
|
413
|
+
...variantAction ? { variantAction } : {}
|
|
414
|
+
});
|
|
415
|
+
writeNotes(file, notes);
|
|
416
|
+
return json(201, { id: newId });
|
|
417
|
+
}
|
|
418
|
+
return json(405, { error: "method not allowed" });
|
|
419
|
+
}
|
|
420
|
+
var parsed = /* @__PURE__ */ new Map();
|
|
421
|
+
function readNotesAt(file, rev) {
|
|
422
|
+
const cached = parsed.get(file);
|
|
423
|
+
if (cached && cached.rev === rev && rev !== "none") return cached.notes;
|
|
424
|
+
const notes = readNotes(file);
|
|
425
|
+
parsed.set(file, { rev, notes });
|
|
426
|
+
return notes;
|
|
427
|
+
}
|
|
428
|
+
function notesRevision(file) {
|
|
429
|
+
try {
|
|
430
|
+
const stat = statSync2(file, { bigint: true });
|
|
431
|
+
return `${stat.ino.toString(36)}.${stat.mtimeNs.toString(36)}.${stat.size.toString(36)}`;
|
|
432
|
+
} catch {
|
|
433
|
+
return "none";
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
var MAX_REPLY_LENGTH = 2e3;
|
|
437
|
+
function replyOf(value) {
|
|
438
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
439
|
+
const v = value;
|
|
440
|
+
if (typeof v.text !== "string" || !v.text.trim() || typeof v.at !== "string" || !Number.isFinite(Date.parse(v.at))) return null;
|
|
441
|
+
return { text: v.text.slice(0, MAX_REPLY_LENGTH), at: v.at, ...typeof v.about === "string" && v.about.length <= 64 ? { about: v.about } : {} };
|
|
442
|
+
}
|
|
443
|
+
function noteFingerprint(note) {
|
|
444
|
+
const update = updateRequestOf(note.update);
|
|
445
|
+
const content = JSON.stringify([
|
|
446
|
+
note.feedback,
|
|
447
|
+
note.route,
|
|
448
|
+
(note.elements ?? (note.element ? [note.element] : [])).map((element) => element.selector),
|
|
449
|
+
note.variant ?? null,
|
|
450
|
+
note.variantAction ?? null,
|
|
451
|
+
update?.to ?? null
|
|
452
|
+
]);
|
|
453
|
+
return createHash("sha256").update(content).digest("base64url").slice(0, 16);
|
|
454
|
+
}
|
|
455
|
+
function replyView(note) {
|
|
456
|
+
const reply = replyOf(note.reply);
|
|
457
|
+
if (!reply) return null;
|
|
458
|
+
return reply.about !== void 0 && reply.about !== noteFingerprint(note) ? { ...reply, answered: true } : reply;
|
|
459
|
+
}
|
|
460
|
+
function cap(v, max) {
|
|
461
|
+
return v.slice(0, max);
|
|
462
|
+
}
|
|
463
|
+
function elementOf(value) {
|
|
464
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
465
|
+
const v = value;
|
|
466
|
+
if (typeof v.selector !== "string" || !v.selector) return null;
|
|
467
|
+
return {
|
|
468
|
+
tag: cap(String(v.tag ?? ""), 30),
|
|
469
|
+
text: cap(String(v.text ?? ""), 120),
|
|
470
|
+
selector: cap(v.selector, MAX_SELECTOR),
|
|
471
|
+
...elementContext(v)
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
var MAX_SELECTOR = 2e3;
|
|
475
|
+
function stringList(value, count, length) {
|
|
476
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item !== "").slice(0, count).map((item) => cap(item, length)) : [];
|
|
477
|
+
}
|
|
478
|
+
function elementContext(value) {
|
|
479
|
+
const v = value;
|
|
480
|
+
const out = {};
|
|
481
|
+
if (v.attributes && typeof v.attributes === "object" && !Array.isArray(v.attributes)) {
|
|
482
|
+
const entries = Object.entries(v.attributes).filter((entry) => typeof entry[1] === "string" && !UNSAFE_KEYS.has(entry[0])).slice(0, 20).map(([name, text]) => [cap(name, 60), cap(text, 300)]);
|
|
483
|
+
if (entries.length) out.attributes = Object.fromEntries(entries);
|
|
484
|
+
}
|
|
485
|
+
const component = stringList(v.component, 8, 80);
|
|
486
|
+
if (component.length) out.component = component;
|
|
487
|
+
const within = stringList(v.within, 6, 120);
|
|
488
|
+
if (within.length) out.within = within;
|
|
489
|
+
if (typeof v.source === "string" && v.source) out.source = cap(v.source, 300);
|
|
490
|
+
if (typeof v.near === "string" && v.near) out.near = cap(v.near, 120);
|
|
491
|
+
const rect = v.rect;
|
|
492
|
+
if (rect && typeof rect === "object" && [rect.x, rect.y, rect.w, rect.h].every((n) => typeof n === "number" && Number.isFinite(n))) {
|
|
493
|
+
out.rect = { x: rect.x, y: rect.y, w: rect.w, h: rect.h };
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
498
|
+
function variantOf(value) {
|
|
499
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
500
|
+
const v = value;
|
|
501
|
+
if (typeof v.axisId !== "string" || typeof v.label !== "string") return null;
|
|
502
|
+
return {
|
|
503
|
+
axisId: cap(v.axisId, 200),
|
|
504
|
+
label: cap(v.label, 200),
|
|
505
|
+
...typeof v.optionId === "string" ? { optionId: cap(v.optionId, 200) } : {},
|
|
506
|
+
...typeof v.optionLabel === "string" ? { optionLabel: cap(v.optionLabel, 200) } : {},
|
|
507
|
+
...typeof v.value === "string" ? { value: cap(v.value, 200) } : {}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function viewportOf(v) {
|
|
511
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return null;
|
|
512
|
+
const w = Math.round(Number(v.w));
|
|
513
|
+
const h = Math.round(Number(v.h));
|
|
514
|
+
if (!Number.isFinite(w) || !Number.isFinite(h)) return null;
|
|
515
|
+
if (w <= 0 || h <= 0 || w > 1e5 || h > 1e5) return null;
|
|
516
|
+
return { w, h };
|
|
517
|
+
}
|
|
518
|
+
function withinPayloadCap(payload) {
|
|
519
|
+
try {
|
|
520
|
+
return Buffer.byteLength(JSON.stringify(payload), "utf8") <= PAYLOAD_MAX;
|
|
521
|
+
} catch {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
var NOTES_ENDPOINT = "/api/iteration/notes";
|
|
526
|
+
function removalOf(value) {
|
|
527
|
+
if (!value || typeof value !== "object") return;
|
|
528
|
+
const v = value;
|
|
529
|
+
if (typeof v.axisId !== "string" || !v.axisId || v.axisId.length > 200 || !["remove-option", "remove-axis", "keep-default"].includes(v.action ?? "")) return;
|
|
530
|
+
if (v.action === "remove-option" && (typeof v.optionId !== "string" || !v.optionId || v.optionId.length > 200)) return;
|
|
531
|
+
return { axisId: v.axisId, action: v.action, ...typeof v.optionId === "string" ? { optionId: v.optionId.slice(0, 200) } : {} };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/variants-store.ts
|
|
535
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
536
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
537
|
+
import {
|
|
538
|
+
existsSync as existsSync3,
|
|
539
|
+
mkdirSync as mkdirSync3,
|
|
540
|
+
lstatSync,
|
|
541
|
+
readdirSync,
|
|
542
|
+
readFileSync as readFileSync3,
|
|
543
|
+
realpathSync as realpathSync3,
|
|
544
|
+
rmSync as rmSync2,
|
|
545
|
+
writeFileSync as writeFileSync3
|
|
546
|
+
} from "fs";
|
|
547
|
+
import { tmpdir } from "os";
|
|
548
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join3, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
|
|
549
|
+
var DEFAULT_VARIANTS_DIR = ".isotone/variants";
|
|
550
|
+
var ACTIVE_FILE = ".isotone/active.json";
|
|
551
|
+
var KNOBS_FILE = ".isotone/knobs.json";
|
|
552
|
+
function mutateVariants(opts, change) {
|
|
553
|
+
return withFileLock(join3(variantsDir(opts), ".mutation"), change);
|
|
554
|
+
}
|
|
555
|
+
function variantsDir(opts) {
|
|
556
|
+
const dir = opts.dir ?? DEFAULT_VARIANTS_DIR;
|
|
557
|
+
return isAbsolute3(dir) ? dir : resolve2(opts.root, dir);
|
|
558
|
+
}
|
|
559
|
+
function variantDir(opts, id) {
|
|
560
|
+
if (!isValidVariantId(id)) throw new Error(`Invalid variant id "${id}"`);
|
|
561
|
+
return join3(variantsDir(opts), id);
|
|
562
|
+
}
|
|
563
|
+
function isValidVariantId(id) {
|
|
564
|
+
return /^[a-z0-9][a-z0-9-]{0,63}$/.test(id);
|
|
565
|
+
}
|
|
566
|
+
function toPosixPath(p) {
|
|
567
|
+
return p.split(sep3).join("/");
|
|
568
|
+
}
|
|
569
|
+
function isEscaping(rel) {
|
|
570
|
+
return rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute3(rel);
|
|
571
|
+
}
|
|
572
|
+
function staysInside(base, file) {
|
|
573
|
+
const canonical = (path) => {
|
|
574
|
+
let existing = resolve2(path);
|
|
575
|
+
while (dirname4(existing) !== existing) {
|
|
576
|
+
try {
|
|
577
|
+
lstatSync(existing);
|
|
578
|
+
break;
|
|
579
|
+
} catch (error) {
|
|
580
|
+
if (error.code !== "ENOENT") throw error;
|
|
581
|
+
existing = dirname4(existing);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return resolve2(realpathSync3(existing), relative3(existing, resolve2(path)));
|
|
585
|
+
};
|
|
586
|
+
return !isEscaping(relative3(canonical(base), canonical(file)));
|
|
587
|
+
}
|
|
588
|
+
function repoRelativeKey(root, file) {
|
|
589
|
+
const abs = isAbsolute3(file) ? resolve2(file) : resolve2(root, file);
|
|
590
|
+
const rel = relative3(root, abs);
|
|
591
|
+
if (!rel || isEscaping(rel) || !isSafeManifestKey(toPosixPath(rel)) || !staysInside(root, abs)) {
|
|
592
|
+
throw new Error(`"${file}" escapes the project root`);
|
|
593
|
+
}
|
|
594
|
+
return { abs, key: toPosixPath(rel) };
|
|
595
|
+
}
|
|
596
|
+
function isSafeManifestKey(key) {
|
|
597
|
+
if (!key || isAbsolute3(key) || /[\\:\0]/.test(key) || key === "variant.json") return false;
|
|
598
|
+
return !key.split("/").some((segment) => !segment || segment === "." || segment === ".." || segment === ".git");
|
|
599
|
+
}
|
|
600
|
+
function isPlausibleVariant(v) {
|
|
601
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return false;
|
|
602
|
+
const o = v;
|
|
603
|
+
return typeof o.id === "string" && typeof o.label === "string" && typeof o.createdAt === "string" && (o.route === void 0 || typeof o.route === "string") && (o.description === void 0 || typeof o.description === "string") && (o.group === void 0 || typeof o.group === "string" && OVERLAY_GROUP_ID.test(o.group)) && (o.groupLabel === void 0 || typeof o.groupLabel === "string") && (o.section === void 0 || typeof o.section === "string") && !!o.files && typeof o.files === "object" && !Array.isArray(o.files) && Object.entries(o.files).every(([key, entry]) => isSafeManifestKey(key) && entry && typeof entry === "object" && "base" in entry && (entry.base === null || typeof entry.base === "string" && /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(entry.base)) && (!("added" in entry) || entry.added === true));
|
|
604
|
+
}
|
|
605
|
+
function readVariant(opts, id) {
|
|
606
|
+
if (!isValidVariantId(id)) return null;
|
|
607
|
+
const file = join3(variantDir(opts, id), "variant.json");
|
|
608
|
+
if (!existsSync3(file)) return null;
|
|
609
|
+
try {
|
|
610
|
+
const data = JSON.parse(readFileSync3(file, "utf8"));
|
|
611
|
+
const dir = variantDir(opts, id);
|
|
612
|
+
return isPlausibleVariant(data) && data.id === id && staysInside(variantsDir(opts), file) && Object.keys(data.files).every((key) => staysInside(opts.root, join3(opts.root, key)) && staysInside(dir, join3(dir, key))) ? data : null;
|
|
613
|
+
} catch {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function writeVariant(opts, v) {
|
|
618
|
+
if (!isPlausibleVariant(v)) throw new Error("Invalid variant manifest");
|
|
619
|
+
const dir = variantDir(opts, v.id);
|
|
620
|
+
if (!staysInside(variantsDir(opts), dir)) throw new Error("Variant directory escapes the variants tree");
|
|
621
|
+
mkdirSync3(dir, { recursive: true });
|
|
622
|
+
writeJson(join3(dir, "variant.json"), v);
|
|
623
|
+
}
|
|
624
|
+
function isStale(base, currentHash) {
|
|
625
|
+
return base === null || currentHash === null || currentHash !== base;
|
|
626
|
+
}
|
|
627
|
+
function readVariants(opts) {
|
|
628
|
+
const dir = variantsDir(opts);
|
|
629
|
+
if (!existsSync3(dir)) return [];
|
|
630
|
+
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => readVariant(opts, e.name)).filter((v) => v !== null).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
631
|
+
}
|
|
632
|
+
function listVariants(opts) {
|
|
633
|
+
const dir = variantsDir(opts);
|
|
634
|
+
if (!existsSync3(dir)) return [];
|
|
635
|
+
const root = resolve2(opts.root);
|
|
636
|
+
const ids = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
637
|
+
const statuses = [];
|
|
638
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
639
|
+
for (const id of ids) {
|
|
640
|
+
const manifest = readVariant(opts, id);
|
|
641
|
+
if (!manifest) continue;
|
|
642
|
+
const stale = [];
|
|
643
|
+
const missing = [];
|
|
644
|
+
for (const [file, entry] of Object.entries(manifest.files)) {
|
|
645
|
+
const overrideAbs = join3(dir, id, file);
|
|
646
|
+
if (!existsSync3(overrideAbs)) {
|
|
647
|
+
missing.push(file);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (entry.added) {
|
|
651
|
+
if (existsSync3(join3(root, file))) stale.push(file);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const hashKey = `${entry.base?.length}:${file}`;
|
|
655
|
+
if (!hashes.has(hashKey)) hashes.set(hashKey, hashFile(join3(root, file), entry.base));
|
|
656
|
+
if (isStale(entry.base, hashes.get(hashKey) ?? null)) stale.push(file);
|
|
657
|
+
}
|
|
658
|
+
statuses.push({ ...manifest, stale, missing });
|
|
659
|
+
}
|
|
660
|
+
statuses.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
661
|
+
return statuses;
|
|
662
|
+
}
|
|
663
|
+
function createVariant(opts, input) {
|
|
664
|
+
return mutateVariants(opts, () => {
|
|
665
|
+
if (!isValidVariantId(input.id)) {
|
|
666
|
+
throw new Error(`Invalid variant id "${input.id}"`);
|
|
667
|
+
}
|
|
668
|
+
if (input.group !== void 0) assertGroupId(input.group);
|
|
669
|
+
if (readVariant(opts, input.id)) {
|
|
670
|
+
throw new Error(`Variant "${input.id}" already exists`);
|
|
671
|
+
}
|
|
672
|
+
const variant = {
|
|
673
|
+
id: input.id,
|
|
674
|
+
label: input.label || input.id,
|
|
675
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
676
|
+
files: {},
|
|
677
|
+
...input.route !== void 0 ? { route: input.route } : {},
|
|
678
|
+
...input.element !== void 0 ? { element: input.element } : {},
|
|
679
|
+
...input.note !== void 0 ? { note: input.note } : {},
|
|
680
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
681
|
+
...input.group !== void 0 ? { group: input.group } : {},
|
|
682
|
+
...input.groupLabel !== void 0 ? { groupLabel: input.groupLabel } : {},
|
|
683
|
+
...input.section !== void 0 ? { section: input.section } : {}
|
|
684
|
+
};
|
|
685
|
+
writeVariant(opts, variant);
|
|
686
|
+
return variant;
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function assertGroupId(group) {
|
|
690
|
+
if (!OVERLAY_GROUP_ID.test(group)) {
|
|
691
|
+
throw new Error(`Invalid group id "${group}" (lowercase letters, digits and hyphens)`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function setVariantGroup(opts, id, group, groupLabel) {
|
|
695
|
+
return mutateVariants(opts, () => {
|
|
696
|
+
const manifest = readVariant(opts, id);
|
|
697
|
+
if (!manifest) throw new Error(`No variant "${id}"`);
|
|
698
|
+
assertGroupId(group);
|
|
699
|
+
manifest.group = group;
|
|
700
|
+
if (groupLabel !== void 0) manifest.groupLabel = groupLabel;
|
|
701
|
+
writeVariant(opts, manifest);
|
|
702
|
+
return manifest;
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
function setVariantSection(opts, id, section) {
|
|
706
|
+
return mutateVariants(opts, () => {
|
|
707
|
+
const manifest = readVariant(opts, id);
|
|
708
|
+
if (!manifest) throw new Error(`No variant "${id}"`);
|
|
709
|
+
if (section === void 0) delete manifest.section;
|
|
710
|
+
else manifest.section = section.replace(/\s+/g, " ").trim();
|
|
711
|
+
writeVariant(opts, manifest);
|
|
712
|
+
return manifest;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function addVariantFile(opts, id, file, { create = false } = {}) {
|
|
716
|
+
return mutateVariants(opts, () => {
|
|
717
|
+
const manifest = readVariant(opts, id);
|
|
718
|
+
if (!manifest) throw new Error(`No variant "${id}"`);
|
|
719
|
+
const root = resolve2(opts.root);
|
|
720
|
+
const { abs: sourceAbs, key } = repoRelativeKey(root, file);
|
|
721
|
+
if (Object.hasOwn(manifest.files, key)) return manifest;
|
|
722
|
+
const destAbs = join3(variantDir(opts, id), key);
|
|
723
|
+
if (!staysInside(variantDir(opts, id), destAbs)) throw new Error("Override path escapes the variant directory");
|
|
724
|
+
if (create) {
|
|
725
|
+
if (existsSync3(sourceAbs)) throw new Error(`"${file}" already exists; add it without --new`);
|
|
726
|
+
mkdirSync3(dirname4(destAbs), { recursive: true });
|
|
727
|
+
if (!existsSync3(destAbs)) writeFileSync3(destAbs, "");
|
|
728
|
+
manifest.files = { ...manifest.files, [key]: { base: null, added: true } };
|
|
729
|
+
writeVariant(opts, manifest);
|
|
730
|
+
return manifest;
|
|
731
|
+
}
|
|
732
|
+
if (!existsSync3(sourceAbs)) throw new Error(`"${file}" does not exist. Use --new for a file the variant adds.`);
|
|
733
|
+
mkdirSync3(dirname4(destAbs), { recursive: true });
|
|
734
|
+
writeFileSync3(destAbs, readFileSync3(sourceAbs));
|
|
735
|
+
manifest.files = { ...manifest.files, [key]: { base: blobOf(sourceAbs) } };
|
|
736
|
+
writeVariant(opts, manifest);
|
|
737
|
+
return manifest;
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
function removeVariantFile(opts, id, file) {
|
|
741
|
+
return mutateVariants(opts, () => {
|
|
742
|
+
const manifest = readVariant(opts, id);
|
|
743
|
+
if (!manifest) throw new Error(`No variant "${id}"`);
|
|
744
|
+
const root = resolve2(opts.root);
|
|
745
|
+
const { key } = repoRelativeKey(root, file);
|
|
746
|
+
if (Object.hasOwn(manifest.files, key)) {
|
|
747
|
+
delete manifest.files[key];
|
|
748
|
+
rmSync2(join3(variantDir(opts, id), key), { force: true });
|
|
749
|
+
writeVariant(opts, manifest);
|
|
750
|
+
}
|
|
751
|
+
return manifest;
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
var CONFLICT_MARKER = "<<<<<<< variant";
|
|
755
|
+
function promoteVariant(opts, id, { force = false } = {}) {
|
|
756
|
+
return mutateVariants(opts, () => {
|
|
757
|
+
if (!isValidVariantId(id)) throw new Error(`Invalid variant id "${id}"`);
|
|
758
|
+
const manifest = readVariant(opts, id);
|
|
759
|
+
if (!manifest) throw new Error(`No variant "${id}"`);
|
|
760
|
+
const active = readActive(opts);
|
|
761
|
+
const root = resolve2(opts.root);
|
|
762
|
+
const vDir = variantDir(opts, id);
|
|
763
|
+
const missing = Object.keys(manifest.files).filter((key) => !existsSync3(join3(vDir, key)));
|
|
764
|
+
if (missing.length) throw new Error(`"${id}" has missing overrides: ${missing.join(", ")}. Restore or remove them before promoting.`);
|
|
765
|
+
if (!force) {
|
|
766
|
+
const status = listVariants(opts).find((v) => v.id === id);
|
|
767
|
+
if (status?.stale.length) {
|
|
768
|
+
const reasons = status.stale.map(
|
|
769
|
+
(file) => manifest.files[file]?.base === null ? `no base recorded for ${file} (not a git repo when the override was made)` : `${file} changed since the override was made`
|
|
770
|
+
);
|
|
771
|
+
throw new Error(
|
|
772
|
+
`"${id}" is stale (${reasons.join("; ")}). Run \`variant sync ${id}\` first, or promote with --force.`
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
const conflicted = Object.keys(manifest.files).filter((key) => {
|
|
776
|
+
const overrideAbs = join3(vDir, key);
|
|
777
|
+
return existsSync3(overrideAbs) && readFileSync3(overrideAbs, "utf8").includes(CONFLICT_MARKER);
|
|
778
|
+
});
|
|
779
|
+
if (conflicted.length) {
|
|
780
|
+
throw new Error(
|
|
781
|
+
`"${id}" has unresolved conflict markers in ${conflicted.join(", ")}. Resolve them, or promote with --force.`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const siblings = overlayGroups(readVariants(opts)).find((g) => g.members.some((m) => m.id === id))?.members.filter((m) => m.id !== id) ?? [];
|
|
786
|
+
const files = [];
|
|
787
|
+
const landed = /* @__PURE__ */ new Map();
|
|
788
|
+
for (const key of Object.keys(manifest.files)) {
|
|
789
|
+
const overrideAbs = join3(vDir, key);
|
|
790
|
+
const destAbs = join3(root, key);
|
|
791
|
+
const before = existsSync3(destAbs) ? currentBlob(destAbs) : null;
|
|
792
|
+
mkdirSync3(dirname4(destAbs), { recursive: true });
|
|
793
|
+
writeFileSync3(destAbs, readFileSync3(overrideAbs));
|
|
794
|
+
landed.set(key, { before, after: blobOf(destAbs) });
|
|
795
|
+
files.push(key);
|
|
796
|
+
}
|
|
797
|
+
rmSync2(vDir, { recursive: true, force: true });
|
|
798
|
+
if (active.includes(id)) writeActive(opts, active.filter((a) => a !== id));
|
|
799
|
+
return { files, rebased: rebaseSiblings(opts, siblings, landed) };
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
function rebaseSiblings(opts, siblings, landed) {
|
|
803
|
+
const rebased = [];
|
|
804
|
+
for (const sibling of siblings) {
|
|
805
|
+
let moved = false;
|
|
806
|
+
for (const [key, entry] of Object.entries(sibling.files)) {
|
|
807
|
+
const change = landed.get(key);
|
|
808
|
+
if (!change?.after || !(entry.added || entry.base !== null && entry.base === change.before)) continue;
|
|
809
|
+
sibling.files[key] = { base: change.after };
|
|
810
|
+
moved = true;
|
|
811
|
+
}
|
|
812
|
+
if (!moved) continue;
|
|
813
|
+
writeVariant(opts, sibling);
|
|
814
|
+
rebased.push(sibling.id);
|
|
815
|
+
}
|
|
816
|
+
return rebased;
|
|
817
|
+
}
|
|
818
|
+
function dropVariant(opts, id) {
|
|
819
|
+
mutateVariants(opts, () => {
|
|
820
|
+
if (!isValidVariantId(id)) throw new Error(`Invalid variant id "${id}"`);
|
|
821
|
+
const active = readActive(opts);
|
|
822
|
+
rmSync2(variantDir(opts, id), { recursive: true, force: true });
|
|
823
|
+
if (active.includes(id)) writeActive(opts, active.filter((a) => a !== id));
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
var GIT_QUIET = ["ignore", "pipe", "ignore"];
|
|
827
|
+
function blobOf(absPath) {
|
|
828
|
+
try {
|
|
829
|
+
return execFileSync2("git", ["hash-object", "--no-filters", "-w", absPath], {
|
|
830
|
+
cwd: dirname4(absPath),
|
|
831
|
+
encoding: "utf8",
|
|
832
|
+
stdio: GIT_QUIET
|
|
833
|
+
}).trim();
|
|
834
|
+
} catch {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function currentBlob(absPath) {
|
|
839
|
+
try {
|
|
840
|
+
return execFileSync2("git", ["hash-object", "--no-filters", absPath], {
|
|
841
|
+
cwd: dirname4(absPath),
|
|
842
|
+
encoding: "utf8",
|
|
843
|
+
stdio: GIT_QUIET
|
|
844
|
+
}).trim();
|
|
845
|
+
} catch {
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
function hashFile(file, base) {
|
|
850
|
+
if (base === null) return null;
|
|
851
|
+
try {
|
|
852
|
+
const contents = readFileSync3(file);
|
|
853
|
+
return createHash2(base.length === 64 ? "sha256" : "sha1").update(`blob ${contents.length}\0`).update(contents).digest("hex");
|
|
854
|
+
} catch {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function catFile(cwd, hash) {
|
|
859
|
+
try {
|
|
860
|
+
return execFileSync2("git", ["cat-file", "-p", hash], { cwd, encoding: "utf8", stdio: GIT_QUIET });
|
|
861
|
+
} catch {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function mergeFile(overrideAbs, baseAbs, currentAbs) {
|
|
866
|
+
try {
|
|
867
|
+
const stdout = execFileSync2(
|
|
868
|
+
"git",
|
|
869
|
+
[
|
|
870
|
+
"merge-file",
|
|
871
|
+
"-p",
|
|
872
|
+
"-L",
|
|
873
|
+
"variant",
|
|
874
|
+
"-L",
|
|
875
|
+
"base",
|
|
876
|
+
"-L",
|
|
877
|
+
"default",
|
|
878
|
+
overrideAbs,
|
|
879
|
+
baseAbs,
|
|
880
|
+
currentAbs
|
|
881
|
+
],
|
|
882
|
+
{ encoding: "utf8", stdio: GIT_QUIET }
|
|
883
|
+
);
|
|
884
|
+
return { status: 0, stdout };
|
|
885
|
+
} catch (err) {
|
|
886
|
+
const e = err;
|
|
887
|
+
return { status: e.status ?? null, stdout: e.stdout ?? "" };
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function isConflictStatus(status) {
|
|
891
|
+
return status !== null && status >= 1 && status <= 127;
|
|
892
|
+
}
|
|
893
|
+
function recordBase(absPath, hash) {
|
|
894
|
+
return blobOf(absPath) ?? hash;
|
|
895
|
+
}
|
|
896
|
+
function syncVariant(opts, id) {
|
|
897
|
+
return mutateVariants(opts, () => {
|
|
898
|
+
const manifest = readVariant(opts, id);
|
|
899
|
+
if (!manifest) throw new Error(`No variant "${id}"`);
|
|
900
|
+
const root = resolve2(opts.root);
|
|
901
|
+
const vDir = variantDir(opts, id);
|
|
902
|
+
const merged = [];
|
|
903
|
+
const conflicts = [];
|
|
904
|
+
const unchanged = [];
|
|
905
|
+
let dirty = false;
|
|
906
|
+
for (const [file, entry] of Object.entries(manifest.files)) {
|
|
907
|
+
const currentAbs = join3(root, file);
|
|
908
|
+
const overrideAbs = join3(vDir, file);
|
|
909
|
+
if (entry.added) {
|
|
910
|
+
(existsSync3(currentAbs) ? conflicts : unchanged).push(file);
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const currentHash = currentBlob(currentAbs);
|
|
914
|
+
if (!isStale(entry.base, currentHash)) {
|
|
915
|
+
unchanged.push(file);
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
const baseContent = entry.base !== null ? catFile(dirname4(currentAbs), entry.base) : null;
|
|
919
|
+
if (baseContent === null || currentHash === null) {
|
|
920
|
+
conflicts.push(file);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
const baseTmp = join3(tmpdir(), `isotone-base-${randomUUID3()}`);
|
|
924
|
+
writeFileSync3(baseTmp, baseContent);
|
|
925
|
+
try {
|
|
926
|
+
const result = mergeFile(overrideAbs, baseTmp, currentAbs);
|
|
927
|
+
if (result.status === 0) {
|
|
928
|
+
writeFileSync3(overrideAbs, result.stdout);
|
|
929
|
+
entry.base = recordBase(currentAbs, currentHash);
|
|
930
|
+
dirty = true;
|
|
931
|
+
merged.push(file);
|
|
932
|
+
} else if (isConflictStatus(result.status)) {
|
|
933
|
+
writeFileSync3(overrideAbs, result.stdout);
|
|
934
|
+
entry.base = recordBase(currentAbs, currentHash);
|
|
935
|
+
dirty = true;
|
|
936
|
+
conflicts.push(file);
|
|
937
|
+
} else {
|
|
938
|
+
conflicts.push(file);
|
|
939
|
+
}
|
|
940
|
+
} finally {
|
|
941
|
+
rmSync2(baseTmp, { force: true });
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (dirty) writeVariant(opts, manifest);
|
|
945
|
+
return { merged, conflicts, unchanged };
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
function resolveOverride(opts, active, absSourcePath) {
|
|
949
|
+
if (active.length === 0) return null;
|
|
950
|
+
const root = resolve2(opts.root);
|
|
951
|
+
const relToRoot = relative3(root, resolve2(absSourcePath));
|
|
952
|
+
if (isEscaping(relToRoot)) return null;
|
|
953
|
+
const key = toPosixPath(relToRoot);
|
|
954
|
+
for (const id of active) {
|
|
955
|
+
const manifest = readVariant(opts, id);
|
|
956
|
+
if (!manifest || !Object.hasOwn(manifest.files, key)) continue;
|
|
957
|
+
const overrideAbs = join3(variantDir(opts, id), key);
|
|
958
|
+
if (existsSync3(overrideAbs)) return overrideAbs;
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
function overrideSourcePath(opts, absOverridePath) {
|
|
963
|
+
const rel = relative3(variantsDir(opts), resolve2(absOverridePath));
|
|
964
|
+
if (!rel || isEscaping(rel)) return null;
|
|
965
|
+
const [id, ...rest] = toPosixPath(rel).split("/");
|
|
966
|
+
if (!id || rest.length === 0) return null;
|
|
967
|
+
return { id, file: rest.join("/") };
|
|
968
|
+
}
|
|
969
|
+
function activeConflict(opts, ids) {
|
|
970
|
+
const owner = /* @__PURE__ */ new Map();
|
|
971
|
+
for (const id of ids) {
|
|
972
|
+
const manifest = readVariant(opts, id);
|
|
973
|
+
for (const file of Object.keys(manifest?.files ?? {})) {
|
|
974
|
+
const first = owner.get(file);
|
|
975
|
+
if (first !== void 0 && first !== id) return [first, id, file];
|
|
976
|
+
owner.set(file, id);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
function crossGroupOverlaps(opts, id) {
|
|
982
|
+
const variants = readVariants(opts);
|
|
983
|
+
const self = variants.find((v) => v.id === id);
|
|
984
|
+
if (!self) return [];
|
|
985
|
+
const siblings = overlayGroups(variants).find((g) => g.members.some((m) => m.id === id))?.members ?? [];
|
|
986
|
+
return variants.flatMap((other) => other.id === id || siblings.includes(other) ? [] : Object.keys(other.files).filter((file) => Object.hasOwn(self.files, file)).map((file) => ({ file, other: other.id })));
|
|
987
|
+
}
|
|
988
|
+
function readKnobs(opts) {
|
|
989
|
+
const file = resolve2(opts.root, KNOBS_FILE);
|
|
990
|
+
if (!existsSync3(file)) return [];
|
|
991
|
+
let parsed2;
|
|
992
|
+
try {
|
|
993
|
+
parsed2 = JSON.parse(readFileSync3(file, "utf8"));
|
|
994
|
+
} catch {
|
|
995
|
+
return [];
|
|
996
|
+
}
|
|
997
|
+
if (!Array.isArray(parsed2)) return [];
|
|
998
|
+
const seen = /* @__PURE__ */ new Set();
|
|
999
|
+
return parsed2.filter((k) => {
|
|
1000
|
+
if (!k || typeof k !== "object") return false;
|
|
1001
|
+
const a = k;
|
|
1002
|
+
if (typeof a.id !== "string" || !isValidVariantId(a.id) || typeof a.label !== "string") return false;
|
|
1003
|
+
if (a.route !== void 0 && typeof a.route !== "string" && (!Array.isArray(a.route) || !a.route.every((route) => typeof route === "string"))) return false;
|
|
1004
|
+
if (a.showWhen !== void 0 && (!a.showWhen || typeof a.showWhen.axis !== "string" || a.showWhen.equals !== void 0 && typeof a.showWhen.equals !== "string" || a.showWhen.not !== void 0 && typeof a.showWhen.not !== "string")) return false;
|
|
1005
|
+
if (a.section !== void 0 && typeof a.section !== "string") return false;
|
|
1006
|
+
if (a.kind === "range") {
|
|
1007
|
+
const { min, max, step, defaultValue } = a;
|
|
1008
|
+
if (typeof min !== "number" || typeof max !== "number" || typeof step !== "number" || typeof defaultValue !== "number") return false;
|
|
1009
|
+
return [min, max, step, defaultValue, max - min].every(Number.isFinite) && min < max && step > 0 && defaultValue >= min && defaultValue <= max && (a.unit === void 0 || typeof a.unit === "string");
|
|
1010
|
+
}
|
|
1011
|
+
if (a.kind === "color") {
|
|
1012
|
+
const c = a;
|
|
1013
|
+
const hex = (value) => typeof value === "string" && /^#(?:[a-f0-9]{3}|[a-f0-9]{6})$/i.test(value);
|
|
1014
|
+
return hex(c.defaultColor) && Array.isArray(c.swatches) && c.swatches.every((s) => s && typeof s.id === "string" && typeof s.label === "string" && hex(s.value));
|
|
1015
|
+
}
|
|
1016
|
+
if (a.kind === "select") {
|
|
1017
|
+
const s = a;
|
|
1018
|
+
return Array.isArray(s.options) && s.options.length > 0 && s.options.every((o) => o && typeof o.id === "string" && typeof o.label === "string") && s.options.some((o) => o.id === s.defaultOption);
|
|
1019
|
+
}
|
|
1020
|
+
return false;
|
|
1021
|
+
}).filter((axis) => {
|
|
1022
|
+
if (seen.has(axis.id)) return false;
|
|
1023
|
+
seen.add(axis.id);
|
|
1024
|
+
return true;
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
function readActive(opts) {
|
|
1028
|
+
const file = join3(resolve2(opts.root), ACTIVE_FILE);
|
|
1029
|
+
if (!existsSync3(file)) return [];
|
|
1030
|
+
let raw;
|
|
1031
|
+
try {
|
|
1032
|
+
raw = JSON.parse(readFileSync3(file, "utf8")).active;
|
|
1033
|
+
} catch {
|
|
1034
|
+
return [];
|
|
1035
|
+
}
|
|
1036
|
+
const ids = Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : [];
|
|
1037
|
+
const kept = [];
|
|
1038
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1039
|
+
const owner = /* @__PURE__ */ new Set();
|
|
1040
|
+
for (const id of ids) {
|
|
1041
|
+
if (typeof id !== "string" || seen.has(id)) continue;
|
|
1042
|
+
seen.add(id);
|
|
1043
|
+
const manifest = readVariant(opts, id);
|
|
1044
|
+
if (!manifest) continue;
|
|
1045
|
+
const files = Object.keys(manifest.files);
|
|
1046
|
+
if (files.some((f) => owner.has(f))) continue;
|
|
1047
|
+
for (const f of files) owner.add(f);
|
|
1048
|
+
kept.push(id);
|
|
1049
|
+
}
|
|
1050
|
+
return kept;
|
|
1051
|
+
}
|
|
1052
|
+
function writeActive(opts, ids) {
|
|
1053
|
+
const file = join3(resolve2(opts.root), ACTIVE_FILE);
|
|
1054
|
+
mkdirSync3(dirname4(file), { recursive: true });
|
|
1055
|
+
const active = Array.from(new Set(ids));
|
|
1056
|
+
writeJson(file, { active });
|
|
1057
|
+
ensureFileIgnored(file);
|
|
1058
|
+
}
|
|
1059
|
+
function variantsResponse(opts, req) {
|
|
1060
|
+
const method = req.method ?? "";
|
|
1061
|
+
const path = (req.url ?? "/").split("?")[0] || "/";
|
|
1062
|
+
const json = (status, body) => ({
|
|
1063
|
+
status,
|
|
1064
|
+
headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
|
|
1065
|
+
body
|
|
1066
|
+
});
|
|
1067
|
+
if (method === "GET" && path === "/") {
|
|
1068
|
+
return json(200, { variants: listVariants(opts), active: readActive(opts), knobs: readKnobs(opts) });
|
|
1069
|
+
}
|
|
1070
|
+
if (method === "POST" && path === "/active") {
|
|
1071
|
+
const body = req.body;
|
|
1072
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
1073
|
+
return json(400, { error: "body must be a JSON object" });
|
|
1074
|
+
}
|
|
1075
|
+
const raw = body.active;
|
|
1076
|
+
if (!Array.isArray(raw) || !raw.every((id) => typeof id === "string")) {
|
|
1077
|
+
return json(400, { error: "active must be an array of variant ids" });
|
|
1078
|
+
}
|
|
1079
|
+
const active = Array.from(new Set(raw));
|
|
1080
|
+
for (const id of active) {
|
|
1081
|
+
if (!isValidVariantId(id)) return json(400, { error: `invalid variant id "${id}"` });
|
|
1082
|
+
}
|
|
1083
|
+
for (const id of active) {
|
|
1084
|
+
if (!readVariant(opts, id)) return json(404, { error: `unknown variant "${id}"` });
|
|
1085
|
+
}
|
|
1086
|
+
const conflict = activeConflict(opts, active);
|
|
1087
|
+
if (conflict) {
|
|
1088
|
+
const [a, b, file] = conflict;
|
|
1089
|
+
const groups = overlayGroups(readVariants(opts));
|
|
1090
|
+
const named = (id) => {
|
|
1091
|
+
const label = groups.find((group) => group.members.some((member) => member.id === id))?.label;
|
|
1092
|
+
return label ? `"${id}" (${label})` : `"${id}"`;
|
|
1093
|
+
};
|
|
1094
|
+
return json(409, {
|
|
1095
|
+
error: `${named(a)} and ${named(b)} both override ${file}; only one can be active`,
|
|
1096
|
+
conflict
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
writeActive(opts, active);
|
|
1100
|
+
return json(200, { active });
|
|
1101
|
+
}
|
|
1102
|
+
return json(405, { error: "method not allowed" });
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
export {
|
|
1106
|
+
writeJson,
|
|
1107
|
+
withFileLock,
|
|
1108
|
+
parseVersion,
|
|
1109
|
+
compareVersions,
|
|
1110
|
+
upgradeCommand,
|
|
1111
|
+
notesProjectRoot,
|
|
1112
|
+
updateRequestOf,
|
|
1113
|
+
changelogUrl,
|
|
1114
|
+
PAYLOAD_MAX,
|
|
1115
|
+
readNotes,
|
|
1116
|
+
writeNotes,
|
|
1117
|
+
updateNotes,
|
|
1118
|
+
ensureFileIgnored,
|
|
1119
|
+
ensureDirIgnored,
|
|
1120
|
+
notesResponse,
|
|
1121
|
+
notesRevision,
|
|
1122
|
+
MAX_REPLY_LENGTH,
|
|
1123
|
+
noteFingerprint,
|
|
1124
|
+
replyView,
|
|
1125
|
+
NOTES_ENDPOINT,
|
|
1126
|
+
DEFAULT_VARIANTS_DIR,
|
|
1127
|
+
ACTIVE_FILE,
|
|
1128
|
+
KNOBS_FILE,
|
|
1129
|
+
variantsDir,
|
|
1130
|
+
variantDir,
|
|
1131
|
+
readVariant,
|
|
1132
|
+
readVariants,
|
|
1133
|
+
listVariants,
|
|
1134
|
+
createVariant,
|
|
1135
|
+
setVariantGroup,
|
|
1136
|
+
setVariantSection,
|
|
1137
|
+
addVariantFile,
|
|
1138
|
+
removeVariantFile,
|
|
1139
|
+
promoteVariant,
|
|
1140
|
+
dropVariant,
|
|
1141
|
+
syncVariant,
|
|
1142
|
+
resolveOverride,
|
|
1143
|
+
overrideSourcePath,
|
|
1144
|
+
crossGroupOverlaps,
|
|
1145
|
+
readKnobs,
|
|
1146
|
+
readActive,
|
|
1147
|
+
writeActive,
|
|
1148
|
+
variantsResponse
|
|
1149
|
+
};
|
|
1150
|
+
//# sourceMappingURL=chunk-L6O4HYGP.js.map
|