dsh-office-kit 0.4.1 → 0.4.3
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/assets/skills/excel-live-control/SKILL.md +120 -148
- package/assets/skills/excel-live-control/container_tools/live_excel.py +9 -0
- package/assets/skills/excel-live-control/domain_guidance/financial_models.md +42 -58
- package/assets/skills/excel-live-control/domain_guidance/healthcare.md +36 -42
- package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +33 -34
- package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +34 -36
- package/assets/skills/excel-live-control/references/office-automation.md +127 -153
- package/assets/skills/excel-live-control/references/read_only_qna.md +40 -45
- package/assets/skills/excel-live-control/style_guidelines.md +56 -64
- package/assets/skills/excel-live-control/workflows/create_workflows.md +40 -47
- package/assets/skills/excel-live-control/workflows/edit_workflows.md +37 -45
- package/assets/skills/office-documents/SKILL.md +24 -60
- package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +5 -5
- package/assets/skills/office-documents/ooxml/comments.md +8 -8
- package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +10 -10
- package/assets/skills/office-documents/ooxml/rels_and_content_types.md +7 -7
- package/assets/skills/office-documents/ooxml/tracked_changes.md +12 -12
- package/assets/skills/office-documents/references/docx-structure.md +11 -13
- package/assets/skills/office-documents/references/template-elicitation.md +11 -13
- package/assets/skills/office-documents/references/writing-quality.md +20 -20
- package/assets/skills/office-documents/tasks/accessibility_a11y.md +16 -22
- package/assets/skills/office-documents/tasks/captions_crossrefs.md +23 -26
- package/assets/skills/office-documents/tasks/clean_tracked_changes.md +17 -22
- package/assets/skills/office-documents/tasks/comments_manage.md +9 -11
- package/assets/skills/office-documents/tasks/compare_diff.md +10 -16
- package/assets/skills/office-documents/tasks/create_edit.md +14 -16
- package/assets/skills/office-documents/tasks/equations.md +17 -28
- package/assets/skills/office-documents/tasks/fields_update.md +14 -16
- package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +19 -33
- package/assets/skills/office-documents/tasks/footnotes_endnotes.md +12 -14
- package/assets/skills/office-documents/tasks/forms_content_controls.md +16 -20
- package/assets/skills/office-documents/tasks/headings_numbering.md +17 -22
- package/assets/skills/office-documents/tasks/images_figures.md +15 -25
- package/assets/skills/office-documents/tasks/multi_doc_merge.md +13 -23
- package/assets/skills/office-documents/tasks/navigation_internal_links.md +18 -21
- package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +16 -22
- package/assets/skills/office-documents/tasks/protection_restrict_editing.md +19 -21
- package/assets/skills/office-documents/tasks/read_review.md +9 -20
- package/assets/skills/office-documents/tasks/redaction_anonymization.md +17 -18
- package/assets/skills/office-documents/tasks/sections_layout.md +16 -23
- package/assets/skills/office-documents/tasks/style_lint_normalize.md +18 -22
- package/assets/skills/office-documents/tasks/tables_spreadsheets.md +21 -31
- package/assets/skills/office-documents/tasks/templates_style_packs.md +17 -21
- package/assets/skills/office-documents/tasks/toc_workflow.md +10 -14
- package/assets/skills/office-documents/tasks/verify_render.md +11 -23
- package/assets/skills/office-documents/tasks/watermarks_background.md +11 -15
- package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +9 -9
- package/assets/skills/office-documents/troubleshooting/run_splitting.md +12 -12
- package/assets/skills/office-pdf/SKILL.md +36 -51
- package/assets/skills/office-pdf/references/form-fields.md +20 -20
- package/assets/skills/office-pdf/references/geometry.md +10 -10
- package/assets/skills/office-pdf/references/layout-quality.md +13 -13
- package/assets/skills/office-pdf/references/limits.md +12 -56
- package/assets/skills/office-pdf/tasks/annotations.md +5 -5
- package/assets/skills/office-pdf/tasks/create.md +6 -9
- package/assets/skills/office-pdf/tasks/edit-pages.md +14 -14
- package/assets/skills/office-pdf/tasks/forms.md +15 -15
- package/assets/skills/office-pdf/tasks/read.md +9 -9
- package/assets/skills/office-pdf/tasks/redact.md +18 -18
- package/assets/skills/office-pdf/tasks/stamp.md +10 -10
- package/assets/skills/office-presentations/SKILL.md +71 -91
- package/assets/skills/office-presentations/container_tools/render_slides.py +1 -1
- package/assets/skills/office-presentations/domain_guidance/finance.md +16 -16
- package/assets/skills/office-presentations/recipes/README.md +24 -35
- package/assets/skills/office-presentations/recipes/autolayout.md +12 -13
- package/assets/skills/office-presentations/recipes/connectors.md +11 -12
- package/assets/skills/office-presentations/recipes/images.md +11 -12
- package/assets/skills/office-presentations/recipes/master-and-layouts.md +18 -18
- package/assets/skills/office-presentations/recipes/native-charts.md +8 -8
- package/assets/skills/office-presentations/recipes/rich-text-runs.md +13 -13
- package/assets/skills/office-presentations/recipes/speaker-notes.md +7 -7
- package/assets/skills/office-presentations/recipes/tables.md +14 -14
- package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +9 -9
- package/assets/skills/office-presentations/recipes/theme-tokens.md +7 -8
- package/assets/skills/office-presentations/references/authoring-workflow.md +38 -43
- package/assets/skills/office-presentations/references/finalization.md +10 -12
- package/assets/skills/office-presentations/references/native-bullets.md +14 -18
- package/assets/skills/office-presentations/references/native-evidence.md +18 -32
- package/assets/skills/office-presentations/references/style-guidelines.md +27 -32
- package/assets/skills/office-presentations/references/templates.md +31 -60
- package/assets/skills/office-presentations/routing/google_slides.md +4 -4
- package/assets/skills/office-presentations/tasks/apply-annotations.md +17 -17
- package/assets/skills/office-presentations/tasks/charts-tables-notes.md +20 -23
- package/assets/skills/office-presentations/tasks/create-deck.md +19 -19
- package/assets/skills/office-presentations/tasks/edit-deck.md +14 -16
- package/assets/skills/office-presentations/tasks/inspect-and-verify.md +31 -34
- package/assets/skills/office-presentations/tasks/template-following.md +23 -28
- package/assets/skills/office-spreadsheets/SKILL.md +32 -39
- package/assets/skills/office-spreadsheets/recipes/charts.md +8 -26
- package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +9 -26
- package/assets/skills/office-spreadsheets/recipes/data-tables.md +7 -23
- package/assets/skills/office-spreadsheets/recipes/named-ranges.md +8 -21
- package/assets/skills/office-spreadsheets/recipes/number-formats.md +7 -23
- package/assets/skills/office-spreadsheets/recipes/sparklines.md +7 -20
- package/assets/skills/office-spreadsheets/references/charts.md +15 -34
- package/assets/skills/office-spreadsheets/references/domain/financial-models.md +10 -25
- package/assets/skills/office-spreadsheets/references/domain/healthcare.md +8 -19
- package/assets/skills/office-spreadsheets/references/domain/marketing.md +8 -22
- package/assets/skills/office-spreadsheets/references/domain/research.md +8 -19
- package/assets/skills/office-spreadsheets/references/formula-patterns.md +20 -50
- package/assets/skills/office-spreadsheets/references/style-guidelines.md +19 -44
- package/assets/skills/office-spreadsheets/references/workbook-structure.md +12 -35
- package/assets/skills/office-spreadsheets/routing/google-sheets.md +11 -26
- package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +10 -25
- package/assets/skills/office-spreadsheets/tasks/annotations.md +12 -30
- package/assets/skills/office-spreadsheets/tasks/create-workbook.md +5 -5
- package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +2 -2
- package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +5 -5
- package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +9 -9
- package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +10 -30
- package/assets/skills/shared/annotation-recipes.md +98 -120
- package/assets/skills/shared/common-workflow.md +70 -80
- package/dist/index.js +105 -29
- package/package.json +1 -1
- package/postinstall.mjs +58 -17
- package/python/office_cli.py +70 -7
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
@@ -125,24 +125,31 @@ const POSTINSTALL_SCRIPT = join(PACKAGE_ROOT, "postinstall.mjs");
|
|
|
125
125
|
const POSTINSTALL_TIMEOUT = 540 * 1e3;
|
|
126
126
|
/** Failed probes are retried after this delay instead of re-running pip on every status call. */
|
|
127
127
|
const FAILURE_RETRY_DELAY = 300 * 1e3;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return
|
|
128
|
+
/** Where every version of this plugin keeps its runtime, next to DSH's own data. */
|
|
129
|
+
function officeHome() {
|
|
130
|
+
return join(homedir(), ".dsh", "office");
|
|
131
131
|
}
|
|
132
132
|
function legacyDataDir() {
|
|
133
133
|
const home = homedir();
|
|
134
134
|
return process.platform === "win32" ? join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "dsh-office-kit") : process.platform === "darwin" ? join(home, "Library", "Application Support", "dsh-office-kit") : join(process.env.XDG_DATA_HOME ?? join(home, ".local", "share"), "dsh-office-kit");
|
|
135
135
|
}
|
|
136
|
-
/**
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Config files older versions wrote, newest first.
|
|
138
|
+
*
|
|
139
|
+
* The pre-0.5 config stayed in the OS config directory (`%APPDATA%\dsh-office-kit`,
|
|
140
|
+
* `$XDG_CONFIG_HOME/dsh-office-kit`, `~/Library/Application Support/dsh-office-kit`).
|
|
141
|
+
* Reading them keeps an upgraded install from rebuilding or mis-reporting its venv.
|
|
142
|
+
*/
|
|
143
|
+
function legacyConfigFiles() {
|
|
144
|
+
const home = homedir();
|
|
145
|
+
return [.../* @__PURE__ */ new Set([process.platform === "win32" ? join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "dsh-office-kit", "config.json") : process.platform === "darwin" ? join(home, "Library", "Application Support", "dsh-office-kit", "config.json") : join(process.env.XDG_CONFIG_HOME ?? join(home, ".config"), "dsh-office-kit", "config.json"), join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "dsh-office-kit", "config.json")])];
|
|
139
146
|
}
|
|
140
147
|
/** Resolve per-OS config/data directories and bundled resources. */
|
|
141
148
|
function officePaths() {
|
|
142
|
-
const runtimeDir = join(
|
|
149
|
+
const runtimeDir = join(officeHome(), "runtime");
|
|
143
150
|
return {
|
|
144
|
-
configFile: join(
|
|
145
|
-
|
|
151
|
+
configFile: join(officeHome(), "config.json"),
|
|
152
|
+
legacyConfigFiles: legacyConfigFiles(),
|
|
146
153
|
venv: join(runtimeDir, "venv"),
|
|
147
154
|
legacyVenv: join(legacyDataDir(), "venv"),
|
|
148
155
|
cli: join(PACKAGE_ROOT, "python", "office_cli.py"),
|
|
@@ -244,10 +251,11 @@ function failedInspection(path, error) {
|
|
|
244
251
|
}
|
|
245
252
|
/**
|
|
246
253
|
* Read the interpreter path postinstall recorded; malformed files are ignored.
|
|
247
|
-
* The legacy
|
|
254
|
+
* The legacy locations are read after the canonical one so an upgrade does not
|
|
255
|
+
* orphan a ready venv.
|
|
248
256
|
*/
|
|
249
|
-
function savedPython(configFile,
|
|
250
|
-
const files =
|
|
257
|
+
function savedPython(configFile, legacyConfigFiles = []) {
|
|
258
|
+
const files = [configFile, ...legacyConfigFiles];
|
|
251
259
|
for (const file of files) {
|
|
252
260
|
if (!isFile(file)) continue;
|
|
253
261
|
try {
|
|
@@ -256,6 +264,61 @@ function savedPython(configFile, legacyConfigFile) {
|
|
|
256
264
|
} catch {}
|
|
257
265
|
}
|
|
258
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Read the plugin config, falling back to files older versions wrote.
|
|
269
|
+
*
|
|
270
|
+
* The canonical file wins key by key, so a legacy record contributes only what
|
|
271
|
+
* the new file has not written yet (an upgrade usually still needs its venv).
|
|
272
|
+
*/
|
|
273
|
+
function loadConfig(configFile) {
|
|
274
|
+
const legacy = officePaths().legacyConfigFiles;
|
|
275
|
+
const merged = {};
|
|
276
|
+
for (const file of [...legacy].reverse()) try {
|
|
277
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
278
|
+
if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed)) Object.assign(merged, parsed);
|
|
279
|
+
} catch {}
|
|
280
|
+
try {
|
|
281
|
+
const parsed = JSON.parse(readFileSync(configFile, "utf8"));
|
|
282
|
+
if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed)) Object.assign(merged, parsed);
|
|
283
|
+
} catch {}
|
|
284
|
+
return merged;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Merge one key into the plugin's config without disturbing the rest.
|
|
288
|
+
*
|
|
289
|
+
* Versions that predate the current layout left stale absolute paths behind, so
|
|
290
|
+
* the record is refreshed whenever the host resolves something different.
|
|
291
|
+
*/
|
|
292
|
+
function saveRecord(configFile, key, value) {
|
|
293
|
+
const config = loadConfig(configFile);
|
|
294
|
+
if (JSON.stringify(config[key]) === JSON.stringify(value) && isFile(configFile)) return;
|
|
295
|
+
const temporary = `${configFile}.${process.pid}.tmp`;
|
|
296
|
+
try {
|
|
297
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
298
|
+
writeFileSync(temporary, `${JSON.stringify({
|
|
299
|
+
...config,
|
|
300
|
+
[key]: value
|
|
301
|
+
}, null, 2)}\n`, "utf8");
|
|
302
|
+
renameSync(temporary, configFile);
|
|
303
|
+
} catch {}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Record the renderer the harness loaded so every later helper call finds it.
|
|
307
|
+
*
|
|
308
|
+
* A profile install keeps the plugin in `~/.dsh/profiles/<name>/node_modules`
|
|
309
|
+
* and the kit in the harness tree, so no walk-up from the plugin can reach it.
|
|
310
|
+
* The helper reads this record first; a read-only config must not stop loading.
|
|
311
|
+
*/
|
|
312
|
+
function saveKitPath(configFile, node, cli) {
|
|
313
|
+
saveRecord(configFile, "libreofficeKit", {
|
|
314
|
+
node,
|
|
315
|
+
cli
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/** Record the interpreter this plugin settled on so the next probe can trust it. */
|
|
319
|
+
function savePythonPath(configFile, python) {
|
|
320
|
+
saveRecord(configFile, "python", python);
|
|
321
|
+
}
|
|
259
322
|
/** The interpreter inside a virtual environment, which differs per platform. */
|
|
260
323
|
function venvPython(venv) {
|
|
261
324
|
return join(venv, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
|
|
@@ -266,14 +329,10 @@ function candidates(configFile) {
|
|
|
266
329
|
const found = [];
|
|
267
330
|
const env = process.env.DSH_OFFICE_PYTHON;
|
|
268
331
|
if (env != null && env.length > 0) found.push(env);
|
|
269
|
-
|
|
332
|
+
found.push(venvPython(paths.venv));
|
|
333
|
+
const saved = savedPython(configFile, paths.legacyConfigFiles);
|
|
270
334
|
if (saved != null) found.push(saved);
|
|
271
|
-
found.push(venvPython(paths.
|
|
272
|
-
const marker = join(PACKAGE_ROOT, ".python-path");
|
|
273
|
-
if (isFile(marker)) {
|
|
274
|
-
const text = readFileSync(marker, "utf8").trim();
|
|
275
|
-
if (text.length > 0) found.push(text);
|
|
276
|
-
}
|
|
335
|
+
found.push(venvPython(paths.legacyVenv));
|
|
277
336
|
for (const name of ["python3", "python"]) found.push(name);
|
|
278
337
|
if (process.platform === "win32") {
|
|
279
338
|
for (const version of [
|
|
@@ -344,9 +403,20 @@ async function inspectRuntime(configFile = officePaths().configFile) {
|
|
|
344
403
|
if (expanded !== candidate && !isFile(expanded)) continue;
|
|
345
404
|
const inspection = await inspectPython(expanded);
|
|
346
405
|
seen.push(inspection);
|
|
347
|
-
if (inspection.compatible && inspection.missing.length === 0) return
|
|
406
|
+
if (inspection.compatible && inspection.missing.length === 0) return settledRuntime(seen, configFile);
|
|
348
407
|
}
|
|
349
|
-
return
|
|
408
|
+
return settledRuntime(seen, configFile);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Freeze the probe result and keep config.json honest about which interpreter won.
|
|
412
|
+
*
|
|
413
|
+
* A venv migrated out of the legacy data directory leaves the old absolute path
|
|
414
|
+
* recorded; rewriting it stops the next launch from probing a dead path first.
|
|
415
|
+
*/
|
|
416
|
+
function settledRuntime(seen, configFile) {
|
|
417
|
+
const status = selectRuntime(seen);
|
|
418
|
+
if (status.state === "ok" && status.python != null && status.python.path !== savedPython(configFile)) savePythonPath(configFile, status.python.path);
|
|
419
|
+
return status;
|
|
350
420
|
}
|
|
351
421
|
/** Run postinstall.mjs as a child process; resolve with an error summary or undefined. */
|
|
352
422
|
function runPostinstall() {
|
|
@@ -433,7 +503,7 @@ function parseSkill(raw, path) {
|
|
|
433
503
|
content: raw.slice(match[0].length).trim()
|
|
434
504
|
};
|
|
435
505
|
}
|
|
436
|
-
const KIT_MISSING = "the DSH LibreOffice kit (@deepseek-ai/libreoffice-kit) was not found:
|
|
506
|
+
const KIT_MISSING = "the DSH LibreOffice kit (@deepseek-ai/libreoffice-kit) was not found: the plugin records where the harness loaded it from in dsh-office-kit's config.json, and DSH_LIBREOFFICE_KIT_CLI overrides that for a plugin running outside the harness";
|
|
437
507
|
const runKit = promisify(execFile);
|
|
438
508
|
async function kitFacts() {
|
|
439
509
|
const cli = resolveKitCli();
|
|
@@ -495,10 +565,7 @@ function apply(ctx) {
|
|
|
495
565
|
const paths = officePaths();
|
|
496
566
|
const candidates = [];
|
|
497
567
|
const kitCli = resolveKitCli();
|
|
498
|
-
if (kitCli != null)
|
|
499
|
-
process.env.DSH_LIBREOFFICE_KIT_CLI ??= kitCli;
|
|
500
|
-
process.env.DSH_NODE ??= process.execPath;
|
|
501
|
-
}
|
|
568
|
+
if (kitCli != null) saveKitPath(paths.configFile, process.execPath, kitCli);
|
|
502
569
|
for (const feature of OFFICE_FEATURES) {
|
|
503
570
|
const directory = join(paths.skillsDir, feature.skill);
|
|
504
571
|
const path = join(directory, "SKILL.md");
|
|
@@ -542,12 +609,17 @@ function apply(ctx) {
|
|
|
542
609
|
}
|
|
543
610
|
const parsed = parseSkill(raw, path);
|
|
544
611
|
const runtime = await ensureRuntime(paths.configFile);
|
|
545
|
-
const
|
|
612
|
+
const kitCli = resolveKitCli();
|
|
613
|
+
const pythonLine = runtime.state === "ok" && runtime.python != null ? `\n\n## Office runtime\n\nVerified absolute paths. Use them for every helper command; quote paths that contain spaces.\n\n- \`python\` runs all helper scripts, \`cli\` is the Office helper CLI.\n- \`skill\` is this skill's directory: its scripts live under \`<skill>/scripts/\`.\n- \`shared\` holds the cross-format notes and shared scripts.\n- \`renderer\` is the DSH LibreOffice kit; the helper finds it itself, so never pass it.\n\n\`\`\`json\n${JSON.stringify({
|
|
546
614
|
python: runtime.python.path,
|
|
547
615
|
cli: paths.cli,
|
|
548
616
|
skill: dirname(path).split("\\").join("/"),
|
|
549
617
|
shared: join(paths.skillsDir, "shared").split("\\").join("/"),
|
|
550
|
-
version: runtime.python.version
|
|
618
|
+
version: runtime.python.version,
|
|
619
|
+
...kitCli == null ? {} : { renderer: {
|
|
620
|
+
kind: "libreoffice-kit",
|
|
621
|
+
cli: kitCli
|
|
622
|
+
} }
|
|
551
623
|
}, null, 2)}\n\`\`\`` : `\n\n## Office runtime\n\nNo working Python runtime is available: ${runtimeReason(runtime)}. Do not run helper commands or claim success. Tell the user to run \`${runtime.remedy}\`, then call \`office_status\` again.`;
|
|
552
624
|
const { rank: _rank, locator: _locator, ...summary } = candidate;
|
|
553
625
|
return {
|
|
@@ -598,6 +670,10 @@ function apply(ctx) {
|
|
|
598
670
|
path: paths.cli,
|
|
599
671
|
exists: cliExists()
|
|
600
672
|
},
|
|
673
|
+
config: {
|
|
674
|
+
path: paths.configFile,
|
|
675
|
+
exists: existsSync(paths.configFile)
|
|
676
|
+
},
|
|
601
677
|
packages: runtime.packages,
|
|
602
678
|
renderers: await kitFacts(),
|
|
603
679
|
skills: candidates.map((candidate) => candidate.name)
|
package/package.json
CHANGED
package/postinstall.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { spawnSync } from 'node:child_process'
|
|
19
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
20
20
|
import { homedir, tmpdir } from 'node:os'
|
|
21
21
|
import { dirname, join } from 'node:path'
|
|
22
22
|
import process from 'node:process'
|
|
@@ -35,21 +35,22 @@ const LEGACY_DATA_DIR = IS_WINDOWS
|
|
|
35
35
|
: join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'dsh-office-kit')
|
|
36
36
|
export const LEGACY_VENV_DIR = join(LEGACY_DATA_DIR, 'venv')
|
|
37
37
|
const LEGACY_VENV_PYTHON = join(LEGACY_VENV_DIR, IS_WINDOWS ? 'Scripts/python.exe' : 'bin/python')
|
|
38
|
-
// Must mirror `
|
|
39
|
-
// every runtime probe, so a
|
|
40
|
-
export const CONFIG_DIR =
|
|
41
|
-
? join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'dsh-office-kit')
|
|
42
|
-
: process.platform === 'darwin'
|
|
43
|
-
? join(homedir(), 'Library', 'Application Support', 'dsh-office-kit')
|
|
44
|
-
: join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'dsh-office-kit')
|
|
38
|
+
// Must mirror `officePaths()` in src/python.ts: the host half reads this file on
|
|
39
|
+
// every runtime probe, so a mismatch here makes a ready venv look missing.
|
|
40
|
+
export const CONFIG_DIR = join(homedir(), '.dsh', 'office')
|
|
45
41
|
export const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|
46
42
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
43
|
+
* Config files older versions wrote in the OS config directory. Read as
|
|
44
|
+
* fallbacks so an upgraded install is not rebuilt or mis-reported.
|
|
49
45
|
*/
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
const LEGACY_CONFIG_FILES = [...new Set([
|
|
47
|
+
IS_WINDOWS
|
|
48
|
+
? join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'dsh-office-kit', 'config.json')
|
|
49
|
+
: process.platform === 'darwin'
|
|
50
|
+
? join(homedir(), 'Library', 'Application Support', 'dsh-office-kit', 'config.json')
|
|
51
|
+
: join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'dsh-office-kit', 'config.json'),
|
|
52
|
+
join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'dsh-office-kit', 'config.json'),
|
|
53
|
+
])]
|
|
53
54
|
const REQUIREMENTS = join(PACKAGE_ROOT, 'python', 'requirements.txt')
|
|
54
55
|
const LOG_PREFIX = '[dsh-office-kit]'
|
|
55
56
|
|
|
@@ -230,7 +231,37 @@ function installPython() {
|
|
|
230
231
|
return true
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Move an upgraded install's venv out of the pre-0.4 data directory.
|
|
236
|
+
*
|
|
237
|
+
* Reusing the legacy venv in place kept the old layout alive forever, so the
|
|
238
|
+
* canonical path was only ever reached by fresh installs. A rename is instant
|
|
239
|
+
* and offline; if it fails (another volume, a locked interpreter) the legacy
|
|
240
|
+
* venv stays usable and the next install retries.
|
|
241
|
+
*/
|
|
242
|
+
export function migrateLegacyVenv(legacy = LEGACY_VENV_DIR, target = VENV_DIR) {
|
|
243
|
+
const interpreter = venvPythonIn(legacy)
|
|
244
|
+
if (existsSync(target) || !existsSync(interpreter)) {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
249
|
+
renameSync(legacy, target)
|
|
250
|
+
log(`moved the virtual environment from ${legacy} to ${target}`)
|
|
251
|
+
return true
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
warn(`could not move the legacy virtual environment: ${error instanceof Error ? error.message : String(error)}`)
|
|
255
|
+
return false
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function venvPythonIn(directory) {
|
|
260
|
+
return join(directory, IS_WINDOWS ? 'Scripts/python.exe' : 'bin/python')
|
|
261
|
+
}
|
|
262
|
+
|
|
233
263
|
function ensureVenv(base) {
|
|
264
|
+
migrateLegacyVenv()
|
|
234
265
|
const existing = workingVenv()
|
|
235
266
|
if (existing) {
|
|
236
267
|
if (existing.directory === LEGACY_VENV_DIR) {
|
|
@@ -284,18 +315,20 @@ function verifyImports(python) {
|
|
|
284
315
|
}
|
|
285
316
|
|
|
286
317
|
function readConfig() {
|
|
287
|
-
|
|
318
|
+
// Oldest first so the canonical file and the newest legacy record win key by key.
|
|
319
|
+
const merged = {}
|
|
320
|
+
for (const file of [...LEGACY_CONFIG_FILES].reverse().concat(CONFIG_FILE)) {
|
|
288
321
|
if (!existsSync(file)) {
|
|
289
322
|
continue
|
|
290
323
|
}
|
|
291
324
|
try {
|
|
292
|
-
|
|
325
|
+
Object.assign(merged, JSON.parse(readFileSync(file, 'utf8')))
|
|
293
326
|
}
|
|
294
327
|
catch {
|
|
295
|
-
// A malformed file must not hide a usable
|
|
328
|
+
// A malformed file must not hide a usable one.
|
|
296
329
|
}
|
|
297
330
|
}
|
|
298
|
-
return
|
|
331
|
+
return merged
|
|
299
332
|
}
|
|
300
333
|
|
|
301
334
|
function writeConfig(config) {
|
|
@@ -314,6 +347,14 @@ function writeReadyConfig(existingConfig, python, basePython, report) {
|
|
|
314
347
|
updatedAt: new Date().toISOString(),
|
|
315
348
|
imports: report,
|
|
316
349
|
})
|
|
350
|
+
// The canonical file now holds everything the legacy files had; a pre-0.5 file
|
|
351
|
+
// that stays behind only makes "where is the config" ambiguous.
|
|
352
|
+
for (const file of LEGACY_CONFIG_FILES) {
|
|
353
|
+
try {
|
|
354
|
+
rmSync(file, { force: true })
|
|
355
|
+
}
|
|
356
|
+
catch {}
|
|
357
|
+
}
|
|
317
358
|
}
|
|
318
359
|
|
|
319
360
|
function main() {
|
package/python/office_cli.py
CHANGED
|
@@ -93,23 +93,86 @@ KIT_RELATIVE = Path("node_modules") / "@deepseek-ai" / "libreoffice-kit" / "lib"
|
|
|
93
93
|
#: Actionable explanation used whenever the kit cannot be located.
|
|
94
94
|
KIT_MISSING = (
|
|
95
95
|
"the LibreOffice renderer is not available: the DSH LibreOffice kit was not found. "
|
|
96
|
-
"
|
|
97
|
-
"
|
|
96
|
+
"The plugin records the path in dsh-office-kit's config.json when the harness loads it; "
|
|
97
|
+
"for a hand-run helper, set DSH_LIBREOFFICE_KIT_CLI to <kit>/lib/cli.js of "
|
|
98
98
|
"@deepseek-ai/libreoffice-kit"
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
def office_home() -> Path:
|
|
103
|
+
"""Everything this plugin owns lives under `~/.dsh/office` (mirrors src/python.ts)."""
|
|
104
|
+
return Path.home() / ".dsh" / "office"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def config_file() -> Path:
|
|
108
|
+
"""The config.json the host half and postinstall write."""
|
|
109
|
+
override = os.environ.get("DSH_OFFICE_CONFIG")
|
|
110
|
+
return Path(override).expanduser() if override else office_home() / "config.json"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def legacy_config_files() -> list[Path]:
|
|
114
|
+
"""Config files older versions wrote in the OS config directory, newest first."""
|
|
115
|
+
if sys.platform == "darwin":
|
|
116
|
+
primary = Path.home() / "Library" / "Application Support" / "dsh-office-kit" / "config.json"
|
|
117
|
+
elif sys.platform == "win32":
|
|
118
|
+
primary = Path(os.environ.get("APPDATA") or Path.home() / "AppData" / "Roaming") / "dsh-office-kit" / "config.json"
|
|
119
|
+
else:
|
|
120
|
+
primary = Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config") / "dsh-office-kit" / "config.json"
|
|
121
|
+
roaming = Path(os.environ.get("APPDATA") or Path.home() / "AppData" / "Roaming") / "dsh-office-kit" / "config.json"
|
|
122
|
+
return list(dict.fromkeys([primary, roaming]))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def read_config() -> dict[str, Any]:
|
|
126
|
+
"""Merged config: the canonical file wins key by key over older locations."""
|
|
127
|
+
merged: dict[str, Any] = {}
|
|
128
|
+
for file in [*reversed(legacy_config_files()), config_file()]:
|
|
129
|
+
try:
|
|
130
|
+
parsed = json.loads(file.read_text("utf-8"))
|
|
131
|
+
except (OSError, ValueError):
|
|
132
|
+
continue
|
|
133
|
+
if isinstance(parsed, dict):
|
|
134
|
+
merged.update(parsed)
|
|
135
|
+
return merged
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def saved_kit() -> tuple[str, str] | None:
|
|
139
|
+
"""Return (node, kit CLI) as recorded by the host half, or None.
|
|
140
|
+
|
|
141
|
+
This is how a profile install works: the kit lives in the harness tree, which
|
|
142
|
+
no walk-up from the plugin directory can reach, so the host half looks it up
|
|
143
|
+
while it loads and stores the result here for every later helper call.
|
|
144
|
+
"""
|
|
145
|
+
kit = read_config().get("libreofficeKit")
|
|
146
|
+
if not isinstance(kit, dict):
|
|
147
|
+
return None
|
|
148
|
+
cli = kit.get("cli")
|
|
149
|
+
if not isinstance(cli, str) or not Path(cli).is_file():
|
|
150
|
+
return None
|
|
151
|
+
node = kit.get("node")
|
|
152
|
+
if not isinstance(node, str) or not node:
|
|
153
|
+
node = os.environ.get("DSH_NODE") or shutil.which("node")
|
|
154
|
+
if not node:
|
|
155
|
+
return None
|
|
156
|
+
return node, cli
|
|
157
|
+
|
|
158
|
+
|
|
102
159
|
def find_kit() -> tuple[str, str] | None:
|
|
103
160
|
"""Return (node, kit CLI) for the bundled renderer, or None when it is absent.
|
|
104
161
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
162
|
+
Order: an explicit `DSH_LIBREOFFICE_KIT_CLI`, then the path the host half
|
|
163
|
+
recorded in config.json (the profile-install case), then a walk up from this
|
|
164
|
+
file, which covers a checkout or a plugin-local dependency.
|
|
108
165
|
"""
|
|
109
|
-
candidates: list[Path] = []
|
|
110
166
|
override = os.environ.get("DSH_LIBREOFFICE_KIT_CLI")
|
|
111
167
|
if override:
|
|
112
|
-
|
|
168
|
+
cli = Path(override).expanduser()
|
|
169
|
+
if cli.is_file():
|
|
170
|
+
node = os.environ.get("DSH_NODE") or shutil.which("node")
|
|
171
|
+
return (node, str(cli)) if node else None
|
|
172
|
+
recorded = saved_kit()
|
|
173
|
+
if recorded is not None:
|
|
174
|
+
return recorded
|
|
175
|
+
candidates: list[Path] = []
|
|
113
176
|
for root in (PACKAGE_ROOT, *PACKAGE_ROOT.parents):
|
|
114
177
|
candidates.append(root / KIT_RELATIVE)
|
|
115
178
|
cli = next((candidate for candidate in candidates if candidate.is_file()), None)
|