@saptools/sharepoint-excel 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,1365 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import process6 from "process";
5
+ import { Command } from "commander";
6
+
7
+ // src/cli/commands.ts
8
+ import process5 from "process";
9
+
10
+ // src/config/resolve.ts
11
+ import process3 from "process";
12
+
13
+ // src/credentials/profile-store.ts
14
+ import { chmod, mkdir, readFile, writeFile } from "fs/promises";
15
+ import { dirname } from "path";
16
+
17
+ // src/config/paths.ts
18
+ import { homedir } from "os";
19
+ import { join } from "path";
20
+ import process from "process";
21
+
22
+ // src/types.ts
23
+ var DEFAULT_PROFILE_NAME = "default";
24
+ var DEFAULT_AUTH_BASE = "https://login.microsoftonline.com";
25
+ var DEFAULT_GRAPH_BASE = "https://graph.microsoft.com/v1.0";
26
+ var ENV_TENANT = "SHAREPOINT_EXCEL_TENANT_ID";
27
+ var ENV_CLIENT_ID = "SHAREPOINT_EXCEL_CLIENT_ID";
28
+ var ENV_CLIENT_SECRET = "SHAREPOINT_EXCEL_CLIENT_SECRET";
29
+ var ENV_SITE = "SHAREPOINT_EXCEL_SITE";
30
+ var ENV_DRIVE = "SHAREPOINT_EXCEL_DRIVE";
31
+ var ENV_PROFILE = "SHAREPOINT_EXCEL_PROFILE";
32
+ var ENV_AUTH_BASE = "SHAREPOINT_EXCEL_AUTH_BASE";
33
+ var ENV_GRAPH_BASE = "SHAREPOINT_EXCEL_GRAPH_BASE";
34
+ var ENV_HOME = "SAPTOOLS_SHAREPOINT_EXCEL_HOME";
35
+ var ENV_ALLOW_PLAINTEXT = "SAPTOOLS_SHAREPOINT_EXCEL_ALLOW_PLAINTEXT";
36
+ var FALLBACK_ENV_TENANT = "SHAREPOINT_TENANT_ID";
37
+ var FALLBACK_ENV_CLIENT_ID = "SHAREPOINT_CLIENT_ID";
38
+ var FALLBACK_ENV_CLIENT_SECRET = "SHAREPOINT_CLIENT_SECRET";
39
+ var FALLBACK_ENV_SITE = "SHAREPOINT_SITE";
40
+ var FALLBACK_ENV_DRIVE = "SHAREPOINT_DRIVE";
41
+
42
+ // src/config/paths.ts
43
+ var SAPTOOLS_DIR_NAME = ".saptools";
44
+ var PACKAGE_DIR_NAME = "sharepoint-excel";
45
+ var PROFILES_FILENAME = "profiles.json";
46
+ var FILE_SECRETS_FILENAME = "secrets.json";
47
+ function packageDataDir(env = process.env) {
48
+ const override = env[ENV_HOME];
49
+ if (override !== void 0 && override.length > 0) {
50
+ return override;
51
+ }
52
+ return join(homedir(), SAPTOOLS_DIR_NAME, PACKAGE_DIR_NAME);
53
+ }
54
+ function profilesPath(env = process.env) {
55
+ return join(packageDataDir(env), PROFILES_FILENAME);
56
+ }
57
+ function fileSecretsPath(env = process.env) {
58
+ return join(packageDataDir(env), FILE_SECRETS_FILENAME);
59
+ }
60
+
61
+ // src/credentials/profile-store.ts
62
+ var PROFILE_FILE_MODE = 384;
63
+ var EMPTY_PROFILE_FILE = { version: 1, profiles: [] };
64
+ function isMissingFileError(err) {
65
+ return err instanceof Error && "code" in err && err.code === "ENOENT";
66
+ }
67
+ function isSecretStoreKind(value) {
68
+ return value === "keyring" || value === "file";
69
+ }
70
+ function toStoredProfile(value) {
71
+ if (typeof value !== "object" || value === null) {
72
+ return void 0;
73
+ }
74
+ const raw = value;
75
+ const name = raw["name"];
76
+ const tenantId = raw["tenantId"];
77
+ const clientId = raw["clientId"];
78
+ const site = raw["site"];
79
+ const drive = raw["drive"];
80
+ const secretStore = raw["secretStore"];
81
+ const updatedAt = raw["updatedAt"];
82
+ if (typeof name !== "string" || typeof tenantId !== "string" || typeof clientId !== "string" || typeof site !== "string" || typeof updatedAt !== "string" || !isSecretStoreKind(secretStore)) {
83
+ return void 0;
84
+ }
85
+ return {
86
+ name,
87
+ tenantId,
88
+ clientId,
89
+ site,
90
+ ...typeof drive === "string" ? { drive } : {},
91
+ secretStore,
92
+ updatedAt
93
+ };
94
+ }
95
+ function isStoredProfile(value) {
96
+ return value !== void 0;
97
+ }
98
+ async function readProfileFile(path) {
99
+ try {
100
+ const raw = await readFile(path, "utf8");
101
+ const parsed = JSON.parse(raw);
102
+ if (parsed.version !== 1 || !Array.isArray(parsed.profiles)) {
103
+ return EMPTY_PROFILE_FILE;
104
+ }
105
+ return { version: 1, profiles: parsed.profiles.map(toStoredProfile).filter(isStoredProfile) };
106
+ } catch (err) {
107
+ if (isMissingFileError(err)) {
108
+ return EMPTY_PROFILE_FILE;
109
+ }
110
+ throw err;
111
+ }
112
+ }
113
+ async function writeProfileFile(path, value) {
114
+ await mkdir(dirname(path), { recursive: true });
115
+ await writeFile(path, `${JSON.stringify(value, null, 2)}
116
+ `, {
117
+ encoding: "utf8",
118
+ mode: PROFILE_FILE_MODE
119
+ });
120
+ await chmod(path, PROFILE_FILE_MODE);
121
+ }
122
+ function createProfileStore(path = profilesPath()) {
123
+ return {
124
+ async readProfiles() {
125
+ return (await readProfileFile(path)).profiles;
126
+ },
127
+ async writeProfiles(profiles) {
128
+ await writeProfileFile(path, { version: 1, profiles });
129
+ }
130
+ };
131
+ }
132
+ function findProfile(profiles, name = DEFAULT_PROFILE_NAME) {
133
+ return profiles.find((profile) => profile.name === name);
134
+ }
135
+ async function upsertProfile(store, vault, input) {
136
+ const name = input.name ?? DEFAULT_PROFILE_NAME;
137
+ const stored = {
138
+ name,
139
+ tenantId: input.tenantId,
140
+ clientId: input.clientId,
141
+ site: input.site,
142
+ ...input.drive === void 0 || input.drive.length === 0 ? {} : { drive: input.drive },
143
+ secretStore: input.secretStore,
144
+ updatedAt: (input.updatedAt ?? /* @__PURE__ */ new Date()).toISOString()
145
+ };
146
+ const profiles = await store.readProfiles();
147
+ const nextProfiles = profiles.some((profile) => profile.name === name) ? profiles.map((profile) => profile.name === name ? stored : profile) : [...profiles, stored];
148
+ await vault.setSecret(name, input.clientSecret);
149
+ await store.writeProfiles(nextProfiles);
150
+ return stored;
151
+ }
152
+ async function removeProfile(store, vault, name = DEFAULT_PROFILE_NAME) {
153
+ const profiles = await store.readProfiles();
154
+ const nextProfiles = profiles.filter((profile) => profile.name !== name);
155
+ await vault.deleteSecret(name);
156
+ if (nextProfiles.length === profiles.length) {
157
+ return false;
158
+ }
159
+ await store.writeProfiles(nextProfiles);
160
+ return true;
161
+ }
162
+ async function redactProfile(profile, vault) {
163
+ const secret = await vault.getSecret(profile.name);
164
+ return {
165
+ ...profile,
166
+ clientId: `${profile.clientId.slice(0, 4)}...${profile.clientId.slice(-4)}`,
167
+ hasClientSecret: secret !== void 0
168
+ };
169
+ }
170
+
171
+ // src/credentials/secret-vault.ts
172
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
173
+ import { dirname as dirname2 } from "path";
174
+ import { Entry } from "@napi-rs/keyring";
175
+ var SERVICE_NAME = "saptools-sharepoint-excel";
176
+ var SECRET_FILE_MODE = 384;
177
+ var EMPTY_SECRET_FILE = { version: 1, entries: {} };
178
+ function isMissingFileError2(err) {
179
+ return err instanceof Error && "code" in err && err.code === "ENOENT";
180
+ }
181
+ async function readSecretFile(path) {
182
+ try {
183
+ const raw = await readFile2(path, "utf8");
184
+ const parsed = JSON.parse(raw);
185
+ if (parsed.version !== 1 || typeof parsed.entries !== "object" || parsed.entries === null) {
186
+ return EMPTY_SECRET_FILE;
187
+ }
188
+ const entries = {};
189
+ for (const [key, value] of Object.entries(parsed.entries)) {
190
+ if (typeof value === "string") {
191
+ entries[key] = value;
192
+ }
193
+ }
194
+ return { version: 1, entries };
195
+ } catch (err) {
196
+ if (isMissingFileError2(err)) {
197
+ return EMPTY_SECRET_FILE;
198
+ }
199
+ throw err;
200
+ }
201
+ }
202
+ async function writeSecretFile(path, value) {
203
+ await mkdir2(dirname2(path), { recursive: true });
204
+ await writeFile2(path, `${JSON.stringify(value, null, 2)}
205
+ `, {
206
+ encoding: "utf8",
207
+ mode: SECRET_FILE_MODE
208
+ });
209
+ await chmod2(path, SECRET_FILE_MODE);
210
+ }
211
+ function createKeyringSecretVault(serviceName = SERVICE_NAME) {
212
+ return {
213
+ getSecret(profileName) {
214
+ const password = new Entry(serviceName, profileName).getPassword();
215
+ return Promise.resolve(password === null || password.length === 0 ? void 0 : password);
216
+ },
217
+ setSecret(profileName, secret) {
218
+ new Entry(serviceName, profileName).setPassword(secret);
219
+ return Promise.resolve();
220
+ },
221
+ deleteSecret(profileName) {
222
+ try {
223
+ new Entry(serviceName, profileName).deletePassword();
224
+ } catch (err) {
225
+ if (err instanceof Error && /not found|no entry|missing/i.test(err.message)) {
226
+ return Promise.resolve();
227
+ }
228
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
229
+ }
230
+ return Promise.resolve();
231
+ }
232
+ };
233
+ }
234
+ function createFileSecretVault(path = fileSecretsPath()) {
235
+ return {
236
+ async getSecret(profileName) {
237
+ const file = await readSecretFile(path);
238
+ return file.entries[profileName];
239
+ },
240
+ async setSecret(profileName, secret) {
241
+ const file = await readSecretFile(path);
242
+ await writeSecretFile(path, {
243
+ version: 1,
244
+ entries: { ...file.entries, [profileName]: secret }
245
+ });
246
+ },
247
+ async deleteSecret(profileName) {
248
+ const file = await readSecretFile(path);
249
+ const remaining = Object.fromEntries(
250
+ Object.entries(file.entries).filter(([entryName]) => entryName !== profileName)
251
+ );
252
+ await writeSecretFile(path, { version: 1, entries: remaining });
253
+ }
254
+ };
255
+ }
256
+
257
+ // src/graph/client.ts
258
+ import process2 from "process";
259
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 503]);
260
+ var DEFAULT_MAX_RETRIES = 2;
261
+ var DEFAULT_BASE_DELAY_MS = 500;
262
+ function defaultSleep(ms) {
263
+ return new Promise((resolve) => {
264
+ setTimeout(resolve, ms);
265
+ });
266
+ }
267
+ function parseRetryAfter(header) {
268
+ if (header === null || header.length === 0) {
269
+ return void 0;
270
+ }
271
+ const seconds = Number.parseFloat(header);
272
+ if (Number.isFinite(seconds) && seconds >= 0) {
273
+ return Math.ceil(seconds * 1e3);
274
+ }
275
+ const dateMs = Date.parse(header);
276
+ return Number.isNaN(dateMs) ? void 0 : Math.max(0, dateMs - Date.now());
277
+ }
278
+ function resolveBaseUrl(options) {
279
+ if (options.baseUrl !== void 0 && options.baseUrl.length > 0) {
280
+ return options.baseUrl.replace(/\/+$/, "");
281
+ }
282
+ const fromEnv = (options.env ?? process2.env)[ENV_GRAPH_BASE];
283
+ return fromEnv === void 0 || fromEnv.length === 0 ? DEFAULT_GRAPH_BASE : fromEnv.replace(/\/+$/, "");
284
+ }
285
+ function resolveUrl(base, path) {
286
+ if (/^https?:\/\//i.test(path)) {
287
+ return path;
288
+ }
289
+ return `${base}${path.startsWith("/") ? path : `/${path}`}`;
290
+ }
291
+ var GraphHttpError = class extends Error {
292
+ status;
293
+ code;
294
+ detail;
295
+ constructor(status, code, detail) {
296
+ super(
297
+ `Microsoft Graph request failed (${status.toString()}${code === void 0 ? "" : ` ${code}`}): ${detail}`
298
+ );
299
+ this.name = "GraphHttpError";
300
+ this.status = status;
301
+ this.code = code;
302
+ this.detail = detail;
303
+ }
304
+ };
305
+ async function extractErrorDetail(response) {
306
+ const text = await response.text().catch(() => "");
307
+ if (text.length === 0) {
308
+ return { code: void 0, detail: response.statusText };
309
+ }
310
+ try {
311
+ const body = JSON.parse(text);
312
+ const code = typeof body.error?.code === "string" ? body.error.code : void 0;
313
+ const detail = typeof body.error?.message === "string" && body.error.message.length > 0 ? body.error.message : response.statusText;
314
+ return { code, detail };
315
+ } catch {
316
+ return { code: void 0, detail: text };
317
+ }
318
+ }
319
+ function buildInit(accessToken, options) {
320
+ const headers = {
321
+ Authorization: `Bearer ${accessToken}`,
322
+ Accept: "application/json",
323
+ ...options.headers
324
+ };
325
+ const init = { method: options.method ?? "GET", headers };
326
+ if (options.rawBody !== void 0) {
327
+ init.body = rawBodyToBodyInit(options.rawBody);
328
+ return init;
329
+ }
330
+ if (options.body !== void 0) {
331
+ headers["Content-Type"] = headers["Content-Type"] ?? "application/json";
332
+ init.body = typeof options.body === "string" ? options.body : JSON.stringify(options.body);
333
+ }
334
+ return init;
335
+ }
336
+ function rawBodyToBodyInit(rawBody) {
337
+ if (typeof rawBody === "string") {
338
+ return rawBody;
339
+ }
340
+ const copy = new ArrayBuffer(rawBody.byteLength);
341
+ new Uint8Array(copy).set(rawBody);
342
+ return copy;
343
+ }
344
+ async function discardBody(response) {
345
+ if (response.body === null) {
346
+ return;
347
+ }
348
+ await response.body.cancel().catch(() => {
349
+ });
350
+ }
351
+ function createGraphClient(options) {
352
+ const baseUrl = resolveBaseUrl(options);
353
+ const fetchFn = options.fetchFn ?? fetch;
354
+ const maxRetries = options.retry?.maxRetries ?? DEFAULT_MAX_RETRIES;
355
+ const baseDelayMs = options.retry?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
356
+ const sleepFn = options.retry?.sleepFn ?? defaultSleep;
357
+ async function execute(path, requestOptions) {
358
+ const url = resolveUrl(baseUrl, path);
359
+ const init = buildInit(options.accessToken, requestOptions);
360
+ let attempt = 0;
361
+ let response = await fetchFn(url, init);
362
+ while (!response.ok && RETRYABLE_STATUSES.has(response.status) && attempt < maxRetries) {
363
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after")) ?? baseDelayMs * 2 ** attempt;
364
+ await discardBody(response);
365
+ await sleepFn(retryAfterMs);
366
+ attempt += 1;
367
+ response = await fetchFn(url, init);
368
+ }
369
+ if (!response.ok) {
370
+ const { code, detail } = await extractErrorDetail(response);
371
+ throw new GraphHttpError(response.status, code, detail);
372
+ }
373
+ return response;
374
+ }
375
+ async function requestJson(path, requestOptions = {}) {
376
+ const response = await execute(path, requestOptions);
377
+ if (response.status === 204) {
378
+ return void 0;
379
+ }
380
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
381
+ if (!contentType.includes("application/json")) {
382
+ return void 0;
383
+ }
384
+ return await response.json();
385
+ }
386
+ async function requestBytes(path, requestOptions = {}) {
387
+ const response = await execute(path, requestOptions);
388
+ return new Uint8Array(await response.arrayBuffer());
389
+ }
390
+ async function requestNoContent(path, requestOptions = {}) {
391
+ await execute(path, requestOptions);
392
+ }
393
+ return { baseUrl, requestJson, requestBytes, requestNoContent };
394
+ }
395
+
396
+ // src/graph/site.ts
397
+ function asString(value, fallback = "") {
398
+ return typeof value === "string" ? value : fallback;
399
+ }
400
+ function stripQueryAndHash(value) {
401
+ const markerIndex = value.search(/[?#]/);
402
+ return markerIndex === -1 ? value : value.slice(0, markerIndex);
403
+ }
404
+ function decodeSitePath(sitePath, input) {
405
+ return sitePath.split("/").map((segment) => {
406
+ try {
407
+ return decodeURIComponent(segment);
408
+ } catch (err) {
409
+ throw new Error(`Invalid site reference "${input}". Site path has invalid encoding`, {
410
+ cause: err
411
+ });
412
+ }
413
+ }).join("/");
414
+ }
415
+ function parseSiteInput(trimmed) {
416
+ if (/^https?:\/\//i.test(trimmed)) {
417
+ const url = new URL(trimmed);
418
+ return { hostname: url.hostname, rawPath: url.pathname };
419
+ }
420
+ const withoutQuery = stripQueryAndHash(trimmed);
421
+ const firstSlash = withoutQuery.indexOf("/");
422
+ if (firstSlash === -1) {
423
+ throw new Error(`Invalid site reference "${trimmed}". Expected host/sites/<name> or full URL`);
424
+ }
425
+ return { hostname: withoutQuery.slice(0, firstSlash), rawPath: withoutQuery.slice(firstSlash + 1) };
426
+ }
427
+ function parseSiteRef(input) {
428
+ const trimmed = input.trim();
429
+ if (trimmed.length === 0) {
430
+ throw new Error("Site reference is empty");
431
+ }
432
+ const { hostname, rawPath } = parseSiteInput(trimmed);
433
+ const sitePath = decodeSitePath(rawPath.replace(/^\/+|\/+$/g, ""), trimmed);
434
+ if (hostname.length === 0 || sitePath.length === 0) {
435
+ throw new Error(`Invalid site reference "${trimmed}". Missing hostname or site path`);
436
+ }
437
+ return { hostname, sitePath };
438
+ }
439
+ function encodeSitePath(sitePath) {
440
+ return sitePath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
441
+ }
442
+ async function resolveSite(client, ref) {
443
+ const path = `/sites/${encodeURIComponent(ref.hostname)}:/${encodeSitePath(ref.sitePath)}`;
444
+ let raw;
445
+ try {
446
+ raw = await client.requestJson(path);
447
+ } catch (err) {
448
+ if (err instanceof GraphHttpError && err.status === 404) {
449
+ throw new Error(`SharePoint site not found at ${ref.hostname}/${ref.sitePath}`, {
450
+ cause: err
451
+ });
452
+ }
453
+ throw err;
454
+ }
455
+ const id = asString(raw.id);
456
+ if (id.length === 0) {
457
+ throw new Error(`Site response missing id for ${ref.hostname}/${ref.sitePath}`);
458
+ }
459
+ return {
460
+ id,
461
+ name: asString(raw.name, ref.sitePath),
462
+ displayName: asString(raw.displayName, asString(raw.name, ref.sitePath)),
463
+ webUrl: asString(raw.webUrl)
464
+ };
465
+ }
466
+
467
+ // src/config/resolve.ts
468
+ function pickEnv(env, primary, fallback) {
469
+ const value = env[primary] ?? env[fallback];
470
+ return value === void 0 || value.length === 0 ? void 0 : value;
471
+ }
472
+ function pickValue(override, envValue, profileValue) {
473
+ if (override !== void 0 && override.length > 0) {
474
+ return override;
475
+ }
476
+ return envValue ?? profileValue;
477
+ }
478
+ function vaultForProfile(profile, options) {
479
+ if (profile?.secretStore === "file") {
480
+ return options.fileVault ?? createFileSecretVault();
481
+ }
482
+ return options.keyringVault ?? createKeyringSecretVault();
483
+ }
484
+ async function resolveClientSecret(override, envValue, profile, options) {
485
+ if (override !== void 0 && override.length > 0) {
486
+ return override;
487
+ }
488
+ if (envValue !== void 0) {
489
+ return envValue;
490
+ }
491
+ return profile === void 0 ? void 0 : await vaultForProfile(profile, options).getSecret(profile.name);
492
+ }
493
+ function requireValue(value, humanName) {
494
+ if (value === void 0 || value.length === 0) {
495
+ throw new Error(`${humanName} is required (pass a flag, set env, or run config set)`);
496
+ }
497
+ return value;
498
+ }
499
+ async function resolveRuntime(options = {}) {
500
+ const env = options.env ?? process3.env;
501
+ const overrides = options.overrides ?? {};
502
+ const profileName = overrides.profile ?? env[ENV_PROFILE] ?? DEFAULT_PROFILE_NAME;
503
+ const store = options.profileStore ?? createProfileStore();
504
+ const profile = findProfile(await store.readProfiles(), profileName);
505
+ const tenantId = pickValue(
506
+ overrides.tenant,
507
+ pickEnv(env, ENV_TENANT, FALLBACK_ENV_TENANT),
508
+ profile?.tenantId
509
+ );
510
+ const clientId = pickValue(
511
+ overrides.clientId,
512
+ pickEnv(env, ENV_CLIENT_ID, FALLBACK_ENV_CLIENT_ID),
513
+ profile?.clientId
514
+ );
515
+ const site = pickValue(overrides.site, pickEnv(env, ENV_SITE, FALLBACK_ENV_SITE), profile?.site);
516
+ const drive = pickValue(overrides.drive, pickEnv(env, ENV_DRIVE, FALLBACK_ENV_DRIVE), profile?.drive);
517
+ const clientSecret = await resolveClientSecret(
518
+ overrides.clientSecret,
519
+ pickEnv(env, ENV_CLIENT_SECRET, FALLBACK_ENV_CLIENT_SECRET),
520
+ profile,
521
+ options
522
+ );
523
+ return {
524
+ target: {
525
+ credentials: {
526
+ tenantId: requireValue(tenantId, "Tenant ID"),
527
+ clientId: requireValue(clientId, "Client ID"),
528
+ clientSecret: requireValue(clientSecret, "Client secret")
529
+ },
530
+ site: parseSiteRef(requireValue(site, "SharePoint site"))
531
+ },
532
+ ...drive === void 0 ? {} : { drive },
533
+ profileName,
534
+ source: profile === void 0 ? "env" : "profile"
535
+ };
536
+ }
537
+ function parseSecretStoreKind(value) {
538
+ if (value === void 0 || value === "keyring") {
539
+ return "keyring";
540
+ }
541
+ if (value === "file") {
542
+ return "file";
543
+ }
544
+ throw new Error(`Invalid secret store "${value}". Expected keyring or file`);
545
+ }
546
+
547
+ // src/output/format.ts
548
+ function formatDriveList(drives) {
549
+ if (drives.length === 0) {
550
+ return "(no drives found)";
551
+ }
552
+ return drives.map((drive) => `- ${drive.name} [${drive.driveType}] (${drive.id})`).join("\n");
553
+ }
554
+ function formatTestResult(site, drives) {
555
+ return [
556
+ `Authenticated and resolved site: ${site.displayName} (${site.id})`,
557
+ `Document libraries: ${drives.length.toString()}`,
558
+ formatDriveList(drives)
559
+ ].join("\n");
560
+ }
561
+ function formatProfile(profile) {
562
+ return [
563
+ `Profile: ${profile.name}`,
564
+ `Tenant: ${profile.tenantId}`,
565
+ `Client: ${profile.clientId}`,
566
+ `Site: ${profile.site}`,
567
+ `Drive: ${profile.drive ?? "(first available)"}`,
568
+ `Secret store: ${profile.secretStore}`,
569
+ `Client secret: ${profile.hasClientSecret ? "stored" : "missing"}`
570
+ ].join("\n");
571
+ }
572
+ function formatCreateResult(result) {
573
+ return `Created ${result.path} in ${result.driveName} (${result.item.id})`;
574
+ }
575
+ function formatMutationResult(action, result) {
576
+ return `${action} ${result.path} in ${result.driveName}; sheet ${result.mutation.sheetName} now has ${result.mutation.rowCount.toString()} row(s)`;
577
+ }
578
+ function formatWorkbookRead(result) {
579
+ return formatWorkbookSheets(result.workbook);
580
+ }
581
+ function formatWorkbookSheets(workbook) {
582
+ return workbook.sheets.map((sheet) => `${sheet.name}: ${sheet.rowCount.toString()} row(s), ${sheet.columnCount.toString()} column(s)`).join("\n");
583
+ }
584
+
585
+ // src/auth/token.ts
586
+ import process4 from "process";
587
+ var DEFAULT_SCOPE = "https://graph.microsoft.com/.default";
588
+ function resolveAuthBase(options) {
589
+ if (options.authBase !== void 0 && options.authBase.length > 0) {
590
+ return options.authBase.replace(/\/+$/, "");
591
+ }
592
+ const fromEnv = (options.env ?? process4.env)[ENV_AUTH_BASE];
593
+ return fromEnv === void 0 || fromEnv.length === 0 ? DEFAULT_AUTH_BASE : fromEnv.replace(/\/+$/, "");
594
+ }
595
+ function assertString(value, field) {
596
+ if (typeof value !== "string" || value.length === 0) {
597
+ throw new Error(`Token response missing field: ${field}`);
598
+ }
599
+ return value;
600
+ }
601
+ function assertNumber(value, field) {
602
+ if (typeof value !== "number" || !Number.isFinite(value)) {
603
+ throw new Error(`Token response missing numeric field: ${field}`);
604
+ }
605
+ return value;
606
+ }
607
+ function parseTokenResponse(text, status) {
608
+ try {
609
+ return JSON.parse(text);
610
+ } catch (err) {
611
+ throw new Error(`Failed to parse token response (HTTP ${status.toString()})`, { cause: err });
612
+ }
613
+ }
614
+ function throwIfTokenError(response, parsed) {
615
+ if (response.ok && typeof parsed.error !== "string") {
616
+ return;
617
+ }
618
+ const code = typeof parsed.error === "string" ? parsed.error : "unknown_error";
619
+ const description = typeof parsed.error_description === "string" && parsed.error_description.length > 0 ? parsed.error_description : response.statusText;
620
+ throw new Error(
621
+ `Azure AD token request failed (HTTP ${response.status.toString()} ${code}): ${description}`
622
+ );
623
+ }
624
+ async function acquireAppToken(credentials, options = {}) {
625
+ if (credentials.tenantId.length === 0) {
626
+ throw new Error("tenantId is required");
627
+ }
628
+ if (credentials.clientId.length === 0) {
629
+ throw new Error("clientId is required");
630
+ }
631
+ if (credentials.clientSecret.length === 0) {
632
+ throw new Error("clientSecret is required");
633
+ }
634
+ const authBase = resolveAuthBase(options);
635
+ const fetchFn = options.fetchFn ?? fetch;
636
+ const url = `${authBase}/${encodeURIComponent(credentials.tenantId)}/oauth2/v2.0/token`;
637
+ const form = new URLSearchParams({
638
+ grant_type: "client_credentials",
639
+ client_id: credentials.clientId,
640
+ client_secret: credentials.clientSecret,
641
+ scope: options.scope ?? DEFAULT_SCOPE
642
+ });
643
+ const response = await fetchFn(url, {
644
+ method: "POST",
645
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
646
+ body: form.toString()
647
+ });
648
+ const parsed = parseTokenResponse(await response.text(), response.status);
649
+ throwIfTokenError(response, parsed);
650
+ const scopeValue = typeof parsed.scope === "string" ? parsed.scope : void 0;
651
+ const base = {
652
+ accessToken: assertString(parsed.access_token, "access_token"),
653
+ tokenType: assertString(parsed.token_type, "token_type"),
654
+ expiresOn: Math.floor(Date.now() / 1e3) + assertNumber(parsed.expires_in, "expires_in")
655
+ };
656
+ return scopeValue === void 0 ? base : { ...base, scope: scopeValue };
657
+ }
658
+
659
+ // src/graph/drive.ts
660
+ var XLSX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
661
+ function asString2(value, fallback = "") {
662
+ return typeof value === "string" ? value : fallback;
663
+ }
664
+ function asNumber(value, fallback = 0) {
665
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
666
+ }
667
+ function encodeDrivePath(relativePath) {
668
+ return relativePath.split("/").filter((segment) => segment.length > 0).map((segment) => encodeURIComponent(segment)).join("/");
669
+ }
670
+ function toDrive(raw) {
671
+ return {
672
+ id: asString2(raw.id),
673
+ name: asString2(raw.name),
674
+ driveType: asString2(raw.driveType, "documentLibrary"),
675
+ webUrl: asString2(raw.webUrl)
676
+ };
677
+ }
678
+ function toDriveItem(raw) {
679
+ const base = {
680
+ id: asString2(raw.id),
681
+ name: asString2(raw.name),
682
+ isFolder: raw.folder !== void 0 && raw.folder !== null,
683
+ size: asNumber(raw.size)
684
+ };
685
+ return {
686
+ ...base,
687
+ ...typeof raw.eTag === "string" ? { eTag: raw.eTag } : {},
688
+ ...typeof raw.cTag === "string" ? { cTag: raw.cTag } : {},
689
+ ...typeof raw.webUrl === "string" ? { webUrl: raw.webUrl } : {}
690
+ };
691
+ }
692
+ async function listDrives(client, siteId) {
693
+ const response = await client.requestJson(`/sites/${encodeURIComponent(siteId)}/drives`);
694
+ if (!Array.isArray(response.value)) {
695
+ return [];
696
+ }
697
+ return response.value.filter((entry) => typeof entry === "object" && entry !== null).map(toDrive);
698
+ }
699
+ function selectDrive(drives, driveHint) {
700
+ if (drives.length === 0) {
701
+ throw new Error("SharePoint site has no drives (document libraries)");
702
+ }
703
+ if (driveHint === void 0 || driveHint.length === 0) {
704
+ const first = drives[0];
705
+ if (first === void 0) {
706
+ throw new Error("No drives available");
707
+ }
708
+ return first;
709
+ }
710
+ const match = drives.find((drive) => drive.id === driveHint || drive.name === driveHint);
711
+ if (match === void 0) {
712
+ throw new Error(`Drive "${driveHint}" not found. Available: ${drives.map((d) => d.name).join(", ")}`);
713
+ }
714
+ return match;
715
+ }
716
+ async function getDriveItemByPath(client, driveId, relativePath) {
717
+ const normalized = relativePath.replace(/^\/+|\/+$/g, "");
718
+ const path = normalized.length === 0 ? `/drives/${encodeURIComponent(driveId)}/root` : `/drives/${encodeURIComponent(driveId)}/root:/${encodeDrivePath(normalized)}`;
719
+ try {
720
+ return toDriveItem(await client.requestJson(path));
721
+ } catch (err) {
722
+ if (err instanceof GraphHttpError && err.status === 404) {
723
+ return null;
724
+ }
725
+ throw err;
726
+ }
727
+ }
728
+ async function downloadDriveFile(client, driveId, relativePath) {
729
+ const encodedPath = encodeDrivePath(relativePath.replace(/^\/+|\/+$/g, ""));
730
+ return await client.requestBytes(`/drives/${encodeURIComponent(driveId)}/root:/${encodedPath}:/content`, {
731
+ headers: { Accept: XLSX_CONTENT_TYPE }
732
+ });
733
+ }
734
+ async function createUploadSession(client, driveId, relativePath) {
735
+ const encodedPath = encodeDrivePath(relativePath.replace(/^\/+|\/+$/g, ""));
736
+ const response = await client.requestJson(
737
+ `/drives/${encodeURIComponent(driveId)}/root:/${encodedPath}:/createUploadSession`,
738
+ {
739
+ method: "POST",
740
+ body: { item: { "@microsoft.graph.conflictBehavior": "fail" } }
741
+ }
742
+ );
743
+ if (typeof response.uploadUrl !== "string" || response.uploadUrl.length === 0) {
744
+ throw new Error("Graph upload session response missing uploadUrl");
745
+ }
746
+ return response.uploadUrl;
747
+ }
748
+ async function uploadNewDriveFile(client, driveId, relativePath, bytes) {
749
+ const existing = await getDriveItemByPath(client, driveId, relativePath);
750
+ if (existing !== null) {
751
+ throw new Error(`Refusing to overwrite existing SharePoint file: ${relativePath}`);
752
+ }
753
+ const uploadUrl = await createUploadSession(client, driveId, relativePath);
754
+ const lastByte = bytes.byteLength - 1;
755
+ const raw = await client.requestJson(uploadUrl, {
756
+ method: "PUT",
757
+ rawBody: bytes,
758
+ headers: {
759
+ "Content-Length": bytes.byteLength.toString(),
760
+ "Content-Range": `bytes 0-${lastByte.toString()}/${bytes.byteLength.toString()}`,
761
+ "Content-Type": XLSX_CONTENT_TYPE
762
+ }
763
+ });
764
+ return toDriveItem(raw);
765
+ }
766
+ async function replaceDriveFile(client, driveId, relativePath, eTag, bytes) {
767
+ const encodedPath = encodeDrivePath(relativePath.replace(/^\/+|\/+$/g, ""));
768
+ const raw = await client.requestJson(
769
+ `/drives/${encodeURIComponent(driveId)}/root:/${encodedPath}:/content`,
770
+ {
771
+ method: "PUT",
772
+ rawBody: bytes,
773
+ headers: {
774
+ "Content-Type": XLSX_CONTENT_TYPE,
775
+ "If-Match": eTag
776
+ }
777
+ }
778
+ );
779
+ return toDriveItem(raw);
780
+ }
781
+
782
+ // src/session.ts
783
+ async function openSession(target, options = {}) {
784
+ const tokenOptions = {
785
+ ...options.authBase === void 0 ? {} : { authBase: options.authBase },
786
+ ...options.fetchFn === void 0 ? {} : { fetchFn: options.fetchFn }
787
+ };
788
+ const token = await acquireAppToken(target.credentials, tokenOptions);
789
+ const client = createGraphClient({
790
+ accessToken: token.accessToken,
791
+ ...options.graphBase === void 0 ? {} : { baseUrl: options.graphBase },
792
+ ...options.fetchFn === void 0 ? {} : { fetchFn: options.fetchFn },
793
+ ...options.retry === void 0 ? {} : { retry: options.retry }
794
+ });
795
+ const site = await resolveSite(client, target.site);
796
+ const drives = await listDrives(client, site.id);
797
+ return { token, client, site, drives };
798
+ }
799
+
800
+ // src/workbook/json.ts
801
+ import { z } from "zod";
802
+ var JsonCellValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
803
+ var JsonRowSchema = z.array(JsonCellValueSchema);
804
+ var JsonRecordSchema = z.record(z.string(), JsonCellValueSchema);
805
+ function parseHeaders(input) {
806
+ if (input === void 0 || input.trim().length === 0) {
807
+ return [];
808
+ }
809
+ return input.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
810
+ }
811
+ function parseJson(input, label) {
812
+ try {
813
+ return JSON.parse(input);
814
+ } catch (err) {
815
+ throw new Error(`Invalid JSON for ${label}`, { cause: err });
816
+ }
817
+ }
818
+ function parseCellValue(input) {
819
+ const trimmed = input.trim();
820
+ if (trimmed.length === 0) {
821
+ return "";
822
+ }
823
+ let parsed;
824
+ try {
825
+ parsed = parseJson(trimmed, "cell value");
826
+ } catch {
827
+ return input;
828
+ }
829
+ const result = JsonCellValueSchema.safeParse(parsed);
830
+ if (!result.success) {
831
+ return input;
832
+ }
833
+ return result.data;
834
+ }
835
+ function parseWorkbookRows(input) {
836
+ if (input === void 0 || input.trim().length === 0) {
837
+ return [];
838
+ }
839
+ const parsed = parseJson(input, "rows");
840
+ const rowResult = JsonRowSchema.safeParse(parsed);
841
+ if (rowResult.success) {
842
+ return [rowResult.data];
843
+ }
844
+ const recordResult = JsonRecordSchema.safeParse(parsed);
845
+ if (recordResult.success) {
846
+ return [recordResult.data];
847
+ }
848
+ if (Array.isArray(parsed)) {
849
+ return parsed.map(parseWorkbookRow);
850
+ }
851
+ throw new Error(`Rows JSON must be an object, row array, or array of rows/objects`);
852
+ }
853
+ function parseWorkbookRow(value) {
854
+ const rowResult = JsonRowSchema.safeParse(value);
855
+ if (rowResult.success) {
856
+ return rowResult.data;
857
+ }
858
+ const recordResult = JsonRecordSchema.safeParse(value);
859
+ if (recordResult.success) {
860
+ return recordResult.data;
861
+ }
862
+ throw new Error(`Rows JSON must be an object, row array, or array of rows/objects`);
863
+ }
864
+
865
+ // src/workbook/excel.ts
866
+ import ExcelJS from "exceljs";
867
+
868
+ // src/workbook/a1.ts
869
+ var CELL_REF_PATTERN = /^([A-Za-z]+)([1-9]\d*)$/;
870
+ function columnNameToNumber(columnName) {
871
+ const normalized = columnName.trim().toUpperCase();
872
+ if (!/^[A-Z]+$/.test(normalized)) {
873
+ throw new Error(`Invalid column name "${columnName}"`);
874
+ }
875
+ let total = 0;
876
+ for (const char of normalized) {
877
+ total = total * 26 + char.charCodeAt(0) - 64;
878
+ }
879
+ return total;
880
+ }
881
+ function parseA1Cell(input) {
882
+ const trimmed = input.trim();
883
+ const match = CELL_REF_PATTERN.exec(trimmed);
884
+ if (match === null) {
885
+ throw new Error(`Invalid A1 cell reference "${input}"`);
886
+ }
887
+ const columnName = match[1];
888
+ const rowValue = match[2];
889
+ if (columnName === void 0 || rowValue === void 0) {
890
+ throw new Error(`Invalid A1 cell reference "${input}"`);
891
+ }
892
+ return {
893
+ row: Number.parseInt(rowValue, 10),
894
+ column: columnNameToNumber(columnName)
895
+ };
896
+ }
897
+ function orderRange(start, end) {
898
+ return {
899
+ start: {
900
+ row: Math.min(start.row, end.row),
901
+ column: Math.min(start.column, end.column)
902
+ },
903
+ end: {
904
+ row: Math.max(start.row, end.row),
905
+ column: Math.max(start.column, end.column)
906
+ }
907
+ };
908
+ }
909
+ function parseA1Range(input) {
910
+ const parts = input.split(":").map((part) => part.trim());
911
+ if (parts.length === 1) {
912
+ const cell = parseA1Cell(parts[0] ?? "");
913
+ return { start: cell, end: cell };
914
+ }
915
+ if (parts.length !== 2) {
916
+ throw new Error(`Invalid A1 range "${input}"`);
917
+ }
918
+ return orderRange(parseA1Cell(parts[0] ?? ""), parseA1Cell(parts[1] ?? ""));
919
+ }
920
+
921
+ // src/workbook/excel.ts
922
+ var INVALID_SHEET_CHARS = /[\][:*?/\\]/;
923
+ function validateSheetName(sheetName) {
924
+ const trimmed = sheetName.trim();
925
+ if (trimmed.length === 0 || trimmed.length > 31 || INVALID_SHEET_CHARS.test(trimmed)) {
926
+ throw new Error(`Invalid Excel sheet name "${sheetName}"`);
927
+ }
928
+ return trimmed;
929
+ }
930
+ function isRecord(row) {
931
+ return !Array.isArray(row);
932
+ }
933
+ function deriveHeaders(headers, rows) {
934
+ if (headers.length > 0) {
935
+ return headers;
936
+ }
937
+ const firstRecord = rows.find(isRecord);
938
+ return firstRecord === void 0 ? [] : Object.keys(firstRecord);
939
+ }
940
+ function rowToValues(row, headers) {
941
+ if (!isRecord(row)) {
942
+ return [...row];
943
+ }
944
+ const record = row;
945
+ if (headers.length === 0) {
946
+ return Object.keys(record).map((key) => record[key] ?? null);
947
+ }
948
+ return headers.map((header) => record[header] ?? null);
949
+ }
950
+ function addRows(sheet, headers, rows) {
951
+ if (headers.length > 0) {
952
+ sheet.addRow([...headers]);
953
+ }
954
+ for (const row of rows) {
955
+ sheet.addRow([...rowToValues(row, headers)]);
956
+ }
957
+ }
958
+ function addTable(sheet, tableName, headers, rows) {
959
+ sheet.addTable({
960
+ name: tableName,
961
+ ref: "A1",
962
+ headerRow: true,
963
+ totalsRow: false,
964
+ style: { theme: "TableStyleMedium2", showRowStripes: true },
965
+ columns: headers.map((name) => ({ name })),
966
+ rows: rows.map((row) => [...rowToValues(row, headers)])
967
+ });
968
+ }
969
+ async function workbookToBytes(workbook) {
970
+ const buffer = await workbook.xlsx.writeBuffer();
971
+ return new Uint8Array(buffer);
972
+ }
973
+ async function loadWorkbook(bytes) {
974
+ const workbook = new ExcelJS.Workbook();
975
+ await workbook.xlsx.load(uint8ArrayToArrayBuffer(bytes));
976
+ return workbook;
977
+ }
978
+ function uint8ArrayToArrayBuffer(bytes) {
979
+ const copy = new ArrayBuffer(bytes.byteLength);
980
+ new Uint8Array(copy).set(bytes);
981
+ return copy;
982
+ }
983
+ function getWorksheet(workbook, sheetName) {
984
+ const sheet = workbook.getWorksheet(sheetName);
985
+ if (sheet === void 0) {
986
+ throw new Error(`Sheet "${sheetName}" not found`);
987
+ }
988
+ return sheet;
989
+ }
990
+ function serializeCellValue(value) {
991
+ if (value === null || value === void 0) {
992
+ return null;
993
+ }
994
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
995
+ return value;
996
+ }
997
+ if (value instanceof Date) {
998
+ return value.toISOString();
999
+ }
1000
+ if (typeof value === "object" && "result" in value) {
1001
+ return serializeCellValue(value.result);
1002
+ }
1003
+ return JSON.stringify(value);
1004
+ }
1005
+ function readRow(sheet, rowNumber, startColumn, endColumn) {
1006
+ const row = sheet.getRow(rowNumber);
1007
+ const columnCount = endColumn - startColumn + 1;
1008
+ return Array.from(
1009
+ { length: columnCount },
1010
+ (_value, index) => serializeCellValue(row.getCell(startColumn + index).value)
1011
+ );
1012
+ }
1013
+ function readHeaderRow(sheet) {
1014
+ if (sheet.actualRowCount === 0) {
1015
+ return [];
1016
+ }
1017
+ return readRow(sheet, 1, 1, Math.max(1, sheet.actualColumnCount)).map((value) => String(value ?? ""));
1018
+ }
1019
+ function readSheet(sheet, options) {
1020
+ const range = options.range === void 0 ? void 0 : parseA1Range(options.range);
1021
+ const startRow = range?.start.row ?? 1;
1022
+ const endRow = range?.end.row ?? Math.max(1, sheet.actualRowCount);
1023
+ const startColumn = range?.start.column ?? 1;
1024
+ const endColumn = range?.end.column ?? Math.max(1, sheet.actualColumnCount);
1025
+ const rows = [];
1026
+ for (let row = startRow; row <= endRow; row += 1) {
1027
+ rows.push(readRow(sheet, row, startColumn, endColumn));
1028
+ }
1029
+ return { name: sheet.name, rowCount: rows.length, columnCount: endColumn - startColumn + 1, rows };
1030
+ }
1031
+ async function createWorkbookBytes(input) {
1032
+ const workbook = new ExcelJS.Workbook();
1033
+ const sheet = workbook.addWorksheet(validateSheetName(input.sheetName));
1034
+ const headers = deriveHeaders(input.headers, input.rows);
1035
+ if (input.tableName !== void 0 && input.tableName.length > 0 && headers.length > 0) {
1036
+ addTable(sheet, input.tableName, headers, input.rows);
1037
+ } else {
1038
+ addRows(sheet, headers, input.rows);
1039
+ }
1040
+ return await workbookToBytes(workbook);
1041
+ }
1042
+ async function readWorkbookBytes(bytes, options = {}) {
1043
+ const workbook = await loadWorkbook(bytes);
1044
+ const sheets = options.sheetName === void 0 ? workbook.worksheets : [getWorksheet(workbook, validateSheetName(options.sheetName))];
1045
+ return { sheets: sheets.map((sheet) => readSheet(sheet, options)) };
1046
+ }
1047
+ async function appendWorkbookRows(bytes, sheetName, rows, matchHeader) {
1048
+ const workbook = await loadWorkbook(bytes);
1049
+ const sheet = getWorksheet(workbook, validateSheetName(sheetName));
1050
+ const headers = matchHeader ? readHeaderRow(sheet) : deriveHeaders([], rows);
1051
+ for (const row of rows) {
1052
+ sheet.addRow([...rowToValues(row, headers)]);
1053
+ }
1054
+ return {
1055
+ bytes: await workbookToBytes(workbook),
1056
+ sheetName: sheet.name,
1057
+ rowCount: sheet.actualRowCount,
1058
+ columnCount: sheet.actualColumnCount
1059
+ };
1060
+ }
1061
+ async function updateWorkbookCell(bytes, sheetName, cellRef, value) {
1062
+ const workbook = await loadWorkbook(bytes);
1063
+ const sheet = getWorksheet(workbook, validateSheetName(sheetName));
1064
+ const cell = parseA1Cell(cellRef);
1065
+ sheet.getCell(cell.row, cell.column).value = value;
1066
+ return {
1067
+ bytes: await workbookToBytes(workbook),
1068
+ sheetName: sheet.name,
1069
+ rowCount: sheet.actualRowCount,
1070
+ columnCount: sheet.actualColumnCount
1071
+ };
1072
+ }
1073
+ async function addWorkbookSheet(bytes, sheetName, headers) {
1074
+ const workbook = await loadWorkbook(bytes);
1075
+ const normalized = validateSheetName(sheetName);
1076
+ if (workbook.getWorksheet(normalized) !== void 0) {
1077
+ throw new Error(`Sheet "${normalized}" already exists`);
1078
+ }
1079
+ const sheet = workbook.addWorksheet(normalized);
1080
+ if (headers.length > 0) {
1081
+ sheet.addRow([...headers]);
1082
+ }
1083
+ return {
1084
+ bytes: await workbookToBytes(workbook),
1085
+ sheetName: sheet.name,
1086
+ rowCount: sheet.actualRowCount,
1087
+ columnCount: sheet.actualColumnCount
1088
+ };
1089
+ }
1090
+
1091
+ // src/workbook/service.ts
1092
+ function normalizeWorkbookPath(path) {
1093
+ const normalized = path.trim().replace(/^\/+|\/+$/g, "");
1094
+ if (normalized.length === 0) {
1095
+ throw new Error("Workbook path is required");
1096
+ }
1097
+ if (!normalized.toLowerCase().endsWith(".xlsx")) {
1098
+ throw new Error(`Workbook path must end with .xlsx: ${path}`);
1099
+ }
1100
+ return normalized;
1101
+ }
1102
+ function requireEtag(item, path) {
1103
+ if (item.eTag === void 0 || item.eTag.length === 0) {
1104
+ throw new Error(`Cannot update ${path}: SharePoint item is missing an ETag`);
1105
+ }
1106
+ return item.eTag;
1107
+ }
1108
+ async function downloadExistingWorkbook(target, path) {
1109
+ const drive = selectDrive(target.session.drives, target.driveHint);
1110
+ const item = await getDriveItemByPath(target.session.client, drive.id, path);
1111
+ if (item === null || item.isFolder) {
1112
+ throw new Error(`Workbook not found: ${path}`);
1113
+ }
1114
+ const bytes = await downloadDriveFile(target.session.client, drive.id, path);
1115
+ return { bytes, item, driveId: drive.id, driveName: drive.name };
1116
+ }
1117
+ async function createRemoteWorkbook(target, path, input) {
1118
+ const normalizedPath = normalizeWorkbookPath(path);
1119
+ const drive = selectDrive(target.session.drives, target.driveHint);
1120
+ const bytes = await createWorkbookBytes(input);
1121
+ const item = await uploadNewDriveFile(target.session.client, drive.id, normalizedPath, bytes);
1122
+ return { driveId: drive.id, driveName: drive.name, path: normalizedPath, item };
1123
+ }
1124
+ async function readRemoteWorkbook(target, path, options = {}) {
1125
+ const normalizedPath = normalizeWorkbookPath(path);
1126
+ const downloaded = await downloadExistingWorkbook(target, normalizedPath);
1127
+ const workbook = await readWorkbookBytes(downloaded.bytes, options);
1128
+ return { ...downloaded, path: normalizedPath, workbook };
1129
+ }
1130
+ async function appendRemoteWorkbookRows(target, path, sheetName, rows, matchHeader) {
1131
+ const normalizedPath = normalizeWorkbookPath(path);
1132
+ const downloaded = await downloadExistingWorkbook(target, normalizedPath);
1133
+ const mutation = await appendWorkbookRows(downloaded.bytes, sheetName, rows, matchHeader);
1134
+ const item = await replaceDriveFile(
1135
+ target.session.client,
1136
+ downloaded.driveId,
1137
+ normalizedPath,
1138
+ requireEtag(downloaded.item, normalizedPath),
1139
+ mutation.bytes
1140
+ );
1141
+ return { ...downloaded, item, path: normalizedPath, mutation };
1142
+ }
1143
+ async function updateRemoteWorkbookCell(target, path, sheetName, cellRef, value) {
1144
+ const normalizedPath = normalizeWorkbookPath(path);
1145
+ const downloaded = await downloadExistingWorkbook(target, normalizedPath);
1146
+ const mutation = await updateWorkbookCell(downloaded.bytes, sheetName, cellRef, value);
1147
+ const item = await replaceDriveFile(
1148
+ target.session.client,
1149
+ downloaded.driveId,
1150
+ normalizedPath,
1151
+ requireEtag(downloaded.item, normalizedPath),
1152
+ mutation.bytes
1153
+ );
1154
+ return { ...downloaded, item, path: normalizedPath, mutation };
1155
+ }
1156
+ async function addRemoteWorkbookSheet(target, path, sheetName, headers) {
1157
+ const normalizedPath = normalizeWorkbookPath(path);
1158
+ const downloaded = await downloadExistingWorkbook(target, normalizedPath);
1159
+ const mutation = await addWorkbookSheet(downloaded.bytes, sheetName, headers);
1160
+ const item = await replaceDriveFile(
1161
+ target.session.client,
1162
+ downloaded.driveId,
1163
+ normalizedPath,
1164
+ requireEtag(downloaded.item, normalizedPath),
1165
+ mutation.bytes
1166
+ );
1167
+ return { ...downloaded, item, path: normalizedPath, mutation };
1168
+ }
1169
+
1170
+ // src/cli/options.ts
1171
+ function addCommonOptions(cmd) {
1172
+ return cmd.option("--profile <name>", "Stored profile name").option("--tenant <id>", "Azure AD tenant ID").option("--client-id <id>", "App registration client ID").option("--client-secret <secret>", "App registration client secret").option("--site <ref>", "SharePoint site, e.g. contoso.sharepoint.com/sites/demo").option("--drive <nameOrId>", "Document library name or ID").option("--json", "Emit JSON output", false);
1173
+ }
1174
+ function toRuntimeOverrides(flags) {
1175
+ return {
1176
+ profile: flags.profile,
1177
+ tenant: flags.tenant,
1178
+ clientId: flags.clientId,
1179
+ clientSecret: flags.clientSecret,
1180
+ site: flags.site,
1181
+ drive: flags.drive
1182
+ };
1183
+ }
1184
+ function requireFlag(value, flagName) {
1185
+ if (value === void 0 || value.trim().length === 0) {
1186
+ throw new Error(`${flagName} is required`);
1187
+ }
1188
+ return value;
1189
+ }
1190
+
1191
+ // src/cli/commands.ts
1192
+ function writeJson(value) {
1193
+ process5.stdout.write(`${JSON.stringify(value, null, 2)}
1194
+ `);
1195
+ }
1196
+ function writeOutput(json, jsonValue, textValue) {
1197
+ if (json === true) {
1198
+ writeJson(jsonValue);
1199
+ return;
1200
+ }
1201
+ process5.stdout.write(`${textValue}
1202
+ `);
1203
+ }
1204
+ function getSecretVault(storeKind) {
1205
+ return storeKind === "keyring" ? createKeyringSecretVault() : createFileSecretVault();
1206
+ }
1207
+ async function openRuntime(flags) {
1208
+ const runtime = await resolveRuntime({ overrides: toRuntimeOverrides(flags) });
1209
+ const session = await openSession(runtime.target);
1210
+ return { runtime, session };
1211
+ }
1212
+ function toServiceTarget(session, runtime, flags) {
1213
+ const driveHint = flags.drive ?? runtime.drive;
1214
+ return driveHint === void 0 ? { session } : { session, driveHint };
1215
+ }
1216
+ function toReadOptions(flags) {
1217
+ return {
1218
+ ...flags.sheet === void 0 ? {} : { sheetName: flags.sheet },
1219
+ ...flags.range === void 0 ? {} : { range: flags.range }
1220
+ };
1221
+ }
1222
+ function assertPlaintextAllowed(flags) {
1223
+ if (flags.store !== "file") {
1224
+ return;
1225
+ }
1226
+ if (flags.allowPlaintextSecret === true || process5.env[ENV_ALLOW_PLAINTEXT] === "1") {
1227
+ return;
1228
+ }
1229
+ throw new Error(
1230
+ `Plaintext secret file store requires --allow-plaintext-secret or ${ENV_ALLOW_PLAINTEXT}=1`
1231
+ );
1232
+ }
1233
+ async function handleConfigSet(flags) {
1234
+ assertPlaintextAllowed(flags);
1235
+ const secretStore = parseSecretStoreKind(flags.store);
1236
+ const input = {
1237
+ name: flags.profile ?? DEFAULT_PROFILE_NAME,
1238
+ tenantId: requireFlag(flags.tenant, "--tenant"),
1239
+ clientId: requireFlag(flags.clientId, "--client-id"),
1240
+ clientSecret: requireFlag(flags.clientSecret, "--client-secret"),
1241
+ site: requireFlag(flags.site, "--site"),
1242
+ secretStore,
1243
+ ...flags.drive === void 0 ? {} : { drive: flags.drive }
1244
+ };
1245
+ const profile = await upsertProfile(createProfileStore(), getSecretVault(secretStore), input);
1246
+ const redacted = await redactProfile(profile, getSecretVault(secretStore));
1247
+ writeOutput(flags.json, redacted, formatProfile(redacted));
1248
+ }
1249
+ async function handleConfigGet(flags) {
1250
+ const name = flags.profile ?? DEFAULT_PROFILE_NAME;
1251
+ const profile = findProfile(await createProfileStore().readProfiles(), name);
1252
+ if (profile === void 0) {
1253
+ throw new Error(`Profile "${name}" not found`);
1254
+ }
1255
+ const redacted = await redactProfile(profile, getSecretVault(profile.secretStore));
1256
+ writeOutput(flags.json, redacted, formatProfile(redacted));
1257
+ }
1258
+ async function handleConfigRemove(flags) {
1259
+ const name = flags.profile ?? DEFAULT_PROFILE_NAME;
1260
+ const profile = findProfile(await createProfileStore().readProfiles(), name);
1261
+ const secretStore = profile?.secretStore ?? "keyring";
1262
+ const removed = await removeProfile(createProfileStore(), getSecretVault(secretStore), name);
1263
+ const output = { profile: name, removed };
1264
+ writeOutput(flags.json, output, `Removed profile ${name}: ${String(removed)}`);
1265
+ }
1266
+ async function handleTest(flags) {
1267
+ const { session } = await openRuntime(flags);
1268
+ const output = { site: session.site, drives: session.drives, tokenType: session.token.tokenType };
1269
+ writeOutput(flags.json, output, formatTestResult(session.site, session.drives));
1270
+ }
1271
+ async function handleDrives(flags) {
1272
+ const { session } = await openRuntime(flags);
1273
+ writeOutput(flags.json, session.drives, formatDriveList(session.drives));
1274
+ }
1275
+ async function handleCreate(flags) {
1276
+ const { runtime, session } = await openRuntime(flags);
1277
+ const result = await createRemoteWorkbook(
1278
+ toServiceTarget(session, runtime, flags),
1279
+ requireFlag(flags.path, "--path"),
1280
+ {
1281
+ sheetName: requireFlag(flags.sheet, "--sheet"),
1282
+ headers: parseHeaders(flags.headers),
1283
+ rows: parseWorkbookRows(flags.rows),
1284
+ ...flags.table === void 0 ? {} : { tableName: flags.table }
1285
+ }
1286
+ );
1287
+ writeOutput(flags.json, result, formatCreateResult(result));
1288
+ }
1289
+ async function handleRead(flags) {
1290
+ const { runtime, session } = await openRuntime(flags);
1291
+ const result = await readRemoteWorkbook(
1292
+ toServiceTarget(session, runtime, flags),
1293
+ requireFlag(flags.path, "--path"),
1294
+ toReadOptions(flags)
1295
+ );
1296
+ writeOutput(flags.json, result, formatWorkbookRead(result));
1297
+ }
1298
+ async function handleAppend(flags) {
1299
+ const { runtime, session } = await openRuntime(flags);
1300
+ const result = await appendRemoteWorkbookRows(
1301
+ toServiceTarget(session, runtime, flags),
1302
+ requireFlag(flags.path, "--path"),
1303
+ requireFlag(flags.sheet, "--sheet"),
1304
+ parseWorkbookRows(requireFlag(flags.record, "--record")),
1305
+ flags.matchHeader !== false
1306
+ );
1307
+ writeOutput(flags.json, result, formatMutationResult("Updated", result));
1308
+ }
1309
+ async function handleUpdateCell(flags) {
1310
+ const { runtime, session } = await openRuntime(flags);
1311
+ const result = await updateRemoteWorkbookCell(
1312
+ toServiceTarget(session, runtime, flags),
1313
+ requireFlag(flags.path, "--path"),
1314
+ requireFlag(flags.sheet, "--sheet"),
1315
+ requireFlag(flags.cell, "--cell"),
1316
+ parseCellValue(requireFlag(flags.value, "--value"))
1317
+ );
1318
+ writeOutput(flags.json, result, formatMutationResult("Updated", result));
1319
+ }
1320
+ async function handleAddSheet(flags) {
1321
+ const { runtime, session } = await openRuntime(flags);
1322
+ const result = await addRemoteWorkbookSheet(
1323
+ toServiceTarget(session, runtime, flags),
1324
+ requireFlag(flags.path, "--path"),
1325
+ requireFlag(flags.sheet, "--sheet"),
1326
+ parseHeaders(flags.headers)
1327
+ );
1328
+ writeOutput(flags.json, result, formatMutationResult("Updated", result));
1329
+ }
1330
+ function registerConfigCommands(program) {
1331
+ const config = program.command("config").description("Manage local SharePoint Excel profiles");
1332
+ config.command("set").description("Store a SharePoint app-only profile").option("--profile <name>", "Profile name", DEFAULT_PROFILE_NAME).requiredOption("--tenant <id>", "Azure AD tenant ID").requiredOption("--client-id <id>", "App registration client ID").requiredOption("--client-secret <secret>", "App registration client secret").requiredOption("--site <ref>", "SharePoint site").option("--drive <nameOrId>", "Default document library").option("--store <keyring|file>", "Secret store", "keyring").option("--allow-plaintext-secret", "Allow plaintext file secret storage", false).option("--json", "Emit JSON output", false).action(handleConfigSet);
1333
+ config.command("get").description("Read a stored profile without printing secrets").option("--profile <name>", "Profile name", DEFAULT_PROFILE_NAME).option("--json", "Emit JSON output", false).action(handleConfigGet);
1334
+ config.command("remove").description("Remove a stored profile").option("--profile <name>", "Profile name", DEFAULT_PROFILE_NAME).option("--json", "Emit JSON output", false).action(handleConfigRemove);
1335
+ }
1336
+ function registerCommands(program) {
1337
+ registerConfigCommands(program);
1338
+ addCommonOptions(program.command("test").description("Authenticate, resolve site, and list drives")).action(handleTest);
1339
+ addCommonOptions(program.command("drives").description("List SharePoint document libraries")).action(handleDrives);
1340
+ addCommonOptions(program.command("create").description("Create a new .xlsx file without overwriting")).requiredOption("--path <xlsxPath>", "SharePoint path for the new workbook").requiredOption("--sheet <name>", "Initial sheet name").option("--headers <csv>", "Comma-separated header row").option("--rows <json>", "JSON row/object or array of rows/objects").option("--table <name>", "Optional Excel table name").action(handleCreate);
1341
+ addCommonOptions(program.command("read").description("Read workbook sheets or a range")).requiredOption("--path <xlsxPath>", "SharePoint workbook path").option("--sheet <name>", "Sheet to read").option("--range <a1Range>", "A1 range, e.g. A1:C10").action(handleRead);
1342
+ addCommonOptions(program.command("append").description("Append one or more records to a sheet")).requiredOption("--path <xlsxPath>", "SharePoint workbook path").requiredOption("--sheet <name>", "Sheet to append").requiredOption("--record <json>", "JSON row/object or array of rows/objects").option("--no-match-header", "Append object values by key order instead of row 1 headers").action(handleAppend);
1343
+ addCommonOptions(program.command("update-cell").description("Update one cell in a workbook")).requiredOption("--path <xlsxPath>", "SharePoint workbook path").requiredOption("--sheet <name>", "Sheet name").requiredOption("--cell <a1Cell>", "A1 cell reference, e.g. B2").requiredOption("--value <jsonOrString>", "JSON scalar or raw string").action(handleUpdateCell);
1344
+ addCommonOptions(program.command("add-sheet").description("Add a new sheet to an existing workbook")).requiredOption("--path <xlsxPath>", "SharePoint workbook path").requiredOption("--sheet <name>", "New sheet name").option("--headers <csv>", "Optional comma-separated header row").action(handleAddSheet);
1345
+ }
1346
+
1347
+ // src/cli/index.ts
1348
+ async function main(argv) {
1349
+ const program = new Command();
1350
+ program.name("saptools-sharepoint-excel").description("Create, read, and update SharePoint-hosted Excel workbooks");
1351
+ registerCommands(program);
1352
+ await program.parseAsync([...argv]);
1353
+ }
1354
+ try {
1355
+ await main(process6.argv);
1356
+ } catch (err) {
1357
+ const message = err instanceof Error ? err.message : String(err);
1358
+ process6.stderr.write(`Error: ${message}
1359
+ `);
1360
+ process6.exit(1);
1361
+ }
1362
+ export {
1363
+ main
1364
+ };
1365
+ //# sourceMappingURL=cli.js.map