@anja.codes/vitakit 0.1.0 → 0.3.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/README.md +52 -131
- package/dist/check.mjs +20 -11
- package/dist/chunks/authoring.mjs +372 -0
- package/dist/{shared/vitakit.CypCF-4Q.mjs → chunks/bibliography.mjs} +74 -237
- package/dist/chunks/build.mjs +178 -0
- package/dist/chunks/documents.mjs +152 -0
- package/dist/chunks/draft.mjs +222 -0
- package/dist/chunks/eject.mjs +59 -0
- package/dist/chunks/emit.mjs +216 -0
- package/dist/chunks/eta.mjs +16 -0
- package/dist/chunks/fields.mjs +79 -0
- package/dist/chunks/inventory.mjs +201 -0
- package/dist/chunks/migrations.mjs +218 -0
- package/dist/chunks/plan.mjs +172 -0
- package/dist/chunks/prompt.mjs +33 -0
- package/dist/chunks/publication-lists.mjs +202 -0
- package/dist/chunks/schema-source.mjs +12 -0
- package/dist/chunks/schemas.mjs +109 -0
- package/dist/chunks/sections.mjs +109 -0
- package/dist/chunks/snippets.mjs +199 -0
- package/dist/chunks/validate.mjs +133 -0
- package/dist/chunks/vue.mjs +153 -0
- package/dist/chunks/yaml.mjs +62 -0
- package/dist/cli.d.mts +33 -40
- package/dist/cli.mjs +7 -224
- package/dist/index.d.mts +379 -49
- package/dist/index.mjs +15 -7
- package/dist/nuxt.mjs +48 -10
- package/dist/runtime/components/VitaPublications.vue +21 -22
- package/dist/runtime/components/VitaSection.vue +94 -38
- package/dist/runtime/composables/useVitakit.ts +14 -3
- package/dist/schemas/education.ts +4 -4
- package/dist/schemas/employment.ts +3 -3
- package/dist/schemas/publications.ts +10 -8
- package/dist/schemas/supervision.ts +3 -3
- package/dist/schemas/talks.ts +1 -1
- package/dist/schemas/teaching.ts +3 -3
- package/dist/shared/index.d.mts +60 -9
- package/dist/shared/index.mjs +3 -57
- package/dist/shared/{vitakit.D8WRg_Xx.d.mts → vitakit.BfR_mUHm.d.mts} +25 -12
- package/dist/shared/vitakit.CCTDg7pz.mjs +199 -0
- package/dist/shared/vitakit.CfB3hMHl.mjs +60 -0
- package/dist/shared/{vitakit.BlU-p01e.mjs → vitakit.CtChKO0I.mjs} +1 -1
- package/dist/shared/vitakit.D-DdJaDZ.mjs +20 -0
- package/dist/shared/vitakit.D10HyDhF.mjs +19 -0
- package/dist/shared/vitakit.DCK90zq4.mjs +67 -0
- package/dist/shared/vitakit.DQ6PmGoL.mjs +34 -0
- package/dist/shared/vitakit.DcQOdM4I.mjs +57 -0
- package/dist/shared/vitakit.DteA9vLy.mjs +135 -0
- package/dist/shared/vitakit.DvABWuUc.mjs +121 -0
- package/dist/shared/vitakit.DvvUYUTS.mjs +200 -0
- package/dist/shared/vitakit.TSMTC5Jl.mjs +6 -0
- package/dist/shared/vitakit.UfoXdL7U.mjs +22 -0
- package/dist/shared/vitakit.zwr1ssO5.mjs +1850 -0
- package/dist/tex/templates/_generic.eta +6 -4
- package/dist/tex/templates/education.eta +10 -5
- package/dist/tex/templates/employment.eta +12 -5
- package/dist/tex/templates/publications.eta +7 -3
- package/dist/tex/templates/supervision.eta +10 -2
- package/dist/tex/templates/talks.eta +5 -1
- package/dist/tex/templates/teaching.eta +6 -4
- package/dist/tex/vitakit-cv.cls +36 -10
- package/dist/tex.d.mts +80 -11
- package/dist/tex.mjs +27 -117
- package/package.json +10 -7
- package/src/bin/vitakit.mjs +3 -153
- package/dist/shared/vitakit.BRhjqpAt.mjs +0 -250
- package/dist/shared/vitakit.BxcnlFGP.mjs +0 -325
- package/src/bin/vitakit-build-cv.mjs +0 -50
- package/src/bin/vitakit-check.mjs +0 -57
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { v as visibility, t as text, a as richText, d as dateRange, u as url, b as absoluteUrl, l as linked, r as requiredLink } from '../shared/vitakit.DvABWuUc.mjs';
|
|
3
|
+
import '../shared/vitakit.DCK90zq4.mjs';
|
|
4
|
+
import 'node:url';
|
|
5
|
+
import '../shared/vitakit.D10HyDhF.mjs';
|
|
6
|
+
|
|
7
|
+
const FIELD_TYPE_NAMES = {
|
|
8
|
+
"text": "text",
|
|
9
|
+
"formatted-text": "formatted text",
|
|
10
|
+
"number": "number",
|
|
11
|
+
"yes-no": "yes/no",
|
|
12
|
+
"date": "date",
|
|
13
|
+
"web-address": "web address",
|
|
14
|
+
"link": "link",
|
|
15
|
+
"choice": "one of a fixed list",
|
|
16
|
+
"group": "group of fields",
|
|
17
|
+
"custom": "custom"
|
|
18
|
+
};
|
|
19
|
+
const PRIMITIVES = /* @__PURE__ */ new Map([
|
|
20
|
+
[text, "text"],
|
|
21
|
+
[richText, "formatted-text"],
|
|
22
|
+
[dateRange, "date"],
|
|
23
|
+
[url, "web-address"],
|
|
24
|
+
[absoluteUrl, "web-address"],
|
|
25
|
+
[linked, "link"],
|
|
26
|
+
[requiredLink, "link"]
|
|
27
|
+
]);
|
|
28
|
+
function describeField(path, schema) {
|
|
29
|
+
let current = schema;
|
|
30
|
+
let optional = false;
|
|
31
|
+
let several = false;
|
|
32
|
+
let fallback;
|
|
33
|
+
let hasDefault = false;
|
|
34
|
+
for (; ; ) {
|
|
35
|
+
if (PRIMITIVES.has(current)) break;
|
|
36
|
+
if (current instanceof z.ZodOptional || current instanceof z.ZodNullable) {
|
|
37
|
+
optional = true;
|
|
38
|
+
current = current.unwrap();
|
|
39
|
+
} else if (current instanceof z.ZodDefault) {
|
|
40
|
+
optional = true;
|
|
41
|
+
hasDefault = true;
|
|
42
|
+
fallback = current.def.defaultValue;
|
|
43
|
+
current = current.unwrap();
|
|
44
|
+
} else if (current instanceof z.ZodArray && !several) {
|
|
45
|
+
several = true;
|
|
46
|
+
current = current.element;
|
|
47
|
+
} else {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const description = { path, type: "custom", optional, several };
|
|
52
|
+
if (hasDefault) description.default = fallback;
|
|
53
|
+
const primitive = PRIMITIVES.get(current);
|
|
54
|
+
if (primitive) description.type = primitive;
|
|
55
|
+
else if (current instanceof z.ZodString) description.type = "text";
|
|
56
|
+
else if (current instanceof z.ZodNumber) description.type = "number";
|
|
57
|
+
else if (current instanceof z.ZodBoolean) description.type = "yes-no";
|
|
58
|
+
else if (current instanceof z.ZodEnum) {
|
|
59
|
+
description.type = "choice";
|
|
60
|
+
description.choices = current.options.map(String);
|
|
61
|
+
} else if (current instanceof z.ZodObject) {
|
|
62
|
+
description.type = "group";
|
|
63
|
+
description.fields = Object.entries(current.shape).map(([name, field]) => describeField(`${path}.${name}`, field));
|
|
64
|
+
}
|
|
65
|
+
return description;
|
|
66
|
+
}
|
|
67
|
+
function describeSection(schema) {
|
|
68
|
+
if (!(schema instanceof z.ZodArray) || !(schema.element instanceof z.ZodObject)) return void 0;
|
|
69
|
+
return Object.entries(schema.element.shape).filter(([name, field]) => !(name === "visibility" && field === visibility)).map(([name, field]) => describeField(name, field));
|
|
70
|
+
}
|
|
71
|
+
function fieldSummary(field) {
|
|
72
|
+
const type = field.type === "choice" && field.choices ? `one of ${field.choices.join(", ")}` : FIELD_TYPE_NAMES[field.type];
|
|
73
|
+
return [
|
|
74
|
+
field.several ? `list of ${type}` : type,
|
|
75
|
+
field.optional ? field.default !== void 0 ? `default ${JSON.stringify(field.default)}` : "optional" : "required"
|
|
76
|
+
].join(", ");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { FIELD_TYPE_NAMES, describeField, describeSection, fieldSummary };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { readFile, stat, readdir } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join, relative, sep } from 'node:path';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
import { parse } from 'yaml';
|
|
6
|
+
import { l as loadContent } from '../shared/vitakit.DteA9vLy.mjs';
|
|
7
|
+
import { describeSection } from './fields.mjs';
|
|
8
|
+
import { findSectionFiles, resolveSectionSchema } from './sections.mjs';
|
|
9
|
+
import { schemas } from './schemas.mjs';
|
|
10
|
+
import { findDocuments, documentSectionProblems } from './documents.mjs';
|
|
11
|
+
import { a as pageRoute, d as documentDirective, p as publicationPlacementProblems, b as findWebPlacements } from '../shared/vitakit.DvvUYUTS.mjs';
|
|
12
|
+
import { m as messageOf, w as whereOf } from '../shared/vitakit.DCK90zq4.mjs';
|
|
13
|
+
import './bibliography.mjs';
|
|
14
|
+
import '../shared/vitakit.DQ6PmGoL.mjs';
|
|
15
|
+
import 'node:url';
|
|
16
|
+
import '@behackl/citation-js-extras';
|
|
17
|
+
import 'unified';
|
|
18
|
+
import 'rehype-parse';
|
|
19
|
+
import 'rehype-sanitize';
|
|
20
|
+
import 'rehype-stringify';
|
|
21
|
+
import 'remark-parse';
|
|
22
|
+
import 'remark-gfm';
|
|
23
|
+
import 'remark-math';
|
|
24
|
+
import 'remark-rehype';
|
|
25
|
+
import 'rehype-katex';
|
|
26
|
+
import 'rehype-raw';
|
|
27
|
+
import 'unist-util-visit';
|
|
28
|
+
import 'zod';
|
|
29
|
+
import './publication-lists.mjs';
|
|
30
|
+
import '../shared/vitakit.D10HyDhF.mjs';
|
|
31
|
+
import '../shared/vitakit.DvABWuUc.mjs';
|
|
32
|
+
import '../shared/vitakit.TSMTC5Jl.mjs';
|
|
33
|
+
import '../shared/vitakit.D-DdJaDZ.mjs';
|
|
34
|
+
|
|
35
|
+
const posix = (path) => path.split(sep).join("/");
|
|
36
|
+
async function files(dir, filter) {
|
|
37
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
38
|
+
const found = [];
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const path = join(dir, entry.name);
|
|
41
|
+
if (entry.isDirectory()) found.push(...await files(path, filter));
|
|
42
|
+
else if (filter(entry.name)) found.push(path);
|
|
43
|
+
}
|
|
44
|
+
return found.sort();
|
|
45
|
+
}
|
|
46
|
+
async function newest(paths) {
|
|
47
|
+
let latest = 0;
|
|
48
|
+
for (const path of paths) {
|
|
49
|
+
const info = await stat(path).catch(() => void 0);
|
|
50
|
+
if (info && info.mtimeMs > latest) latest = info.mtimeMs;
|
|
51
|
+
}
|
|
52
|
+
return latest;
|
|
53
|
+
}
|
|
54
|
+
function sharedSchema(source) {
|
|
55
|
+
return /export\s*\{\s*default\s*\}\s*from\s*['"]\.\/([a-z0-9-]+)(?:\.ts)?['"]/.exec(source)?.[1] ?? /^export\s+default\s+schemas\.([a-z]\w*)\s*;?\s*$/m.exec(source)?.[1];
|
|
56
|
+
}
|
|
57
|
+
function belongsTo(problem, file) {
|
|
58
|
+
return problem.where === file.where || problem.where === `schemas/${file.id}.ts`;
|
|
59
|
+
}
|
|
60
|
+
async function inventory(rootDir, options = {}) {
|
|
61
|
+
const content = await loadContent(rootDir, { strict: false, maxVisibility: "private" });
|
|
62
|
+
const problems = [...content.issues];
|
|
63
|
+
const { files: sectionFiles } = await findSectionFiles(rootDir).catch(() => ({ files: [] }));
|
|
64
|
+
const sections = [];
|
|
65
|
+
const schemasForDocuments = [];
|
|
66
|
+
for (const file of sectionFiles) {
|
|
67
|
+
const local = join(rootDir, "schemas", `${file.id}.ts`);
|
|
68
|
+
const builtIn = Object.hasOwn(schemas, file.id);
|
|
69
|
+
const section = {
|
|
70
|
+
id: file.id,
|
|
71
|
+
where: file.where,
|
|
72
|
+
schema: { origin: existsSync(local) ? "local" : builtIn ? "built-in" : "none" },
|
|
73
|
+
problems: problems.filter((problem) => belongsTo(problem, file))
|
|
74
|
+
};
|
|
75
|
+
if (section.schema.origin === "local") {
|
|
76
|
+
section.schema.file = `schemas/${file.id}.ts`;
|
|
77
|
+
const source = await readFile(local, "utf8");
|
|
78
|
+
const shared = sharedSchema(source);
|
|
79
|
+
if (shared) section.schema.sharedWith = shared;
|
|
80
|
+
if (builtIn && /\bextendSection\s*\(\s*schemas\.[\w$]+/.test(source)) section.schema.extendsBuiltIn = true;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const raw = parse(await readFile(file.file, "utf8"));
|
|
84
|
+
if (Array.isArray(raw)) section.entries = raw.length;
|
|
85
|
+
else if (raw === null) section.entries = 0;
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
if (section.schema.origin !== "none") {
|
|
89
|
+
try {
|
|
90
|
+
const source = await resolveSectionSchema(rootDir, file);
|
|
91
|
+
schemasForDocuments.push({ id: file.id, schema: source.schema });
|
|
92
|
+
const fields = describeSection(source.schema);
|
|
93
|
+
if (fields) section.fields = fields;
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!section.problems.length) {
|
|
98
|
+
const counts = { public: 0, cv: 0, private: 0 };
|
|
99
|
+
for (const entry of content.sections[file.id] ?? []) counts[entry.visibility]++;
|
|
100
|
+
section.counts = counts;
|
|
101
|
+
}
|
|
102
|
+
sections.push(section);
|
|
103
|
+
}
|
|
104
|
+
const menu = new Set(content.profile.nav.map((item) => item.url));
|
|
105
|
+
const pages = /* @__PURE__ */ new Map();
|
|
106
|
+
const page = (route) => {
|
|
107
|
+
if (!pages.has(route)) pages.set(route, { route, inMenu: menu.has(route) });
|
|
108
|
+
return pages.get(route);
|
|
109
|
+
};
|
|
110
|
+
const pagesDir = join(rootDir, "content", "pages");
|
|
111
|
+
for (const path of await files(pagesDir, (name) => name.endsWith(".md"))) {
|
|
112
|
+
const slug = relative(pagesDir, path).replace(/\.md$/, "");
|
|
113
|
+
let route = slug === "index" ? "/" : `/${slug}/`;
|
|
114
|
+
let title;
|
|
115
|
+
try {
|
|
116
|
+
const { data } = matter(await readFile(path, "utf8"));
|
|
117
|
+
if (typeof data.route === "string") route = data.route;
|
|
118
|
+
if (typeof data.title === "string") title = data.title;
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
Object.assign(page(route), { markdown: posix(relative(rootDir, path)), ...title ? { title } : {} });
|
|
122
|
+
}
|
|
123
|
+
let catchAll;
|
|
124
|
+
const vuePagesDir = join(rootDir, "app", "pages");
|
|
125
|
+
for (const path of await files(vuePagesDir, (name) => name.endsWith(".vue"))) {
|
|
126
|
+
const below = relative(vuePagesDir, path);
|
|
127
|
+
if (below.includes("[")) {
|
|
128
|
+
if (/\[\.\.\.[^\]]+\]\.vue$/.test(below) && !below.includes(sep)) catchAll = posix(relative(rootDir, path));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
page(pageRoute(below)).vue = posix(relative(rootDir, path));
|
|
132
|
+
}
|
|
133
|
+
const bib = await readFile(join(rootDir, "content", "publications.bib"), "utf8").catch(() => void 0);
|
|
134
|
+
const publications = bib === void 0 ? void 0 : {
|
|
135
|
+
file: "content/publications.bib",
|
|
136
|
+
// Counted, not parsed: the lists were parsed above, and may be broken.
|
|
137
|
+
entries: bib.match(/^\s*@(?!comment\b|string\b|preamble\b)\w+\s*[{(]/gim)?.length ?? 0,
|
|
138
|
+
counts: Object.fromEntries(Object.entries(content.publications).map(([name, list]) => [name, list.count]))
|
|
139
|
+
};
|
|
140
|
+
const cvDir = join(rootDir, "cv-tex");
|
|
141
|
+
const { documents: found, problems: documentProblems } = await findDocuments(cvDir);
|
|
142
|
+
problems.push(...documentProblems);
|
|
143
|
+
const known = /* @__PURE__ */ new Set([...sections.map((section) => section.id), "profile"]);
|
|
144
|
+
const lists = Object.keys(content.publications);
|
|
145
|
+
const documents = [];
|
|
146
|
+
for (const document of found) {
|
|
147
|
+
const tex = `cv-tex/${document.name}.tex`;
|
|
148
|
+
const source = await readFile(document.texFile, "utf8");
|
|
149
|
+
const pdfPath = join(cvDir, `${document.name}.pdf`);
|
|
150
|
+
const pdf = await stat(pdfPath).catch(() => void 0);
|
|
151
|
+
const own = documentSectionProblems(document, schemasForDocuments);
|
|
152
|
+
const directive = documentDirective(source);
|
|
153
|
+
if (!directive) own.push({ where: tex, message: `has no \\vitadocument{${document.name}}` });
|
|
154
|
+
else if (directive.name !== document.name) {
|
|
155
|
+
own.push({ where: `${tex}:${directive.line}`, message: `\\vitadocument{${directive.name}} does not match the file name; expected \\vitadocument{${document.name}}` });
|
|
156
|
+
}
|
|
157
|
+
const { placements } = document;
|
|
158
|
+
for (const placement of placements) {
|
|
159
|
+
if (!known.has(placement.section) && placement.section !== "publications") {
|
|
160
|
+
own.push({ where: `${tex}:${placement.line}`, message: `no section called "${placement.section}"` });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
own.push(...publicationPlacementProblems([], [...placements, ...document.publications], lists));
|
|
164
|
+
problems.push(...own);
|
|
165
|
+
documents.push({
|
|
166
|
+
name: document.name,
|
|
167
|
+
tex,
|
|
168
|
+
...existsSync(join(cvDir, `${document.name}.yml`)) ? { yml: `cv-tex/${document.name}.yml` } : {},
|
|
169
|
+
options: document.options,
|
|
170
|
+
placements,
|
|
171
|
+
publications: document.publications,
|
|
172
|
+
...pdf ? { pdf: { file: `cv-tex/${document.name}.pdf`, mtimeMs: pdf.mtimeMs } } : {},
|
|
173
|
+
problems: [...documentProblems.filter((problem) => problem.where === `cv-tex/${document.name}.yml`), ...own]
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const sourcesMtimeMs = await newest([
|
|
177
|
+
...await files(join(rootDir, "content"), () => true),
|
|
178
|
+
...await files(join(rootDir, "schemas"), () => true),
|
|
179
|
+
...await files(cvDir, (name) => /\.(tex|yml|yaml|eta|cls)$/.test(name)).then((list) => list.filter((path) => !relative(cvDir, path).startsWith("generated")))
|
|
180
|
+
]);
|
|
181
|
+
const none = () => ({ placements: [], publications: [], dynamic: [], components: /* @__PURE__ */ new Map() });
|
|
182
|
+
const web = options.web === false ? none() : await findWebPlacements(rootDir).catch((error) => {
|
|
183
|
+
problems.push({ where: whereOf(error, "app/"), message: messageOf(error) });
|
|
184
|
+
return none();
|
|
185
|
+
});
|
|
186
|
+
problems.push(...publicationPlacementProblems(web.publications, [], lists));
|
|
187
|
+
return {
|
|
188
|
+
root: rootDir,
|
|
189
|
+
sections,
|
|
190
|
+
pages: [...pages.values()].sort((a, b) => a.route.localeCompare(b.route)),
|
|
191
|
+
...catchAll ? { catchAll } : {},
|
|
192
|
+
...publications ? { publications } : {},
|
|
193
|
+
documents,
|
|
194
|
+
web,
|
|
195
|
+
sourcesMtimeMs,
|
|
196
|
+
problems,
|
|
197
|
+
content
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { inventory };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, sep } from 'node:path';
|
|
3
|
+
import { parseExpression } from '@babel/parser';
|
|
4
|
+
import { parse, parseDocument, isMap, isScalar } from 'yaml';
|
|
5
|
+
import { l as loadPublicationsSchema, d as defaultPublications } from './bibliography.mjs';
|
|
6
|
+
import { i as importProjectModule } from '../shared/vitakit.DCK90zq4.mjs';
|
|
7
|
+
import { addPublicationList, publicationListYaml } from './yaml.mjs';
|
|
8
|
+
import { r as renamePublicationsSettings } from '../shared/vitakit.CCTDg7pz.mjs';
|
|
9
|
+
import { publicationsElements } from './vue.mjs';
|
|
10
|
+
import { f as findPdfPlacements } from '../shared/vitakit.DvvUYUTS.mjs';
|
|
11
|
+
import { LISTS_FILE, LISTS_HEADER } from './publication-lists.mjs';
|
|
12
|
+
import 'node:fs';
|
|
13
|
+
import '../shared/vitakit.DQ6PmGoL.mjs';
|
|
14
|
+
import 'node:url';
|
|
15
|
+
import '@behackl/citation-js-extras';
|
|
16
|
+
import 'unified';
|
|
17
|
+
import 'rehype-parse';
|
|
18
|
+
import 'rehype-sanitize';
|
|
19
|
+
import 'rehype-stringify';
|
|
20
|
+
import 'remark-parse';
|
|
21
|
+
import 'remark-gfm';
|
|
22
|
+
import 'remark-math';
|
|
23
|
+
import 'remark-rehype';
|
|
24
|
+
import 'rehype-katex';
|
|
25
|
+
import 'rehype-raw';
|
|
26
|
+
import 'unist-util-visit';
|
|
27
|
+
import 'zod';
|
|
28
|
+
import '../shared/vitakit.UfoXdL7U.mjs';
|
|
29
|
+
import '@vue/compiler-sfc';
|
|
30
|
+
|
|
31
|
+
class ListPlan {
|
|
32
|
+
constructor(existing) {
|
|
33
|
+
this.existing = existing;
|
|
34
|
+
}
|
|
35
|
+
added = /* @__PURE__ */ new Map();
|
|
36
|
+
/** The name of a list with these settings: an existing one, or a new one. */
|
|
37
|
+
name(wanted, settings) {
|
|
38
|
+
const same = (other) => JSON.stringify(other ?? {}) === JSON.stringify(settings);
|
|
39
|
+
for (const [name2, other] of [...Object.entries(this.existing), ...this.added]) if (same(other)) return name2;
|
|
40
|
+
let name = wanted;
|
|
41
|
+
for (let index = 2; Object.hasOwn(this.existing, name) || this.added.has(name); index++) name = `${wanted}-${index}`;
|
|
42
|
+
this.added.set(name, settings);
|
|
43
|
+
return name;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function statusList(statuses, grouped = true) {
|
|
47
|
+
return {
|
|
48
|
+
wanted: `${statuses?.join("-") ?? "all"}${grouped ? "" : "-ungrouped"}`,
|
|
49
|
+
settings: {
|
|
50
|
+
...statuses ? { where: { status: statuses.length === 1 ? statuses[0] : statuses } } : {},
|
|
51
|
+
...grouped ? { groupBy: "status" } : {}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function literalStrings(expression) {
|
|
56
|
+
try {
|
|
57
|
+
const node = parseExpression(expression);
|
|
58
|
+
if (node.type !== "ArrayExpression") return void 0;
|
|
59
|
+
const values = node.elements.map((element) => element?.type === "StringLiteral" ? element.value : void 0);
|
|
60
|
+
return values.every((value) => value !== void 0) ? values : void 0;
|
|
61
|
+
} catch {
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function legacyList(element, statuses) {
|
|
66
|
+
const groupsProp = element.props.find((prop) => prop.name === "groups");
|
|
67
|
+
const headingsProp = element.props.find((prop) => prop.name === "headings");
|
|
68
|
+
const groups = groupsProp?.expression !== void 0 ? literalStrings(groupsProp.expression) : void 0;
|
|
69
|
+
if (groupsProp && !groups) return { reason: "its groups are not a literal list such as ['preprint']" };
|
|
70
|
+
const headings = !headingsProp || headingsProp.expression === "true" || headingsProp.expression === void 0 && headingsProp.value === void 0;
|
|
71
|
+
if (headingsProp && !headings && headingsProp.expression !== "false") return { reason: "its headings are not literally true or false" };
|
|
72
|
+
const list = statusList(groups, headings);
|
|
73
|
+
const reordered = headings && groups && groups.join() !== statuses.filter((status) => groups.includes(status)).join();
|
|
74
|
+
const merged = !headings && (groups?.length ?? 2) > 1;
|
|
75
|
+
const note = reordered ? "its groups now appear in the order of the statuses in schemas/publications.ts" : merged ? "it is now one list, newest first, rather than one status after the other" : void 0;
|
|
76
|
+
return { ...list, ...note ? { note } : {} };
|
|
77
|
+
}
|
|
78
|
+
async function statusOrder(root) {
|
|
79
|
+
const ids = (statuses) => statuses.map((status) => status.id);
|
|
80
|
+
try {
|
|
81
|
+
return ids((await loadPublicationsSchema(root)).statuses);
|
|
82
|
+
} catch {
|
|
83
|
+
const path = join(root, "schemas", "publications.ts");
|
|
84
|
+
const old = await stat(path).then((info) => importProjectModule(path, info.mtimeMs, "schemas/publications.ts")).then((module) => module.default?.groups, () => void 0);
|
|
85
|
+
return Array.isArray(old) ? ids(old) : ids(defaultPublications.statuses);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function vueFiles(root, dir = join(root, "app")) {
|
|
89
|
+
const files = [];
|
|
90
|
+
for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
91
|
+
const path = join(dir, entry.name);
|
|
92
|
+
if (entry.isDirectory()) files.push(...await vueFiles(root, path));
|
|
93
|
+
else if (entry.name.endsWith(".vue")) files.push(relative(root, path).split(sep).join("/"));
|
|
94
|
+
}
|
|
95
|
+
return files.sort();
|
|
96
|
+
}
|
|
97
|
+
async function migrateSchema(ws, notes) {
|
|
98
|
+
const path = "schemas/publications.ts";
|
|
99
|
+
const source = await ws.read(path);
|
|
100
|
+
if (source === void 0 || !/\b(groups|defaultGroup)\s*:/.test(source)) return;
|
|
101
|
+
const renamed = renamePublicationsSettings(source, { groups: "statuses", defaultGroup: "defaultStatus" });
|
|
102
|
+
if (!renamed.ok) notes.after.push(`By hand: in ${path}, rename groups to statuses and defaultGroup to defaultStatus (${renamed.reason}).`);
|
|
103
|
+
else if (renamed.value !== source) await ws.edit(path, renamed.value, "groups is now statuses, defaultGroup defaultStatus");
|
|
104
|
+
}
|
|
105
|
+
async function migratePages(root, ws, lists, notes) {
|
|
106
|
+
const statuses = await statusOrder(root);
|
|
107
|
+
for (const path of await vueFiles(root)) {
|
|
108
|
+
const source = await ws.read(path);
|
|
109
|
+
if (/(^|\/)VitaPublications\.vue$/.test(path)) {
|
|
110
|
+
notes.after.push(`By hand: ${path} is your copy of VitaPublications. Its props are now list and title, and it reads usePublicationList(list); \`vitakit eject VitaPublications\` shows vitakit's.`);
|
|
111
|
+
}
|
|
112
|
+
if (/\busePublications\s*\(/.test(source)) {
|
|
113
|
+
notes.after.push(`By hand: ${path} calls usePublications(); use usePublicationList('<list>') with a list from ${LISTS_FILE}.`);
|
|
114
|
+
}
|
|
115
|
+
const found = publicationsElements(source);
|
|
116
|
+
if (!found.ok) {
|
|
117
|
+
if (source.includes("VitaPublications")) notes.after.push(`By hand: ${path} ${found.reason}; check its VitaPublications.`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const edits = [];
|
|
121
|
+
const shown = [];
|
|
122
|
+
for (const element of found.value) {
|
|
123
|
+
if (element.props.some((prop) => prop.name === "list")) continue;
|
|
124
|
+
const old = element.props.filter((prop) => prop.name === "groups" || prop.name === "headings");
|
|
125
|
+
const legacy = old.length ? legacyList(element, statuses) : statusList();
|
|
126
|
+
if ("reason" in legacy) {
|
|
127
|
+
notes.after.push(`By hand: ${path}:${element.line}: ${legacy.reason}. Declare the list in ${LISTS_FILE} and write <VitaPublications list="<name>" />.`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const name = lists.name(legacy.wanted, legacy.settings);
|
|
131
|
+
for (const prop of old) {
|
|
132
|
+
let start = prop.start;
|
|
133
|
+
while (start > 0 && /\s/.test(source[start - 1])) start--;
|
|
134
|
+
edits.push({ start, end: prop.end, text: "" });
|
|
135
|
+
}
|
|
136
|
+
const afterTag = element.start + 1 + element.tag.length;
|
|
137
|
+
edits.push({ start: afterTag, end: afterTag, text: ` list="${name}"` });
|
|
138
|
+
shown.push(name);
|
|
139
|
+
if ("note" in legacy && legacy.note) notes.before.push(`${path}:${element.line} shows the list ${name}: ${legacy.note}.`);
|
|
140
|
+
}
|
|
141
|
+
if (!edits.length) continue;
|
|
142
|
+
let result = source;
|
|
143
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
144
|
+
await ws.edit(path, result, `VitaPublications shows ${[...new Set(shown)].join(", ")}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function migrateDocuments(root, ws, lists, notes) {
|
|
148
|
+
const files = (await readdir(join(root, "cv-tex")).catch(() => [])).filter((name) => name.endsWith(".tex")).sort();
|
|
149
|
+
for (const file of files) {
|
|
150
|
+
const tex = `cv-tex/${file}`;
|
|
151
|
+
const yml = tex.replace(/\.tex$/, ".yml");
|
|
152
|
+
let list = statusList();
|
|
153
|
+
const options = await ws.read(yml);
|
|
154
|
+
const document = options === void 0 ? void 0 : parseDocument(options);
|
|
155
|
+
const pair = document && isMap(document.contents) ? document.contents.items.find((item) => isScalar(item.key) && item.key.value === "publications") : void 0;
|
|
156
|
+
if (options !== void 0 && pair) {
|
|
157
|
+
const groups = document.toJS().publications?.groups;
|
|
158
|
+
if (Array.isArray(groups) && groups.every((group) => typeof group === "string")) list = statusList(groups);
|
|
159
|
+
const start = pair.key.range?.[0];
|
|
160
|
+
const end = pair.value?.range?.[1];
|
|
161
|
+
if (start === void 0 || end === void 0) {
|
|
162
|
+
notes.after.push(`By hand: remove publications from ${yml}; which publications the PDF shows is now a list.`);
|
|
163
|
+
} else {
|
|
164
|
+
const lines2 = options.split("\n");
|
|
165
|
+
const lineOf = (offset) => options.slice(0, offset).split("\n").length;
|
|
166
|
+
lines2.splice(lineOf(start) - 1, lineOf(Math.max(start, end - 1)) - lineOf(start) + 1);
|
|
167
|
+
await ws.edit(yml, lines2.join("\n"), `publications are now a list in ${LISTS_FILE}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const source = await ws.read(tex);
|
|
171
|
+
const inputs = findPdfPlacements("", tex, source).filter((placement) => placement.macro === "vitainput" && placement.section === "publications");
|
|
172
|
+
if (!inputs.length) continue;
|
|
173
|
+
const name = lists.name(list.wanted, list.settings);
|
|
174
|
+
const lines = source.split("\n");
|
|
175
|
+
for (const input of [...inputs].reverse()) {
|
|
176
|
+
const index = input.line - 1;
|
|
177
|
+
const heading = index > 0 ? /^\s*\\section\{([^{}]*)\}\s*$/.exec(lines[index - 1])?.[1] : void 0;
|
|
178
|
+
lines[index] = lines[index].replace("\\vitainput{publications}", `\\vitapublications${heading !== void 0 ? `[${heading}]` : ""}{${name}}`);
|
|
179
|
+
if (heading !== void 0) lines.splice(index - 1, 1);
|
|
180
|
+
}
|
|
181
|
+
await ws.edit(tex, lines.join("\n"), `shows the publication list ${name}`);
|
|
182
|
+
notes.before.push(`${tex} shows the list ${name}: in the PDF too, publications now come in the list's order, as on the website (biblatex sorted them by year and author before), and each group is numbered from 1.`);
|
|
183
|
+
}
|
|
184
|
+
if (await ws.read("cv-tex/templates/publications.eta") !== void 0) {
|
|
185
|
+
notes.after.push("By hand: cv-tex/templates/publications.eta is your copy of the PDF template. vitakit's now cites a list in its order, with `<%~ it.bib.cite() %>` instead of `\\nocite{*}`, inside `\\begin{refcontext}[sorting=none]`.");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function migratePublicationLists(root, ws, notes) {
|
|
189
|
+
await migrateSchema(ws, notes);
|
|
190
|
+
const source = await ws.read(LISTS_FILE);
|
|
191
|
+
let existing;
|
|
192
|
+
try {
|
|
193
|
+
existing = source === void 0 ? {} : parse(source) ?? {};
|
|
194
|
+
} catch {
|
|
195
|
+
notes.after.push(`By hand: ${LISTS_FILE} cannot be read; fix it, then run \`vitakit migrate\` again.`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const lists = new ListPlan(typeof existing === "object" && !Array.isArray(existing) ? existing : {});
|
|
199
|
+
await migratePages(root, ws, lists, notes);
|
|
200
|
+
await migrateDocuments(root, ws, lists, notes);
|
|
201
|
+
if (!lists.added.size) return;
|
|
202
|
+
let lines = source ?? LISTS_HEADER;
|
|
203
|
+
for (const [name, settings] of lists.added) {
|
|
204
|
+
const added = addPublicationList(lines, name, settings);
|
|
205
|
+
if (!added.ok) {
|
|
206
|
+
notes.after.push(`By hand: add to ${LISTS_FILE} (${added.reason}):
|
|
207
|
+
${[...lists.added].map(([other, value]) => publicationListYaml(other, value)).join("\n")}`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
lines = added.value;
|
|
211
|
+
}
|
|
212
|
+
notes.after.push(`The new lists are named after what they show. To rename one, change its name in ${LISTS_FILE} and where it is shown.`);
|
|
213
|
+
const reason = `declares ${[...lists.added.keys()].join(", ")}`;
|
|
214
|
+
if (source === void 0) ws.create(LISTS_FILE, lines, reason, { show: true });
|
|
215
|
+
else await ws.edit(LISTS_FILE, lines, reason);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export { migratePublicationLists };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, mkdir, writeFile, rename } from 'node:fs/promises';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { C as CommandError } from '../shared/vitakit.zwr1ssO5.mjs';
|
|
5
|
+
import '../shared/vitakit.DQ6PmGoL.mjs';
|
|
6
|
+
import 'node:url';
|
|
7
|
+
import 'node:process';
|
|
8
|
+
import 'node:util';
|
|
9
|
+
import 'node:readline/promises';
|
|
10
|
+
|
|
11
|
+
const writes = (plan) => plan.items.filter((item) => item.kind === "create" || item.kind === "replace" || item.kind === "edit");
|
|
12
|
+
function diff(before, after, context = 2) {
|
|
13
|
+
const a = before.split("\n");
|
|
14
|
+
const b = after.split("\n");
|
|
15
|
+
let start = 0;
|
|
16
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start++;
|
|
17
|
+
let endA = a.length;
|
|
18
|
+
let endB = b.length;
|
|
19
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
|
|
20
|
+
endA--;
|
|
21
|
+
endB--;
|
|
22
|
+
}
|
|
23
|
+
const midA = a.slice(start, endA);
|
|
24
|
+
const midB = b.slice(start, endB);
|
|
25
|
+
const ops = a.slice(0, start).map((line) => ({ kind: " ", line }));
|
|
26
|
+
if (midA.length * midB.length > 1e6) {
|
|
27
|
+
ops.push(...midA.map((line) => ({ kind: "-", line })), ...midB.map((line) => ({ kind: "+", line })));
|
|
28
|
+
} else {
|
|
29
|
+
const lengths = Array.from({ length: midA.length + 1 }, () => new Array(midB.length + 1).fill(0));
|
|
30
|
+
for (let i2 = midA.length - 1; i2 >= 0; i2--) {
|
|
31
|
+
for (let j2 = midB.length - 1; j2 >= 0; j2--) {
|
|
32
|
+
lengths[i2][j2] = midA[i2] === midB[j2] ? lengths[i2 + 1][j2 + 1] + 1 : Math.max(lengths[i2 + 1][j2], lengths[i2][j2 + 1]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
let i = 0;
|
|
36
|
+
let j = 0;
|
|
37
|
+
while (i < midA.length || j < midB.length) {
|
|
38
|
+
if (i < midA.length && j < midB.length && midA[i] === midB[j]) {
|
|
39
|
+
ops.push({ kind: " ", line: midA[i] });
|
|
40
|
+
i++;
|
|
41
|
+
j++;
|
|
42
|
+
} else if (i < midA.length && (j === midB.length || lengths[i + 1][j] >= lengths[i][j + 1])) {
|
|
43
|
+
ops.push({ kind: "-", line: midA[i++] });
|
|
44
|
+
} else {
|
|
45
|
+
ops.push({ kind: "+", line: midB[j++] });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
ops.push(...a.slice(endA).map((line) => ({ kind: " ", line })));
|
|
50
|
+
const shown = ops.map((op, index) => op.kind !== " " || ops.slice(Math.max(0, index - context), index + context + 1).some((near) => near.kind !== " "));
|
|
51
|
+
const lines = [];
|
|
52
|
+
ops.forEach((op, index) => {
|
|
53
|
+
if (!shown[index]) return;
|
|
54
|
+
if (lines.length && !shown[index - 1]) lines.push(" \u2026");
|
|
55
|
+
lines.push(` ${op.kind === " " ? " " : op.kind} ${op.line}`);
|
|
56
|
+
});
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
function printPlan(io, style, plan) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
for (const item of plan.items) {
|
|
62
|
+
if (item.kind === "create") {
|
|
63
|
+
lines.push(`${style.green("+")} ${item.path} ${style.dim(item.reason)}`);
|
|
64
|
+
if (item.show) lines.push(...item.content.trimEnd().split("\n").map((line) => style.dim(` ${line}`)));
|
|
65
|
+
} else if (item.kind === "replace") {
|
|
66
|
+
lines.push(`${style.yellow("!")} ${item.path} ${style.dim(item.reason)}`);
|
|
67
|
+
if (item.show) lines.push(...item.content.trimEnd().split("\n").map((line) => style.dim(` ${line}`)));
|
|
68
|
+
} else if (item.kind === "keep") lines.push(`${style.dim("=")} ${item.path} ${style.dim(item.reason)}`);
|
|
69
|
+
else if (item.kind === "edit") {
|
|
70
|
+
lines.push(`${style.yellow("~")} ${item.path} ${style.dim(item.reason)}`);
|
|
71
|
+
lines.push(...diff(item.before, item.after).map((line) => line.startsWith(" +") ? style.green(line) : line.startsWith(" -") ? style.red(line) : style.dim(line)));
|
|
72
|
+
} else {
|
|
73
|
+
lines.push(`${style.yellow("?")} ${item.path} ${item.reason}`);
|
|
74
|
+
lines.push(...item.snippet.split("\n").map((line) => ` ${line}`));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
io.out(`${lines.join("\n")}
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
async function confirmPlan(io, plan, yes) {
|
|
81
|
+
if (!writes(plan).length || yes) return true;
|
|
82
|
+
if (!io.interactive) {
|
|
83
|
+
throw new CommandError("nothing written: confirm with --yes, or only preview with --dry-run");
|
|
84
|
+
}
|
|
85
|
+
const answer = (await io.ask("\nWrite these changes? [Y/n] ")).trim().toLowerCase();
|
|
86
|
+
if (answer === "" || answer === "y" || answer === "yes") return true;
|
|
87
|
+
io.out("Cancelled; nothing was written.\n");
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
function backupPath(path) {
|
|
91
|
+
let candidate = `${path}.backup`;
|
|
92
|
+
for (let index = 2; existsSync(candidate); index++) candidate = `${path}.backup-${index}`;
|
|
93
|
+
return candidate;
|
|
94
|
+
}
|
|
95
|
+
async function applyPlan(root, plan) {
|
|
96
|
+
for (const item of writes(plan)) {
|
|
97
|
+
const path = join(root, item.path);
|
|
98
|
+
if (item.kind === "create" && existsSync(path)) throw new CommandError(`${item.path} exists now; nothing was written`);
|
|
99
|
+
if (item.kind !== "create" && !existsSync(path)) throw new CommandError(`${item.path} is gone now; nothing was written`);
|
|
100
|
+
}
|
|
101
|
+
const written = [];
|
|
102
|
+
try {
|
|
103
|
+
for (const item of writes(plan)) {
|
|
104
|
+
const path = join(root, item.path);
|
|
105
|
+
await mkdir(dirname(path), { recursive: true });
|
|
106
|
+
if (item.kind === "create") await writeFile(path, item.content, { flag: "wx" });
|
|
107
|
+
else if (item.kind === "replace") {
|
|
108
|
+
await rename(path, backupPath(path));
|
|
109
|
+
await writeFile(path, item.content);
|
|
110
|
+
} else await writeFile(path, item.after);
|
|
111
|
+
written.push(item.path);
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw new CommandError(`${error instanceof Error ? error.message : String(error)}${written.length ? `
|
|
115
|
+
Already written: ${written.join(", ")}` : "\n Nothing was written."}`);
|
|
116
|
+
}
|
|
117
|
+
return written;
|
|
118
|
+
}
|
|
119
|
+
class Workspace {
|
|
120
|
+
constructor(root) {
|
|
121
|
+
this.root = root;
|
|
122
|
+
}
|
|
123
|
+
items = [];
|
|
124
|
+
find(path) {
|
|
125
|
+
return this.items.find((item) => item.path === path && item.kind !== "paste" && item.kind !== "keep");
|
|
126
|
+
}
|
|
127
|
+
/** The file as it will be: planned contents first, the disk otherwise. */
|
|
128
|
+
async read(path) {
|
|
129
|
+
const planned = this.find(path);
|
|
130
|
+
if (planned?.kind === "create" || planned?.kind === "replace") return planned.content;
|
|
131
|
+
if (planned?.kind === "edit") return planned.after;
|
|
132
|
+
return readFile(join(this.root, path), "utf8").catch(() => void 0);
|
|
133
|
+
}
|
|
134
|
+
exists(path) {
|
|
135
|
+
return Boolean(this.find(path)) || existsSync(join(this.root, path));
|
|
136
|
+
}
|
|
137
|
+
/** A new file; `show` prints its contents in the plan, for short files worth reading. */
|
|
138
|
+
create(path, content, reason, { show = false } = {}) {
|
|
139
|
+
if (this.exists(path)) throw new CommandError(`${path} exists already; nothing was written`);
|
|
140
|
+
this.items.push({ kind: "create", path, content, reason, ...show ? { show } : {} });
|
|
141
|
+
}
|
|
142
|
+
/** Replace an existing file; the old one is kept as a backup. */
|
|
143
|
+
replace(path, content, reason, { show = false } = {}) {
|
|
144
|
+
this.items.push({ kind: "replace", path, content, reason, ...show ? { show } : {} });
|
|
145
|
+
}
|
|
146
|
+
/** Change a file; for a file this plan creates, the change becomes part of it. */
|
|
147
|
+
async edit(path, after, reason) {
|
|
148
|
+
const planned = this.find(path);
|
|
149
|
+
if (planned?.kind === "create" || planned?.kind === "replace") {
|
|
150
|
+
planned.content = after;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (planned?.kind === "edit") {
|
|
154
|
+
planned.after = after;
|
|
155
|
+
planned.reason = `${planned.reason}; ${reason}`;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const before = await readFile(join(this.root, path), "utf8");
|
|
159
|
+
this.items.push({ kind: "edit", path, before, after, reason });
|
|
160
|
+
}
|
|
161
|
+
keep(path, reason) {
|
|
162
|
+
this.items.push({ kind: "keep", path, reason });
|
|
163
|
+
}
|
|
164
|
+
paste(path, snippet, reason) {
|
|
165
|
+
this.items.push({ kind: "paste", path, snippet, reason });
|
|
166
|
+
}
|
|
167
|
+
plan() {
|
|
168
|
+
return { items: [...this.items] };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { Workspace, applyPlan, backupPath, confirmPlan, diff, printPlan };
|