@xenonbyte/da-vinci-workflow 0.1.15 → 0.1.17

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +35 -1
  3. package/README.zh-CN.md +35 -1
  4. package/SKILL.md +18 -0
  5. package/commands/claude/dv/design.md +7 -0
  6. package/commands/codex/prompts/dv-design.md +7 -0
  7. package/commands/gemini/dv/design.toml +7 -0
  8. package/docs/mode-use-cases.md +5 -1
  9. package/docs/prompt-presets/README.md +3 -0
  10. package/docs/prompt-presets/desktop-app.md +16 -1
  11. package/docs/prompt-presets/mobile-app.md +16 -1
  12. package/docs/prompt-presets/tablet-app.md +16 -1
  13. package/docs/prompt-presets/web-app.md +16 -1
  14. package/docs/visual-assist-presets/README.md +5 -0
  15. package/docs/workflow-examples.md +22 -8
  16. package/docs/zh-CN/mode-use-cases.md +13 -4
  17. package/docs/zh-CN/prompt-presets/README.md +3 -0
  18. package/docs/zh-CN/prompt-presets/desktop-app.md +16 -1
  19. package/docs/zh-CN/prompt-presets/mobile-app.md +16 -1
  20. package/docs/zh-CN/prompt-presets/tablet-app.md +16 -1
  21. package/docs/zh-CN/prompt-presets/web-app.md +16 -1
  22. package/docs/zh-CN/visual-assist-presets/README.md +5 -0
  23. package/docs/zh-CN/workflow-examples.md +22 -8
  24. package/lib/audit.js +66 -2
  25. package/lib/cli.js +356 -1
  26. package/lib/mcp-runtime-gate.js +53 -1
  27. package/lib/pen-persistence.js +515 -0
  28. package/lib/pencil-lock.js +128 -0
  29. package/lib/pencil-preflight.js +438 -0
  30. package/lib/pencil-session.js +229 -0
  31. package/package.json +6 -2
  32. package/references/artifact-templates.md +2 -0
  33. package/references/checkpoints.md +14 -0
  34. package/references/pencil-design-to-code.md +15 -0
  35. package/scripts/fixtures/complex-sample.pen +295 -0
  36. package/scripts/test-mcp-runtime-gate.js +88 -0
  37. package/scripts/test-pen-persistence.js +250 -0
  38. package/scripts/test-pencil-preflight.js +153 -0
  39. package/scripts/test-pencil-session.js +152 -0
  40. package/scripts/test-persistence-flows.js +315 -0
