@lotics/cli 0.32.0 → 0.34.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 +27 -0
- package/dist/cli.js +673 -0
- package/dist/{src/dev → dev}/wrapper_page.d.ts +6 -4
- package/dist/{src/dev → dev}/wrapper_page.js +40 -10
- package/dist/docx.d.ts +2 -0
- package/dist/docx.js +341 -0
- package/dist/docx.test.js +145 -0
- package/dist/file_command_io.d.ts +12 -0
- package/dist/file_command_io.js +34 -0
- package/dist/src/cli.js +46757 -500
- package/dist/{src/starter_template.d.ts → starter_template.d.ts} +1 -1
- package/dist/{src/starter_template.js → starter_template.js} +1 -1
- package/dist/starter_template.test.d.ts +1 -0
- package/dist/xlsx.d.ts +2 -0
- package/dist/xlsx.js +489 -0
- package/dist/xlsx.test.d.ts +1 -0
- package/dist/xlsx.test.js +131 -0
- package/package.json +6 -2
- /package/dist/{src/app_commands.d.ts → app_commands.d.ts} +0 -0
- /package/dist/{src/app_commands.js → app_commands.js} +0 -0
- /package/dist/{src/app_commands.test.d.ts → app_commands.test.d.ts} +0 -0
- /package/dist/{src/app_commands.test.js → app_commands.test.js} +0 -0
- /package/dist/{src/args.d.ts → args.d.ts} +0 -0
- /package/dist/{src/args.js → args.js} +0 -0
- /package/dist/{src/args.test.d.ts → args.test.d.ts} +0 -0
- /package/dist/{src/args.test.js → args.test.js} +0 -0
- /package/dist/{src/cli.d.ts → cli.d.ts} +0 -0
- /package/dist/{src/client.d.ts → client.d.ts} +0 -0
- /package/dist/{src/client.js → client.js} +0 -0
- /package/dist/{src/config.d.ts → config.d.ts} +0 -0
- /package/dist/{src/config.js → config.js} +0 -0
- /package/dist/{src/config.test.d.ts → config.test.d.ts} +0 -0
- /package/dist/{src/config.test.js → config.test.js} +0 -0
- /package/dist/{src/dev → dev}/rpc_handler.d.ts +0 -0
- /package/dist/{src/dev → dev}/rpc_handler.js +0 -0
- /package/dist/{src/dev → dev}/server.d.ts +0 -0
- /package/dist/{src/dev → dev}/server.js +0 -0
- /package/dist/{src/starter_template.test.d.ts → docx.test.d.ts} +0 -0
- /package/dist/{src/generate_app_queries_dts.d.ts → generate_app_queries_dts.d.ts} +0 -0
- /package/dist/{src/generate_app_queries_dts.js → generate_app_queries_dts.js} +0 -0
- /package/dist/{src/generate_app_workflows_dts.d.ts → generate_app_workflows_dts.d.ts} +0 -0
- /package/dist/{src/generate_app_workflows_dts.js → generate_app_workflows_dts.js} +0 -0
- /package/dist/{src/starter_template.test.js → starter_template.test.js} +0 -0
- /package/dist/{src/version.d.ts → version.d.ts} +0 -0
- /package/dist/{src/version.js → version.js} +0 -0
|
@@ -40,7 +40,7 @@ export interface StarterFile {
|
|
|
40
40
|
* fall back here when the lookup fails.
|
|
41
41
|
*/
|
|
42
42
|
export declare const STARTER_FALLBACK_UI_VERSION = "1.8.0";
|
|
43
|
-
export declare const STARTER_FALLBACK_SDK_VERSION = "0.
|
|
43
|
+
export declare const STARTER_FALLBACK_SDK_VERSION = "0.11.0";
|
|
44
44
|
export declare function buildStarterTemplate(args: {
|
|
45
45
|
app_name: string;
|
|
46
46
|
app_id: string;
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* fall back here when the lookup fails.
|
|
37
37
|
*/
|
|
38
38
|
export const STARTER_FALLBACK_UI_VERSION = "1.8.0";
|
|
39
|
-
export const STARTER_FALLBACK_SDK_VERSION = "0.
|
|
39
|
+
export const STARTER_FALLBACK_SDK_VERSION = "0.11.0";
|
|
40
40
|
export function buildStarterTemplate(args) {
|
|
41
41
|
const uiVersion = args.ui_version ?? `^${STARTER_FALLBACK_UI_VERSION}`;
|
|
42
42
|
const sdkVersion = args.sdk_version ?? `^${STARTER_FALLBACK_SDK_VERSION}`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/xlsx.d.ts
ADDED
package/dist/xlsx.js
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { fail, writeFileAtomic } from "./file_command_io.js";
|
|
3
|
+
import { parseExcelBuffer } from "@lotics/xlsx/excel_parser";
|
|
4
|
+
import { unzipXlsx } from "@lotics/xlsx/ooxml_zip";
|
|
5
|
+
import { exportWorkbook } from "@lotics/xlsx/xlsx_writer";
|
|
6
|
+
import { loadWorkbookFromSnapshot, WorkbookModel, SheetModel, StyleRegistry } from "@lotics/xlsx/workbook_model";
|
|
7
|
+
import { FormulaEngine } from "@lotics/xlsx/formula_engine";
|
|
8
|
+
import { refToRowCol, rowColToRef } from "@lotics/xlsx/excel_utils";
|
|
9
|
+
import { setCellValueCommand, clearRangeCommand, mergeCellsCommand, unmergeCellsCommand, addSheetCommand, deleteSheetCommand, renameSheetCommand, insertRowsCommand, deleteRowsCommand, insertColsCommand, deleteColsCommand, setRangeStyleCommand, } from "@lotics/xlsx/spreadsheet_commands";
|
|
10
|
+
function loadFile(filePath) {
|
|
11
|
+
if (!fs.existsSync(filePath))
|
|
12
|
+
fail(`File not found: ${filePath}`);
|
|
13
|
+
const buffer = fs.readFileSync(filePath);
|
|
14
|
+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
15
|
+
const originalZip = unzipXlsx(arrayBuffer);
|
|
16
|
+
const parsed = parseExcelBuffer(arrayBuffer);
|
|
17
|
+
const workbook = loadWorkbookFromSnapshot(parsed);
|
|
18
|
+
return { workbook, originalZip };
|
|
19
|
+
}
|
|
20
|
+
function exportAndWrite(loaded, filePath) {
|
|
21
|
+
const bytes = exportWorkbook(loaded.workbook, loaded.originalZip);
|
|
22
|
+
writeFileAtomic(filePath, bytes);
|
|
23
|
+
}
|
|
24
|
+
function resolveSheetIndex(workbook, sheetName) {
|
|
25
|
+
const index = workbook.sheets.findIndex((s) => s.name === sheetName);
|
|
26
|
+
if (index < 0) {
|
|
27
|
+
const names = workbook.sheets.map((s) => `"${s.name}"`).join(", ");
|
|
28
|
+
fail(`Sheet not found: "${sheetName}". Available: ${names}`);
|
|
29
|
+
}
|
|
30
|
+
return index;
|
|
31
|
+
}
|
|
32
|
+
function parseRange(range) {
|
|
33
|
+
const [start, end] = range.includes(":") ? range.split(":") : [range, range];
|
|
34
|
+
const s = refToRowCol(start.toUpperCase());
|
|
35
|
+
const e = refToRowCol(end.toUpperCase());
|
|
36
|
+
if (!s || !e)
|
|
37
|
+
fail(`Invalid range: ${range}`);
|
|
38
|
+
return {
|
|
39
|
+
startRow: Math.min(s.row, e.row),
|
|
40
|
+
startCol: Math.min(s.col, e.col),
|
|
41
|
+
endRow: Math.max(s.row, e.row),
|
|
42
|
+
endCol: Math.max(s.col, e.col),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function parseInt10(s, name) {
|
|
46
|
+
if (s === undefined)
|
|
47
|
+
fail(`Missing ${name}`);
|
|
48
|
+
const n = parseInt(s, 10);
|
|
49
|
+
if (!Number.isFinite(n))
|
|
50
|
+
fail(`Invalid ${name}: ${s}`);
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
function lazyEngine(workbook) {
|
|
54
|
+
let engine;
|
|
55
|
+
return {
|
|
56
|
+
get() {
|
|
57
|
+
if (!engine) {
|
|
58
|
+
engine = new FormulaEngine(workbook);
|
|
59
|
+
engine.buildGraph();
|
|
60
|
+
}
|
|
61
|
+
return engine;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// JSON shape: ParsedSpreadsheet (read) and SimpleWorkbook (write)
|
|
67
|
+
// =============================================================================
|
|
68
|
+
function readToJson(filePath) {
|
|
69
|
+
const buffer = fs.readFileSync(filePath);
|
|
70
|
+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
71
|
+
const parsed = parseExcelBuffer(arrayBuffer);
|
|
72
|
+
return {
|
|
73
|
+
activeSheetIndex: parsed.activeSheetIndex,
|
|
74
|
+
sheets: parsed.sheets.map((s) => ({
|
|
75
|
+
name: s.name,
|
|
76
|
+
totalRowCount: s.totalRowCount,
|
|
77
|
+
truncated: s.truncated,
|
|
78
|
+
cells: cellsFromParsedRows(s.rows),
|
|
79
|
+
merges: s.mergedCells.map((m) => `${rowColToRef(m.startRow, m.startCol)}:${rowColToRef(m.endRow, m.endCol)}`),
|
|
80
|
+
freeze: s.freezePane ? { row: s.freezePane.frozenRows, col: s.freezePane.frozenCols } : undefined,
|
|
81
|
+
})),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function cellsFromParsedRows(rows) {
|
|
85
|
+
const out = {};
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
for (const cell of row.cells) {
|
|
88
|
+
const ref = rowColToRef(row.index, cell.column);
|
|
89
|
+
const value = cell.typedValue ?? cell.value;
|
|
90
|
+
const entry = { value };
|
|
91
|
+
if (cell.formula)
|
|
92
|
+
entry.formula = cell.formula;
|
|
93
|
+
out[ref] = entry;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function buildWorkbookFromJson(input) {
|
|
99
|
+
if (!input || !Array.isArray(input.sheets) || input.sheets.length === 0) {
|
|
100
|
+
fail("Workbook JSON must include a non-empty `sheets` array");
|
|
101
|
+
}
|
|
102
|
+
for (let i = 0; i < input.sheets.length; i++)
|
|
103
|
+
validateSimpleSheet(input.sheets[i], i);
|
|
104
|
+
const styles = new StyleRegistry();
|
|
105
|
+
const sheets = input.sheets.map((s) => new SheetModel(s.name, styles));
|
|
106
|
+
const workbook = new WorkbookModel({ sheets, activeSheetIndex: 0, styles });
|
|
107
|
+
for (let i = 0; i < input.sheets.length; i++) {
|
|
108
|
+
applySheetCells(workbook, i, input.sheets[i]);
|
|
109
|
+
}
|
|
110
|
+
return workbook;
|
|
111
|
+
}
|
|
112
|
+
function validateSimpleSheet(s, index) {
|
|
113
|
+
if (!s || typeof s !== "object")
|
|
114
|
+
fail(`sheets[${index}] must be an object`);
|
|
115
|
+
if (typeof s.name !== "string" || s.name === "")
|
|
116
|
+
fail(`sheets[${index}].name must be a non-empty string`);
|
|
117
|
+
if (s.cells !== undefined && (typeof s.cells !== "object" || s.cells === null || Array.isArray(s.cells))) {
|
|
118
|
+
fail(`sheets[${index}].cells must be an object keyed by cell ref (e.g. {"A1": "value"})`);
|
|
119
|
+
}
|
|
120
|
+
if (s.merges !== undefined && !Array.isArray(s.merges)) {
|
|
121
|
+
fail(`sheets[${index}].merges must be an array of ranges (e.g. ["A1:B2"])`);
|
|
122
|
+
}
|
|
123
|
+
if (s.freeze !== undefined) {
|
|
124
|
+
if (typeof s.freeze !== "object" || s.freeze === null)
|
|
125
|
+
fail(`sheets[${index}].freeze must be {row, col}`);
|
|
126
|
+
if (typeof s.freeze.row !== "number" || typeof s.freeze.col !== "number") {
|
|
127
|
+
fail(`sheets[${index}].freeze.row and .col must be numbers`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function applySheetCells(workbook, sheetIndex, def) {
|
|
132
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
133
|
+
if (def.cells) {
|
|
134
|
+
for (const [ref, raw] of Object.entries(def.cells)) {
|
|
135
|
+
const cell = normalizeSimpleCell(raw);
|
|
136
|
+
const coord = refToRowCol(ref.toUpperCase());
|
|
137
|
+
if (!coord)
|
|
138
|
+
fail(`Invalid cell ref in sheet "${def.name}": ${ref}`);
|
|
139
|
+
const stringValue = cell.formula ? `=${cell.formula}` : valueToInput(cell.value);
|
|
140
|
+
const cmd = setCellValueCommand(workbook, undefined, sheetIndex, coord.row, coord.col, stringValue);
|
|
141
|
+
cmd.execute();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (def.merges) {
|
|
145
|
+
for (const range of def.merges) {
|
|
146
|
+
const r = parseRange(range);
|
|
147
|
+
const cmd = mergeCellsCommand(workbook, sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol);
|
|
148
|
+
cmd.execute();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (def.freeze) {
|
|
152
|
+
sheet.freeze = { row: def.freeze.row, col: def.freeze.col };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function normalizeSimpleCell(raw) {
|
|
156
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
157
|
+
return raw;
|
|
158
|
+
}
|
|
159
|
+
return { value: raw };
|
|
160
|
+
}
|
|
161
|
+
function valueToInput(value) {
|
|
162
|
+
if (value === null || value === undefined)
|
|
163
|
+
return "";
|
|
164
|
+
return String(value);
|
|
165
|
+
}
|
|
166
|
+
// =============================================================================
|
|
167
|
+
// Subcommand handlers
|
|
168
|
+
// =============================================================================
|
|
169
|
+
function xlsxRead(filePath) {
|
|
170
|
+
if (!filePath)
|
|
171
|
+
fail("Usage: lotics xlsx read <file>");
|
|
172
|
+
const data = readToJson(filePath);
|
|
173
|
+
console.log(JSON.stringify(data, null, 2));
|
|
174
|
+
}
|
|
175
|
+
function xlsxWrite(filePath, json) {
|
|
176
|
+
if (!filePath || !json)
|
|
177
|
+
fail("Usage: lotics xlsx write <file> '<json>'");
|
|
178
|
+
let parsed;
|
|
179
|
+
try {
|
|
180
|
+
parsed = JSON.parse(json);
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
fail(`Invalid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
184
|
+
}
|
|
185
|
+
const workbook = buildWorkbookFromJson(parsed);
|
|
186
|
+
const bytes = exportWorkbook(workbook);
|
|
187
|
+
writeFileAtomic(filePath, bytes);
|
|
188
|
+
}
|
|
189
|
+
function parseSheetRef(spec) {
|
|
190
|
+
const idx = spec.indexOf("!");
|
|
191
|
+
if (idx < 0)
|
|
192
|
+
fail(`Expected <sheet>!<ref>, got: ${spec}`);
|
|
193
|
+
return { sheet: spec.slice(0, idx), ref: spec.slice(idx + 1) };
|
|
194
|
+
}
|
|
195
|
+
function xlsxSetCell(filePath, rest) {
|
|
196
|
+
const [sheetRef, value] = rest;
|
|
197
|
+
if (!filePath || !sheetRef || value === undefined) {
|
|
198
|
+
fail("Usage: lotics xlsx set-cell <file> <sheet>!<ref> '<value>'");
|
|
199
|
+
}
|
|
200
|
+
const { sheet, ref } = parseSheetRef(sheetRef);
|
|
201
|
+
const coord = refToRowCol(ref.toUpperCase());
|
|
202
|
+
if (!coord)
|
|
203
|
+
fail(`Invalid cell ref: ${ref}`);
|
|
204
|
+
const loaded = loadFile(filePath);
|
|
205
|
+
const sheetIndex = resolveSheetIndex(loaded.workbook, sheet);
|
|
206
|
+
const engine = lazyEngine(loaded.workbook);
|
|
207
|
+
const cmd = setCellValueCommand(loaded.workbook, engine.get(), sheetIndex, coord.row, coord.col, value);
|
|
208
|
+
cmd.execute();
|
|
209
|
+
exportAndWrite(loaded, filePath);
|
|
210
|
+
}
|
|
211
|
+
function xlsxClearRange(filePath, rest) {
|
|
212
|
+
const [sheetRef] = rest;
|
|
213
|
+
if (!filePath || !sheetRef)
|
|
214
|
+
fail("Usage: lotics xlsx clear-range <file> <sheet>!<range>");
|
|
215
|
+
const { sheet, ref } = parseSheetRef(sheetRef);
|
|
216
|
+
const r = parseRange(ref);
|
|
217
|
+
const loaded = loadFile(filePath);
|
|
218
|
+
const sheetIndex = resolveSheetIndex(loaded.workbook, sheet);
|
|
219
|
+
const engine = lazyEngine(loaded.workbook);
|
|
220
|
+
const cmd = clearRangeCommand(loaded.workbook, engine.get(), sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol);
|
|
221
|
+
cmd.execute();
|
|
222
|
+
exportAndWrite(loaded, filePath);
|
|
223
|
+
}
|
|
224
|
+
function xlsxMerge(filePath, rest) {
|
|
225
|
+
const [sheetRef] = rest;
|
|
226
|
+
if (!filePath || !sheetRef)
|
|
227
|
+
fail("Usage: lotics xlsx merge <file> <sheet>!<range>");
|
|
228
|
+
const { sheet, ref } = parseSheetRef(sheetRef);
|
|
229
|
+
const r = parseRange(ref);
|
|
230
|
+
const loaded = loadFile(filePath);
|
|
231
|
+
const sheetIndex = resolveSheetIndex(loaded.workbook, sheet);
|
|
232
|
+
const cmd = mergeCellsCommand(loaded.workbook, sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol);
|
|
233
|
+
cmd.execute();
|
|
234
|
+
exportAndWrite(loaded, filePath);
|
|
235
|
+
}
|
|
236
|
+
function xlsxUnmerge(filePath, rest) {
|
|
237
|
+
const [sheetRef] = rest;
|
|
238
|
+
if (!filePath || !sheetRef)
|
|
239
|
+
fail("Usage: lotics xlsx unmerge <file> <sheet>!<range>");
|
|
240
|
+
const { sheet, ref } = parseSheetRef(sheetRef);
|
|
241
|
+
const r = parseRange(ref);
|
|
242
|
+
const mergeRef = `${rowColToRef(r.startRow, r.startCol)}:${rowColToRef(r.endRow, r.endCol)}`;
|
|
243
|
+
const loaded = loadFile(filePath);
|
|
244
|
+
const sheetIndex = resolveSheetIndex(loaded.workbook, sheet);
|
|
245
|
+
const cmd = unmergeCellsCommand(loaded.workbook, sheetIndex, mergeRef);
|
|
246
|
+
cmd.execute();
|
|
247
|
+
exportAndWrite(loaded, filePath);
|
|
248
|
+
}
|
|
249
|
+
function xlsxAddSheet(filePath, rest) {
|
|
250
|
+
const [name] = rest;
|
|
251
|
+
if (!filePath || !name)
|
|
252
|
+
fail("Usage: lotics xlsx add-sheet <file> <name>");
|
|
253
|
+
const loaded = loadFile(filePath);
|
|
254
|
+
const cmd = addSheetCommand(loaded.workbook, name);
|
|
255
|
+
cmd.execute();
|
|
256
|
+
exportAndWrite(loaded, filePath);
|
|
257
|
+
}
|
|
258
|
+
function xlsxDeleteSheet(filePath, rest) {
|
|
259
|
+
const [name] = rest;
|
|
260
|
+
if (!filePath || !name)
|
|
261
|
+
fail("Usage: lotics xlsx delete-sheet <file> <name>");
|
|
262
|
+
const loaded = loadFile(filePath);
|
|
263
|
+
const sheetIndex = resolveSheetIndex(loaded.workbook, name);
|
|
264
|
+
const cmd = deleteSheetCommand(loaded.workbook, sheetIndex);
|
|
265
|
+
cmd.execute();
|
|
266
|
+
exportAndWrite(loaded, filePath);
|
|
267
|
+
}
|
|
268
|
+
function xlsxRenameSheet(filePath, rest) {
|
|
269
|
+
const [oldName, newName] = rest;
|
|
270
|
+
if (!filePath || !oldName || !newName)
|
|
271
|
+
fail("Usage: lotics xlsx rename-sheet <file> <old> <new>");
|
|
272
|
+
const loaded = loadFile(filePath);
|
|
273
|
+
const sheetIndex = resolveSheetIndex(loaded.workbook, oldName);
|
|
274
|
+
const cmd = renameSheetCommand(loaded.workbook, sheetIndex, newName);
|
|
275
|
+
cmd.execute();
|
|
276
|
+
exportAndWrite(loaded, filePath);
|
|
277
|
+
}
|
|
278
|
+
function xlsxInsertRows(filePath, rest) {
|
|
279
|
+
const [sheetName, atStr, countStr] = rest;
|
|
280
|
+
if (!filePath || !sheetName)
|
|
281
|
+
fail("Usage: lotics xlsx insert-rows <file> <sheet> <at> <count>");
|
|
282
|
+
const at = parseInt10(atStr, "at");
|
|
283
|
+
const count = parseInt10(countStr, "count");
|
|
284
|
+
const loaded = loadFile(filePath);
|
|
285
|
+
const sheetIndex = resolveSheetIndex(loaded.workbook, sheetName);
|
|
286
|
+
const cmd = insertRowsCommand(loaded.workbook, sheetIndex, at, count);
|
|
287
|
+
cmd.execute();
|
|
288
|
+
exportAndWrite(loaded, filePath);
|
|
289
|
+
}
|
|
290
|
+
function xlsxDeleteRows(filePath, rest) {
|
|
291
|
+
const [sheetName, atStr, countStr] = rest;
|
|
292
|
+
if (!filePath || !sheetName)
|
|
293
|
+
fail("Usage: lotics xlsx delete-rows <file> <sheet> <at> <count>");
|
|
294
|
+
const at = parseInt10(atStr, "at");
|
|
295
|
+
const count = parseInt10(countStr, "count");
|
|
296
|
+
const loaded = loadFile(filePath);
|
|
297
|
+
const sheetIndex = resolveSheetIndex(loaded.workbook, sheetName);
|
|
298
|
+
const cmd = deleteRowsCommand(loaded.workbook, sheetIndex, at, count);
|
|
299
|
+
cmd.execute();
|
|
300
|
+
exportAndWrite(loaded, filePath);
|
|
301
|
+
}
|
|
302
|
+
function xlsxInsertCols(filePath, rest) {
|
|
303
|
+
const [sheetName, atStr, countStr] = rest;
|
|
304
|
+
if (!filePath || !sheetName)
|
|
305
|
+
fail("Usage: lotics xlsx insert-cols <file> <sheet> <at> <count>");
|
|
306
|
+
const at = parseInt10(atStr, "at");
|
|
307
|
+
const count = parseInt10(countStr, "count");
|
|
308
|
+
const loaded = loadFile(filePath);
|
|
309
|
+
const sheetIndex = resolveSheetIndex(loaded.workbook, sheetName);
|
|
310
|
+
const cmd = insertColsCommand(loaded.workbook, sheetIndex, at, count);
|
|
311
|
+
cmd.execute();
|
|
312
|
+
exportAndWrite(loaded, filePath);
|
|
313
|
+
}
|
|
314
|
+
function xlsxDeleteCols(filePath, rest) {
|
|
315
|
+
const [sheetName, atStr, countStr] = rest;
|
|
316
|
+
if (!filePath || !sheetName)
|
|
317
|
+
fail("Usage: lotics xlsx delete-cols <file> <sheet> <at> <count>");
|
|
318
|
+
const at = parseInt10(atStr, "at");
|
|
319
|
+
const count = parseInt10(countStr, "count");
|
|
320
|
+
const loaded = loadFile(filePath);
|
|
321
|
+
const sheetIndex = resolveSheetIndex(loaded.workbook, sheetName);
|
|
322
|
+
const cmd = deleteColsCommand(loaded.workbook, sheetIndex, at, count);
|
|
323
|
+
cmd.execute();
|
|
324
|
+
exportAndWrite(loaded, filePath);
|
|
325
|
+
}
|
|
326
|
+
function xlsxSetStyle(filePath, rest) {
|
|
327
|
+
const [sheetRef, styleJson] = rest;
|
|
328
|
+
if (!filePath || !sheetRef || !styleJson) {
|
|
329
|
+
fail("Usage: lotics xlsx set-style <file> <sheet>!<range> '<style-json>'");
|
|
330
|
+
}
|
|
331
|
+
const { sheet, ref } = parseSheetRef(sheetRef);
|
|
332
|
+
const r = parseRange(ref);
|
|
333
|
+
let style;
|
|
334
|
+
try {
|
|
335
|
+
style = JSON.parse(styleJson);
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
fail(`Invalid style JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
339
|
+
}
|
|
340
|
+
const loaded = loadFile(filePath);
|
|
341
|
+
const sheetIndex = resolveSheetIndex(loaded.workbook, sheet);
|
|
342
|
+
const cmd = setRangeStyleCommand(loaded.workbook, sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol, style);
|
|
343
|
+
cmd.execute();
|
|
344
|
+
exportAndWrite(loaded, filePath);
|
|
345
|
+
}
|
|
346
|
+
function xlsxBatch(filePath, json) {
|
|
347
|
+
if (!filePath || !json)
|
|
348
|
+
fail("Usage: lotics xlsx batch <file> '<json-array-of-ops>'");
|
|
349
|
+
let ops;
|
|
350
|
+
try {
|
|
351
|
+
ops = JSON.parse(json);
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
fail(`Invalid batch JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
355
|
+
}
|
|
356
|
+
if (!Array.isArray(ops))
|
|
357
|
+
fail("Batch must be a JSON array of ops");
|
|
358
|
+
for (let i = 0; i < ops.length; i++) {
|
|
359
|
+
const op = ops[i];
|
|
360
|
+
if (!op || typeof op !== "object" || typeof op.op !== "string") {
|
|
361
|
+
fail(`[op ${i}] each op must be an object with an "op" string field`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const loaded = loadFile(filePath);
|
|
365
|
+
const engine = lazyEngine(loaded.workbook);
|
|
366
|
+
for (let i = 0; i < ops.length; i++) {
|
|
367
|
+
const op = ops[i];
|
|
368
|
+
const cmd = buildBatchCommand(loaded.workbook, engine, op, i);
|
|
369
|
+
cmd.execute();
|
|
370
|
+
}
|
|
371
|
+
exportAndWrite(loaded, filePath);
|
|
372
|
+
}
|
|
373
|
+
function buildBatchCommand(workbook, engine, op, index) {
|
|
374
|
+
switch (op.op) {
|
|
375
|
+
case "set-cell": {
|
|
376
|
+
const sheetIndex = resolveSheetIndex(workbook, op.sheet);
|
|
377
|
+
const coord = refToRowCol(op.ref.toUpperCase());
|
|
378
|
+
if (!coord)
|
|
379
|
+
fail(`[op ${index}] Invalid cell ref: ${op.ref}`);
|
|
380
|
+
return setCellValueCommand(workbook, engine.get(), sheetIndex, coord.row, coord.col, op.value);
|
|
381
|
+
}
|
|
382
|
+
case "clear-range": {
|
|
383
|
+
const sheetIndex = resolveSheetIndex(workbook, op.sheet);
|
|
384
|
+
const r = parseRange(op.range);
|
|
385
|
+
return clearRangeCommand(workbook, engine.get(), sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol);
|
|
386
|
+
}
|
|
387
|
+
case "merge": {
|
|
388
|
+
const sheetIndex = resolveSheetIndex(workbook, op.sheet);
|
|
389
|
+
const r = parseRange(op.range);
|
|
390
|
+
return mergeCellsCommand(workbook, sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol);
|
|
391
|
+
}
|
|
392
|
+
case "unmerge": {
|
|
393
|
+
const sheetIndex = resolveSheetIndex(workbook, op.sheet);
|
|
394
|
+
const r = parseRange(op.range);
|
|
395
|
+
const mergeRef = `${rowColToRef(r.startRow, r.startCol)}:${rowColToRef(r.endRow, r.endCol)}`;
|
|
396
|
+
return unmergeCellsCommand(workbook, sheetIndex, mergeRef);
|
|
397
|
+
}
|
|
398
|
+
case "add-sheet":
|
|
399
|
+
return addSheetCommand(workbook, op.name);
|
|
400
|
+
case "delete-sheet":
|
|
401
|
+
return deleteSheetCommand(workbook, resolveSheetIndex(workbook, op.name));
|
|
402
|
+
case "rename-sheet":
|
|
403
|
+
return renameSheetCommand(workbook, resolveSheetIndex(workbook, op.old), op.new);
|
|
404
|
+
case "insert-rows":
|
|
405
|
+
return insertRowsCommand(workbook, resolveSheetIndex(workbook, op.sheet), op.at, op.count);
|
|
406
|
+
case "delete-rows":
|
|
407
|
+
return deleteRowsCommand(workbook, resolveSheetIndex(workbook, op.sheet), op.at, op.count);
|
|
408
|
+
case "insert-cols":
|
|
409
|
+
return insertColsCommand(workbook, resolveSheetIndex(workbook, op.sheet), op.at, op.count);
|
|
410
|
+
case "delete-cols":
|
|
411
|
+
return deleteColsCommand(workbook, resolveSheetIndex(workbook, op.sheet), op.at, op.count);
|
|
412
|
+
case "set-style": {
|
|
413
|
+
const sheetIndex = resolveSheetIndex(workbook, op.sheet);
|
|
414
|
+
const r = parseRange(op.range);
|
|
415
|
+
return setRangeStyleCommand(workbook, sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol, op.style);
|
|
416
|
+
}
|
|
417
|
+
default: {
|
|
418
|
+
const exhaustive = op;
|
|
419
|
+
void exhaustive;
|
|
420
|
+
fail(`[op ${index}] Unknown op: ${JSON.stringify(op)}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// =============================================================================
|
|
425
|
+
// Dispatch + help
|
|
426
|
+
// =============================================================================
|
|
427
|
+
export function printXlsxHelp() {
|
|
428
|
+
console.error(`Lotics xlsx commands — manipulate .xlsx files in place.
|
|
429
|
+
Uses Lotics' own xlsx engine; round-trips faithfully with the Lotics editor and templates.
|
|
430
|
+
|
|
431
|
+
lotics xlsx read <file> Dump file as JSON (sheets, cells, merges, freeze)
|
|
432
|
+
lotics xlsx write <file> '<json>' Create .xlsx from {"sheets":[{"name","cells":{"A1":...}}]}
|
|
433
|
+
lotics xlsx set-cell <file> <sheet>!<ref> '<v>' Set cell value (prefix '=' for formula)
|
|
434
|
+
lotics xlsx clear-range <file> <sheet>!<range> Clear all cells in a range
|
|
435
|
+
lotics xlsx merge <file> <sheet>!<range> Merge a range into one cell
|
|
436
|
+
lotics xlsx unmerge <file> <sheet>!<range> Unmerge a previously merged range
|
|
437
|
+
lotics xlsx add-sheet <file> <name> Append a new sheet
|
|
438
|
+
lotics xlsx delete-sheet <file> <name> Remove a sheet
|
|
439
|
+
lotics xlsx rename-sheet <file> <old> <new> Rename a sheet
|
|
440
|
+
lotics xlsx insert-rows <file> <sheet> <at> <n> Insert <n> rows at 1-based position
|
|
441
|
+
lotics xlsx delete-rows <file> <sheet> <at> <n> Delete <n> rows starting at <at>
|
|
442
|
+
lotics xlsx insert-cols <file> <sheet> <at> <n> Insert <n> columns at 1-based position
|
|
443
|
+
lotics xlsx delete-cols <file> <sheet> <at> <n> Delete <n> columns starting at <at>
|
|
444
|
+
lotics xlsx set-style <file> <sheet>!<range> '<style-json>'
|
|
445
|
+
Apply partial CellStyle to range (fontBold, backgroundColor, ...)
|
|
446
|
+
lotics xlsx batch <file> '<json-array-of-ops>' Apply many ops in one parse/export cycle
|
|
447
|
+
|
|
448
|
+
Edit ops mutate the file atomically (temp file + rename).`);
|
|
449
|
+
}
|
|
450
|
+
export async function runXlsxCommand(subcommand, toolArgs, restArgs) {
|
|
451
|
+
switch (subcommand) {
|
|
452
|
+
case "read":
|
|
453
|
+
return xlsxRead(toolArgs);
|
|
454
|
+
case "write":
|
|
455
|
+
return xlsxWrite(toolArgs, restArgs[0]);
|
|
456
|
+
case "set-cell":
|
|
457
|
+
return xlsxSetCell(toolArgs, restArgs);
|
|
458
|
+
case "clear-range":
|
|
459
|
+
return xlsxClearRange(toolArgs, restArgs);
|
|
460
|
+
case "merge":
|
|
461
|
+
return xlsxMerge(toolArgs, restArgs);
|
|
462
|
+
case "unmerge":
|
|
463
|
+
return xlsxUnmerge(toolArgs, restArgs);
|
|
464
|
+
case "add-sheet":
|
|
465
|
+
return xlsxAddSheet(toolArgs, restArgs);
|
|
466
|
+
case "delete-sheet":
|
|
467
|
+
return xlsxDeleteSheet(toolArgs, restArgs);
|
|
468
|
+
case "rename-sheet":
|
|
469
|
+
return xlsxRenameSheet(toolArgs, restArgs);
|
|
470
|
+
case "insert-rows":
|
|
471
|
+
return xlsxInsertRows(toolArgs, restArgs);
|
|
472
|
+
case "delete-rows":
|
|
473
|
+
return xlsxDeleteRows(toolArgs, restArgs);
|
|
474
|
+
case "insert-cols":
|
|
475
|
+
return xlsxInsertCols(toolArgs, restArgs);
|
|
476
|
+
case "delete-cols":
|
|
477
|
+
return xlsxDeleteCols(toolArgs, restArgs);
|
|
478
|
+
case "set-style":
|
|
479
|
+
return xlsxSetStyle(toolArgs, restArgs);
|
|
480
|
+
case "batch":
|
|
481
|
+
return xlsxBatch(toolArgs, restArgs[0]);
|
|
482
|
+
default: {
|
|
483
|
+
if (subcommand)
|
|
484
|
+
console.error(`Unknown xlsx subcommand: ${subcommand}\n`);
|
|
485
|
+
printXlsxHelp();
|
|
486
|
+
process.exit(subcommand ? 1 : 0);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { parseExcelBuffer } from "@lotics/xlsx/excel_parser";
|
|
6
|
+
import { runXlsxCommand } from "./xlsx.js";
|
|
7
|
+
import { CliError } from "./file_command_io.js";
|
|
8
|
+
let tmpDir;
|
|
9
|
+
let file;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "xlsx-test-"));
|
|
12
|
+
file = path.join(tmpDir, "test.xlsx");
|
|
13
|
+
// Silence the "Wrote <path>" status line that handlers emit on writes.
|
|
14
|
+
vi.spyOn(console, "error").mockImplementation(() => { });
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
function parseFile(p) {
|
|
21
|
+
const bytes = fs.readFileSync(p);
|
|
22
|
+
const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
23
|
+
return parseExcelBuffer(ab);
|
|
24
|
+
}
|
|
25
|
+
async function writeFresh(json) {
|
|
26
|
+
await runXlsxCommand("write", file, [json]);
|
|
27
|
+
}
|
|
28
|
+
describe("xlsx write + read", () => {
|
|
29
|
+
it("round-trips cells, merges, and freeze", async () => {
|
|
30
|
+
await writeFresh(JSON.stringify({
|
|
31
|
+
sheets: [{
|
|
32
|
+
name: "S1",
|
|
33
|
+
cells: { A1: "hi", B1: 42, C1: true, D1: { formula: "A1" } },
|
|
34
|
+
merges: ["A1:B1"],
|
|
35
|
+
freeze: { row: 1, col: 1 },
|
|
36
|
+
}],
|
|
37
|
+
}));
|
|
38
|
+
const parsed = parseFile(file);
|
|
39
|
+
expect(parsed.sheets[0].name).toBe("S1");
|
|
40
|
+
expect(parsed.sheets[0].rows[0].cells.map((c) => c.value)).toEqual(["hi", "42", "TRUE", ""]);
|
|
41
|
+
expect(parsed.sheets[0].rows[0].cells[3].formula).toBe("A1");
|
|
42
|
+
expect(parsed.sheets[0].mergedCells).toHaveLength(1);
|
|
43
|
+
expect(parsed.sheets[0].freezePane).toEqual({ frozenRows: 1, frozenCols: 1 });
|
|
44
|
+
});
|
|
45
|
+
it("read dumps the file as JSON to stdout", async () => {
|
|
46
|
+
await writeFresh(JSON.stringify({ sheets: [{ name: "S1", cells: { A1: "hi" } }] }));
|
|
47
|
+
const stdout = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
48
|
+
await runXlsxCommand("read", file, []);
|
|
49
|
+
const printed = JSON.parse(stdout.mock.calls[0][0]);
|
|
50
|
+
expect(printed.sheets[0].name).toBe("S1");
|
|
51
|
+
expect(printed.sheets[0].cells.A1.value).toBe("hi");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("xlsx single-op mutations", () => {
|
|
55
|
+
beforeEach(async () => {
|
|
56
|
+
await writeFresh(JSON.stringify({ sheets: [{ name: "S1", cells: { A1: "hi" } }] }));
|
|
57
|
+
});
|
|
58
|
+
it("set-cell writes a literal", async () => {
|
|
59
|
+
await runXlsxCommand("set-cell", file, ["S1!B2", "world"]);
|
|
60
|
+
expect(parseFile(file).sheets[0].rows[1].cells[0].value).toBe("world");
|
|
61
|
+
});
|
|
62
|
+
it("set-cell writes a formula", async () => {
|
|
63
|
+
await runXlsxCommand("set-cell", file, ["S1!B1", "=A1"]);
|
|
64
|
+
expect(parseFile(file).sheets[0].rows[0].cells[1].formula).toBe("A1");
|
|
65
|
+
});
|
|
66
|
+
it("clear-range removes cells in the range", async () => {
|
|
67
|
+
await runXlsxCommand("set-cell", file, ["S1!B1", "x"]);
|
|
68
|
+
await runXlsxCommand("clear-range", file, ["S1!A1:B1"]);
|
|
69
|
+
expect(parseFile(file).sheets[0].rows).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
it("merge + unmerge round-trip", async () => {
|
|
72
|
+
await runXlsxCommand("merge", file, ["S1!A1:B1"]);
|
|
73
|
+
expect(parseFile(file).sheets[0].mergedCells).toHaveLength(1);
|
|
74
|
+
await runXlsxCommand("unmerge", file, ["S1!A1:B1"]);
|
|
75
|
+
expect(parseFile(file).sheets[0].mergedCells).toHaveLength(0);
|
|
76
|
+
});
|
|
77
|
+
it("add-sheet, rename-sheet, delete-sheet", async () => {
|
|
78
|
+
await runXlsxCommand("add-sheet", file, ["S2"]);
|
|
79
|
+
expect(parseFile(file).sheets.map((s) => s.name)).toEqual(["S1", "S2"]);
|
|
80
|
+
await runXlsxCommand("rename-sheet", file, ["S2", "Renamed"]);
|
|
81
|
+
expect(parseFile(file).sheets.map((s) => s.name)).toEqual(["S1", "Renamed"]);
|
|
82
|
+
await runXlsxCommand("delete-sheet", file, ["Renamed"]);
|
|
83
|
+
expect(parseFile(file).sheets.map((s) => s.name)).toEqual(["S1"]);
|
|
84
|
+
});
|
|
85
|
+
it("insert-rows shifts existing cells", async () => {
|
|
86
|
+
await runXlsxCommand("set-cell", file, ["S1!A2", "below"]);
|
|
87
|
+
await runXlsxCommand("insert-rows", file, ["S1", "1", "1"]);
|
|
88
|
+
// Original A1 ("hi") shifts to A2; original A2 ("below") shifts to A3.
|
|
89
|
+
const rows = parseFile(file).sheets[0].rows;
|
|
90
|
+
expect(rows.find((r) => r.index === 2)?.cells[0].value).toBe("hi");
|
|
91
|
+
expect(rows.find((r) => r.index === 3)?.cells[0].value).toBe("below");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("xlsx batch", () => {
|
|
95
|
+
it("applies multiple ops in one parse/export cycle", async () => {
|
|
96
|
+
await writeFresh(JSON.stringify({ sheets: [{ name: "S1", cells: { A1: "x" } }] }));
|
|
97
|
+
const ops = [
|
|
98
|
+
{ op: "set-cell", sheet: "S1", ref: "A2", value: "y" },
|
|
99
|
+
{ op: "merge", sheet: "S1", range: "A1:B1" },
|
|
100
|
+
{ op: "add-sheet", name: "S2" },
|
|
101
|
+
];
|
|
102
|
+
await runXlsxCommand("batch", file, [JSON.stringify(ops)]);
|
|
103
|
+
const parsed = parseFile(file);
|
|
104
|
+
expect(parsed.sheets.map((s) => s.name)).toEqual(["S1", "S2"]);
|
|
105
|
+
expect(parsed.sheets[0].mergedCells).toHaveLength(1);
|
|
106
|
+
expect(parsed.sheets[0].rows.find((r) => r.index === 2)?.cells[0].value).toBe("y");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe("xlsx error paths", () => {
|
|
110
|
+
it("fails on unknown sheet name", async () => {
|
|
111
|
+
await writeFresh(JSON.stringify({ sheets: [{ name: "S1" }] }));
|
|
112
|
+
await expect(runXlsxCommand("set-cell", file, ["Nope!A1", "x"])).rejects.toBeInstanceOf(CliError);
|
|
113
|
+
});
|
|
114
|
+
it("fails on invalid cell ref", async () => {
|
|
115
|
+
await writeFresh(JSON.stringify({ sheets: [{ name: "S1" }] }));
|
|
116
|
+
await expect(runXlsxCommand("set-cell", file, ["S1!ZZZ", "x"])).rejects.toBeInstanceOf(CliError);
|
|
117
|
+
});
|
|
118
|
+
it("fails on missing file", async () => {
|
|
119
|
+
await expect(runXlsxCommand("set-cell", path.join(tmpDir, "no.xlsx"), ["S1!A1", "x"])).rejects.toBeInstanceOf(CliError);
|
|
120
|
+
});
|
|
121
|
+
it("fails on invalid write JSON", async () => {
|
|
122
|
+
await expect(runXlsxCommand("write", file, ["{not json"])).rejects.toBeInstanceOf(CliError);
|
|
123
|
+
});
|
|
124
|
+
it("rejects write with non-object cells", async () => {
|
|
125
|
+
await expect(runXlsxCommand("write", file, [JSON.stringify({ sheets: [{ name: "S", cells: "bad" }] })])).rejects.toBeInstanceOf(CliError);
|
|
126
|
+
});
|
|
127
|
+
it("rejects batch op without an 'op' string", async () => {
|
|
128
|
+
await writeFresh(JSON.stringify({ sheets: [{ name: "S1" }] }));
|
|
129
|
+
await expect(runXlsxCommand("batch", file, [JSON.stringify([{ sheet: "S1" }])])).rejects.toBeInstanceOf(CliError);
|
|
130
|
+
});
|
|
131
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Lotics SDK and CLI for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,12 +14,16 @@
|
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "tsgo",
|
|
17
|
+
"build": "tsgo && node scripts/build_cli.mjs",
|
|
18
18
|
"typecheck": "tsgo --noEmit",
|
|
19
19
|
"test": "vitest run",
|
|
20
20
|
"prepublishOnly": "npm run build"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
+
"@lotics/docx": "*",
|
|
24
|
+
"@lotics/xlsx": "*",
|
|
25
|
+
"@types/node": "^22.15.21",
|
|
26
|
+
"esbuild": "^0.25.0",
|
|
23
27
|
"vitest": "^4.0.15"
|
|
24
28
|
},
|
|
25
29
|
"keywords": [
|
|
File without changes
|
|
File without changes
|