@@ -0,0 +1,515 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const crypto = require("crypto");
5
+ const { spawnSync } = require("child_process");
6
+
7
+ const DEFAULT_PEN_VERSION = "2.9";
8
+ const DEFAULT_READ_DEPTH = 50;
9
+
10
+ function isPlainObject(value) {
11
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
12
+ }
13
+
14
+ function extractFirstJson(text) {
15
+ const source = String(text || "");
16
+ const decoder = new JSONDecoder();
17
+ return decoder.decodeFirst(source);
18
+ }
19
+
20
+ class JSONDecoder {
21
+ constructor() {
22
+ this.decoder = JSON;
23
+ }
24
+
25
+ decodeFirst(text) {
26
+ for (let index = 0; index < text.length; index += 1) {
27
+ const ch = text[index];
28
+ if (ch !== "{" && ch !== "[") {
29
+ continue;
30
+ }
31
+
32
+ try {
33
+ const { value } = this.rawDecode(text.slice(index));
34
+ return value;
35
+ } catch (error) {
36
+ continue;
37
+ }
38
+ }
39
+
40
+ throw new Error("No JSON payload found.");
41
+ }
42
+
43
+ rawDecode(text) {
44
+ let end = 0;
45
+ let inString = false;
46
+ let escape = false;
47
+ let depth = 0;
48
+ const opening = text[0];
49
+ const closing = opening === "{" ? "}" : "]";
50
+
51
+ for (let index = 0; index < text.length; index += 1) {
52
+ const ch = text[index];
53
+
54
+ if (inString) {
55
+ if (escape) {
56
+ escape = false;
57
+ } else if (ch === "\\") {
58
+ escape = true;
59
+ } else if (ch === "\"") {
60
+ inString = false;
61
+ }
62
+ continue;
63
+ }
64
+
65
+ if (ch === "\"") {
66
+ inString = true;
67
+ continue;
68
+ }
69
+
70
+ if (ch === opening) {
71
+ depth += 1;
72
+ } else if (ch === closing) {
73
+ depth -= 1;
74
+ if (depth === 0) {
75
+ end = index + 1;
76
+ break;
77
+ }
78
+ }
79
+ }
80
+
81
+ if (end === 0) {
82
+ throw new Error("Incomplete JSON payload.");
83
+ }
84
+
85
+ return {
86
+ value: JSON.parse(text.slice(0, end)),
87
+ end
88
+ };
89
+ }
90
+ }
91
+
92
+ function readJsonPayload(filePath) {
93
+ const raw = fs.readFileSync(filePath, "utf8");
94
+ try {
95
+ return JSON.parse(raw);
96
+ } catch (error) {
97
+ return extractFirstJson(raw);
98
+ }
99
+ }
100
+
101
+ function hasTruncatedChildren(value) {
102
+ if (Array.isArray(value)) {
103
+ return value.some((item) => hasTruncatedChildren(item));
104
+ }
105
+
106
+ if (!isPlainObject(value)) {
107
+ return value === "...";
108
+ }
109
+
110
+ return Object.values(value).some((item) => hasTruncatedChildren(item));
111
+ }
112
+
113
+ function normalizeNodesPayload(payload) {
114
+ const nodes = getNodesArray(payload);
115
+
116
+ if (!nodes) {
117
+ throw new Error("Nodes payload must be an array or an object with a `nodes` array.");
118
+ }
119
+
120
+ if (hasTruncatedChildren(nodes)) {
121
+ throw new Error("Nodes payload is truncated (`...`). Re-read the live document with a deeper batch_get before persisting.");
122
+ }
123
+
124
+ return nodes;
125
+ }
126
+
127
+ function getNodesArray(payload) {
128
+ if (Array.isArray(payload)) {
129
+ return payload;
130
+ }
131
+
132
+ if (payload && Array.isArray(payload.nodes)) {
133
+ return payload.nodes;
134
+ }
135
+
136
+ return null;
137
+ }
138
+
139
+ function normalizeVariablesPayload(payload) {
140
+ if (!payload) {
141
+ return undefined;
142
+ }
143
+
144
+ if (isPlainObject(payload.variables)) {
145
+ return payload.variables;
146
+ }
147
+
148
+ if (isPlainObject(payload)) {
149
+ return payload;
150
+ }
151
+
152
+ throw new Error("Variables payload must be an object or an object with a `variables` key.");
153
+ }
154
+
155
+ function canonicalizeJson(value) {
156
+ if (Array.isArray(value)) {
157
+ return value.map((item) => canonicalizeJson(item));
158
+ }
159
+
160
+ if (!isPlainObject(value)) {
161
+ return value;
162
+ }
163
+
164
+ return Object.keys(value)
165
+ .sort()
166
+ .reduce((accumulator, key) => {
167
+ accumulator[key] = canonicalizeJson(value[key]);
168
+ return accumulator;
169
+ }, {});
170
+ }
171
+
172
+ function buildPenDocument({ version = DEFAULT_PEN_VERSION, nodes, variables }) {
173
+ const document = {
174
+ version,
175
+ children: normalizeNodesPayload(nodes)
176
+ };
177
+
178
+ const normalizedVariables = normalizeVariablesPayload(variables);
179
+ if (normalizedVariables && Object.keys(normalizedVariables).length > 0) {
180
+ document.variables = normalizedVariables;
181
+ }
182
+
183
+ return document;
184
+ }
185
+
186
+ function hashPenDocument(document) {
187
+ return crypto
188
+ .createHash("sha256")
189
+ .update(JSON.stringify(canonicalizeJson(document)))
190
+ .digest("hex");
191
+ }
192
+
193
+ function getStandardPenStatePath(outputPath) {
194
+ const targetPath = path.resolve(outputPath);
195
+ const marker = `${path.sep}.da-vinci${path.sep}designs${path.sep}`;
196
+ const markerIndex = targetPath.indexOf(marker);
197
+
198
+ if (markerIndex < 0) {
199
+ return `${targetPath}.meta.json`;
200
+ }
201
+
202
+ const projectRoot = targetPath.slice(0, markerIndex);
203
+ const relativePenPath = targetPath.slice(markerIndex + marker.length);
204
+ const stateFileName = `${relativePenPath.split(path.sep).join("__")}.json`;
205
+ return path.join(projectRoot, ".da-vinci", "state", "pens", stateFileName);
206
+ }
207
+
208
+ function writePenDocumentAtomic(outputPath, document) {
209
+ const targetPath = path.resolve(outputPath);
210
+ const targetDir = path.dirname(targetPath);
211
+ const tempPath = path.join(
212
+ targetDir,
213
+ `.${path.basename(targetPath)}.tmp-${process.pid}-${Date.now()}`
214
+ );
215
+
216
+ fs.mkdirSync(targetDir, { recursive: true });
217
+ fs.writeFileSync(tempPath, JSON.stringify(document, null, 2) + "\n", "utf8");
218
+ fs.renameSync(tempPath, targetPath);
219
+ return targetPath;
220
+ }
221
+
222
+ function writeJsonFileAtomic(outputPath, payload) {
223
+ const targetPath = path.resolve(outputPath);
224
+ const targetDir = path.dirname(targetPath);
225
+ const tempPath = path.join(
226
+ targetDir,
227
+ `.${path.basename(targetPath)}.tmp-${process.pid}-${Date.now()}`
228
+ );
229
+
230
+ fs.mkdirSync(targetDir, { recursive: true });
231
+ fs.writeFileSync(tempPath, JSON.stringify(payload, null, 2) + "\n", "utf8");
232
+ fs.renameSync(tempPath, targetPath);
233
+ return targetPath;
234
+ }
235
+
236
+ function buildPenState(outputPath, document, options = {}) {
237
+ return {
238
+ schema: 1,
239
+ source: options.source || "write-pen",
240
+ penPath: path.resolve(outputPath),
241
+ version: document.version || DEFAULT_PEN_VERSION,
242
+ snapshotHash: hashPenDocument(document),
243
+ topLevelIds: Array.isArray(document.children) ? document.children.map((node) => node.id) : [],
244
+ topLevelCount: Array.isArray(document.children) ? document.children.length : 0,
245
+ persistedAt: options.persistedAt || new Date().toISOString()
246
+ };
247
+ }
248
+
249
+ function writePenStateAtomic(outputPath, state) {
250
+ const statePath = getStandardPenStatePath(outputPath);
251
+ writeJsonFileAtomic(statePath, state);
252
+ return statePath;
253
+ }
254
+
255
+ function readPenState(outputPath) {
256
+ const statePath = getStandardPenStatePath(outputPath);
257
+ if (!fs.existsSync(statePath)) {
258
+ return null;
259
+ }
260
+ return readJsonPayload(statePath);
261
+ }
262
+
263
+ function runPencilInteractive(inputPath, commands, options = {}) {
264
+ const pencilBin = options.pencilBin || "pencil";
265
+ const unusedOutput = path.join(
266
+ os.tmpdir(),
267
+ `da-vinci-verify-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.pen`
268
+ );
269
+ const payload = Array.isArray(commands) ? commands.join("\n") : String(commands || "");
270
+ const result = spawnSync(
271
+ pencilBin,
272
+ ["interactive", "-i", path.resolve(inputPath), "-o", unusedOutput],
273
+ {
274
+ input: `${payload}\nexit()\n`,
275
+ encoding: "utf8",
276
+ maxBuffer: 8 * 1024 * 1024
277
+ }
278
+ );
279
+
280
+ fs.rmSync(unusedOutput, { force: true });
281
+
282
+ const stdout = result.stdout || "";
283
+ const stderr = result.stderr || "";
284
+ if (result.status !== 0) {
285
+ throw new Error(
286
+ `Pencil interactive failed for ${inputPath}.\n${stdout}${stderr}`.trim()
287
+ );
288
+ }
289
+
290
+ return `${stdout}${stderr}`;
291
+ }
292
+
293
+ function verifyPenFileWithPencil(filePath, options = {}) {
294
+ const output = runPencilInteractive(filePath, ['batch_get({ readDepth: 1 })'], options);
295
+ const payload = extractFirstJson(output);
296
+ const nodes = getNodesArray(payload);
297
+ const expectedTopLevelIds = options.expectedTopLevelIds || [];
298
+
299
+ if (!nodes) {
300
+ throw new Error("Pencil reopen verification did not return a `nodes` array.");
301
+ }
302
+
303
+ if (expectedTopLevelIds.length > 0) {
304
+ const actualIds = nodes.map((node) => node.id);
305
+ if (JSON.stringify(actualIds) !== JSON.stringify(expectedTopLevelIds)) {
306
+ throw new Error(
307
+ `Reopened .pen file top-level ids do not match. Expected ${expectedTopLevelIds.join(", ")}, got ${actualIds.join(", ")}.`
308
+ );
309
+ }
310
+ }
311
+
312
+ return {
313
+ topLevelCount: nodes.length,
314
+ topLevelIds: nodes.map((node) => node.id)
315
+ };
316
+ }
317
+
318
+ function writePenFromPayloadFiles(options) {
319
+ const outputPath = path.resolve(options.outputPath);
320
+ const nodes = readJsonPayload(options.nodesFile);
321
+ const variables = options.variablesFile ? readJsonPayload(options.variablesFile) : undefined;
322
+ const document = buildPenDocument({
323
+ version: options.version || DEFAULT_PEN_VERSION,
324
+ nodes,
325
+ variables
326
+ });
327
+
328
+ writePenDocumentAtomic(outputPath, document);
329
+ const state = buildPenState(outputPath, document, { source: "write-pen" });
330
+ const statePath = writePenStateAtomic(outputPath, state);
331
+
332
+ const verification = options.verifyWithPencil
333
+ ? verifyPenFileWithPencil(outputPath, {
334
+ pencilBin: options.pencilBin,
335
+ expectedTopLevelIds: document.children.map((node) => node.id)
336
+ })
337
+ : null;
338
+
339
+ return {
340
+ outputPath,
341
+ document,
342
+ verification,
343
+ state,
344
+ statePath
345
+ };
346
+ }
347
+
348
+ function readPenVersion(inputPath) {
349
+ const payload = JSON.parse(fs.readFileSync(inputPath, "utf8"));
350
+ return payload.version || DEFAULT_PEN_VERSION;
351
+ }
352
+
353
+ function capturePenSnapshot(inputPath, options = {}) {
354
+ const readDepth = options.readDepth || DEFAULT_READ_DEPTH;
355
+ const nodesOutput = runPencilInteractive(
356
+ inputPath,
357
+ [
358
+ `batch_get({ readDepth: ${readDepth}, includePathGeometry: true, resolveInstances: false, resolveVariables: false })`
359
+ ],
360
+ options
361
+ );
362
+ const variablesOutput = runPencilInteractive(inputPath, ["get_variables()"], options);
363
+
364
+ return {
365
+ nodes: normalizeNodesPayload(extractFirstJson(nodesOutput)),
366
+ variables: normalizeVariablesPayload(extractFirstJson(variablesOutput))
367
+ };
368
+ }
369
+
370
+ function snapshotPenFile(options) {
371
+ const inputPath = path.resolve(options.inputPath);
372
+ const outputPath = path.resolve(options.outputPath);
373
+ const snapshot = capturePenSnapshot(inputPath, options);
374
+ const document = buildPenDocument({
375
+ version: options.version || readPenVersion(inputPath),
376
+ nodes: snapshot.nodes,
377
+ variables: snapshot.variables
378
+ });
379
+
380
+ writePenDocumentAtomic(outputPath, document);
381
+ const state = buildPenState(outputPath, document, { source: "snapshot-pen" });
382
+ const statePath = writePenStateAtomic(outputPath, state);
383
+
384
+ const verification = options.verifyWithPencil
385
+ ? verifyPenFileWithPencil(outputPath, {
386
+ pencilBin: options.pencilBin,
387
+ expectedTopLevelIds: document.children.map((node) => node.id)
388
+ })
389
+ : null;
390
+
391
+ return {
392
+ inputPath,
393
+ outputPath,
394
+ document,
395
+ verification,
396
+ state,
397
+ statePath
398
+ };
399
+ }
400
+
401
+ function readPenDocument(inputPath) {
402
+ return readJsonPayload(inputPath);
403
+ }
404
+
405
+ function ensurePenFile(options) {
406
+ const outputPath = path.resolve(options.outputPath);
407
+ const exists = fs.existsSync(outputPath);
408
+ let document;
409
+ let created = false;
410
+
411
+ if (exists) {
412
+ document = readPenDocument(outputPath);
413
+ if (!Array.isArray(document.children)) {
414
+ throw new Error(`Existing .pen file is invalid or missing \`children\`: ${outputPath}`);
415
+ }
416
+ } else {
417
+ document = buildPenDocument({
418
+ version: options.version || DEFAULT_PEN_VERSION,
419
+ nodes: { nodes: [] },
420
+ variables: {}
421
+ });
422
+ writePenDocumentAtomic(outputPath, document);
423
+ created = true;
424
+ }
425
+
426
+ const state = buildPenState(outputPath, document, {
427
+ source: created ? "ensure-pen:create" : "ensure-pen:existing"
428
+ });
429
+ const statePath = writePenStateAtomic(outputPath, state);
430
+
431
+ const verification = options.verifyWithPencil
432
+ ? verifyPenFileWithPencil(outputPath, {
433
+ pencilBin: options.pencilBin,
434
+ expectedTopLevelIds: document.children.map((node) => node.id)
435
+ })
436
+ : null;
437
+
438
+ return {
439
+ outputPath,
440
+ created,
441
+ document,
442
+ verification,
443
+ state,
444
+ statePath
445
+ };
446
+ }
447
+
448
+ function hashPayloadFiles(options) {
449
+ const nodes = readJsonPayload(options.nodesFile);
450
+ const variables = options.variablesFile ? readJsonPayload(options.variablesFile) : undefined;
451
+ const document = buildPenDocument({
452
+ version: options.version || DEFAULT_PEN_VERSION,
453
+ nodes,
454
+ variables
455
+ });
456
+
457
+ return {
458
+ document,
459
+ snapshotHash: hashPenDocument(document)
460
+ };
461
+ }
462
+
463
+ function comparePenSync(options) {
464
+ const penPath = path.resolve(options.penPath);
465
+ if (!fs.existsSync(penPath)) {
466
+ throw new Error(`Registered .pen file does not exist: ${penPath}`);
467
+ }
468
+
469
+ const persistedDocument = readPenDocument(penPath);
470
+ const live = hashPayloadFiles({
471
+ ...options,
472
+ version: options.version || persistedDocument.version || DEFAULT_PEN_VERSION
473
+ });
474
+ const persistedState = readPenState(penPath);
475
+ const persistedHash = persistedState && persistedState.snapshotHash
476
+ ? String(persistedState.snapshotHash)
477
+ : hashPenDocument(persistedDocument);
478
+
479
+ return {
480
+ penPath,
481
+ statePath: getStandardPenStatePath(penPath),
482
+ persistedHash,
483
+ liveHash: live.snapshotHash,
484
+ inSync: persistedHash === live.snapshotHash,
485
+ usedStateFile: Boolean(persistedState && persistedState.snapshotHash),
486
+ state: persistedState
487
+ };
488
+ }
489
+
490
+ module.exports = {
491
+ DEFAULT_PEN_VERSION,
492
+ DEFAULT_READ_DEPTH,
493
+ extractFirstJson,
494
+ readJsonPayload,
495
+ normalizeNodesPayload,
496
+ normalizeVariablesPayload,
497
+ canonicalizeJson,
498
+ buildPenDocument,
499
+ hashPenDocument,
500
+ getStandardPenStatePath,
501
+ writePenDocumentAtomic,
502
+ writeJsonFileAtomic,
503
+ buildPenState,
504
+ writePenStateAtomic,
505
+ readPenState,
506
+ runPencilInteractive,
507
+ verifyPenFileWithPencil,
508
+ writePenFromPayloadFiles,
509
+ capturePenSnapshot,
510
+ snapshotPenFile,
511
+ readPenDocument,
512
+ ensurePenFile,
513
+ hashPayloadFiles,
514
+ comparePenSync
515
+ };
@@ -0,0 +1,128 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+
5
+ function getLockPath(options = {}) {
6
+ const homeDir = options.homeDir ? path.resolve(options.homeDir) : os.homedir();
7
+ return path.join(homeDir, ".da-vinci", "pencil-mcp.lock");
8
+ }
9
+
10
+ function readLock(lockPath) {
11
+ const resolvedLockPath = path.resolve(lockPath);
12
+ if (!fs.existsSync(resolvedLockPath)) {
13
+ return null;
14
+ }
15
+
16
+ return JSON.parse(fs.readFileSync(resolvedLockPath, "utf8"));
17
+ }
18
+
19
+ function writeLock(lockPath, payload) {
20
+ const resolvedLockPath = path.resolve(lockPath);
21
+ fs.mkdirSync(path.dirname(resolvedLockPath), { recursive: true });
22
+ const handle = fs.openSync(resolvedLockPath, "wx");
23
+
24
+ try {
25
+ fs.writeFileSync(handle, JSON.stringify(payload, null, 2) + "\n", "utf8");
26
+ } finally {
27
+ fs.closeSync(handle);
28
+ }
29
+ }
30
+
31
+ function acquirePencilLock(options = {}) {
32
+ const projectPath = path.resolve(options.projectPath || process.cwd());
33
+ const owner = options.owner || path.basename(projectPath);
34
+ const waitMs = Number.isFinite(Number(options.waitMs)) ? Number(options.waitMs) : 0;
35
+ const pollIntervalMs = Math.max(50, Number(options.pollIntervalMs) || 100);
36
+ const lockPath = getLockPath(options);
37
+ const deadline = Date.now() + Math.max(0, waitMs);
38
+ const payload = {
39
+ schema: 1,
40
+ projectPath,
41
+ owner,
42
+ pid: process.pid,
43
+ acquiredAt: new Date().toISOString()
44
+ };
45
+
46
+ while (true) {
47
+ try {
48
+ writeLock(lockPath, payload);
49
+ return {
50
+ lockPath,
51
+ acquired: true,
52
+ alreadyHeld: false,
53
+ lock: payload
54
+ };
55
+ } catch (error) {
56
+ if (error.code !== "EEXIST") {
57
+ throw error;
58
+ }
59
+
60
+ const current = readLock(lockPath);
61
+ if (current && current.projectPath === projectPath) {
62
+ return {
63
+ lockPath,
64
+ acquired: true,
65
+ alreadyHeld: true,
66
+ lock: current
67
+ };
68
+ }
69
+
70
+ if (Date.now() >= deadline) {
71
+ const holder = current
72
+ ? `${current.projectPath} (owner: ${current.owner || "unknown"}, pid: ${current.pid || "unknown"})`
73
+ : "unknown holder";
74
+ throw new Error(`Pencil MCP lock is already held by ${holder}.`);
75
+ }
76
+
77
+ const sleepUntil = Date.now() + pollIntervalMs;
78
+ while (Date.now() < sleepUntil) {
79
+ // Busy wait to keep the CLI synchronous without additional runtime dependencies.
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ function releasePencilLock(options = {}) {
86
+ const projectPath = options.projectPath ? path.resolve(options.projectPath) : "";
87
+ const lockPath = getLockPath(options);
88
+ const current = readLock(lockPath);
89
+
90
+ if (!current) {
91
+ return {
92
+ lockPath,
93
+ released: false,
94
+ hadLock: false,
95
+ lock: null
96
+ };
97
+ }
98
+
99
+ if (projectPath && current.projectPath !== projectPath && !options.force) {
100
+ throw new Error(
101
+ `Pencil MCP lock is held by a different project: ${current.projectPath}`
102
+ );
103
+ }
104
+
105
+ fs.rmSync(lockPath, { force: true });
106
+ return {
107
+ lockPath,
108
+ released: true,
109
+ hadLock: true,
110
+ lock: current
111
+ };
112
+ }
113
+
114
+ function getPencilLockStatus(options = {}) {
115
+ const lockPath = getLockPath(options);
116
+ return {
117
+ lockPath,
118
+ lock: readLock(lockPath)
119
+ };
120
+ }
121
+
122
+ module.exports = {
123
+ getLockPath,
124
+ readLock,
125
+ acquirePencilLock,
126
+ releasePencilLock,
127
+ getPencilLockStatus
128
+ };