#!/usr/bin/env node // CLI wrapper around SharePointListsClient. Prints JSON to stdout; errors to // stderr with a non-zero exit so callers can detect failure reliably. // // Credentials are read from the environment (or a .env file in the CWD or this // skill's directory). Required: // SP_TENANT_ID (or AZURE_TENANT_ID) // SP_CLIENT_ID (or AZURE_CLIENT_ID) // SP_CLIENT_SECRET (or AZURE_CLIENT_SECRET) // Optional default so you can omit --site on every call: // SP_SITE_URL e.g. https://contoso.sharepoint.com/sites/Marketing // // Usage: // node sp.mjs test [--site URL] // node sp.mjs lists --site URL // node sp.mjs columns --site URL --list NAME_OR_ID // node sp.mjs items --site URL --list NAME_OR_ID [--filter ODATA] [--select F1,F2] [--orderby "fields/Created desc"] [--top N] [--all] // node sp.mjs get --site URL --list NAME_OR_ID --id ITEM_ID // node sp.mjs create --site URL --list NAME_OR_ID --fields '{"Title":"Hi"}' // node sp.mjs update --site URL --list NAME_OR_ID --id ITEM_ID --fields '{"Status":"Done"}' // node sp.mjs delete --site URL --list NAME_OR_ID --id ITEM_ID // // --fields accepts inline JSON, @path/to/file.json, or "-" to read JSON from stdin. import { readFileSync, existsSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { SharePointListsClient, GraphError, secretExpiryStatus } from "./lib/graph.mjs"; const __dirname = dirname(fileURLToPath(import.meta.url)); main().catch((err) => { const payload = { ok: false, error: err.message, ...(err instanceof GraphError ? { status: err.status, code: err.code, requestId: err.requestId } : {}), }; process.stderr.write(JSON.stringify(payload, null, 2) + "\n"); process.exit(1); }); async function main() { loadDotEnv(); const [, , command, ...rest] = process.argv; const flags = parseFlags(rest); if (!command || command === "help" || flags.help) { printHelp(); return; } const client = new SharePointListsClient({ tenantId: env("SP_TENANT_ID", "AZURE_TENANT_ID"), clientId: env("SP_CLIENT_ID", "AZURE_CLIENT_ID"), clientSecret: env("SP_CLIENT_SECRET", "AZURE_CLIENT_SECRET"), }); // Nag (on stderr, so stdout stays clean JSON) when the client secret is // close to its self-reported expiry — see secretExpiryStatus in graph.mjs. const expiry = secretExpiryStatus(process.env.SP_SECRET_EXPIRES); if (expiry?.message) process.stderr.write(`⚠ ${expiry.message}\n`); const site = flags.site || process.env.SP_SITE_URL; const needSite = () => { if (!site) throw new Error("Missing --site URL (or set SP_SITE_URL)."); return client.resolveSiteId(site); }; const needList = () => { if (!flags.list) throw new Error("Missing --list NAME_OR_ID."); return flags.list; }; const needId = () => { if (!flags.id) throw new Error("Missing --id ITEM_ID."); return flags.id; }; let result; switch (command) { case "test": result = await client.test(site); if (expiry) result.secretExpiry = expiry; break; case "lists": result = await client.listLists(await needSite()); break; case "columns": result = await client.getColumns(await needSite(), needList()); break; case "items": result = await client.listItems(await needSite(), needList(), { filter: flags.filter, select: flags.select, orderby: flags.orderby, top: flags.top ? Number(flags.top) : undefined, all: Boolean(flags.all), }); break; case "get": result = await client.getItem(await needSite(), needList(), needId()); break; case "create": result = await client.createItem(await needSite(), needList(), readFields(flags)); break; case "update": result = await client.updateItem(await needSite(), needList(), needId(), readFields(flags)); break; case "delete": result = await client.deleteItem(await needSite(), needList(), needId()); result = { ok: true, deleted: needId() }; break; default: throw new Error(`Unknown command "${command}". Run "node sp.mjs help".`); } process.stdout.write(JSON.stringify(result, null, 2) + "\n"); } // ---- helpers -------------------------------------------------------------- function env(...names) { for (const n of names) if (process.env[n]) return process.env[n]; return undefined; } function parseFlags(args) { const flags = {}; for (let i = 0; i < args.length; i++) { const a = args[i]; if (!a.startsWith("--")) continue; const key = a.slice(2); const next = args[i + 1]; if (next === undefined || next.startsWith("--")) { flags[key] = true; // boolean flag (e.g. --all) } else { flags[key] = next; i++; } } return flags; } function readFields(flags) { const raw = flags.fields; if (raw === undefined || raw === true) { throw new Error('Missing --fields \'{"Col":"value"}\' (or @file.json, or - for stdin).'); } let text = raw; if (raw === "-") text = readFileSync(0, "utf8"); else if (raw.startsWith("@")) text = readFileSync(raw.slice(1), "utf8"); try { return JSON.parse(text); } catch (e) { throw new Error(`--fields is not valid JSON: ${e.message}`); } } // Minimal .env loader (no dependency). Looks in CWD then this skill's dir. function loadDotEnv() { for (const dir of [process.cwd(), __dirname, join(__dirname, "..")]) { const file = join(dir, ".env"); if (!existsSync(file)) continue; for (const line of readFileSync(file, "utf8").split("\n")) { const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/i); if (!m) continue; const key = m[1]; let val = m[2].trim(); if ( (val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'")) ) { val = val.slice(1, -1); } if (process.env[key] === undefined) process.env[key] = val; } } } function printHelp() { process.stdout.write( `sharepoint-lists CLI — Microsoft Graph, app-only auth Commands: test [--site URL] Verify auth (+ list lists if --site) lists --site URL List all lists in a site columns --site URL --list NAME_OR_ID Show column internal names + types items --site URL --list NAME_OR_ID [filters] Query list items get --site URL --list NAME_OR_ID --id ID Get one item create --site URL --list NAME_OR_ID --fields J Create an item update --site URL --list NAME_OR_ID --id ID --fields J Update an item delete --site URL --list NAME_OR_ID --id ID Delete an item items filters: --filter "fields/Status eq 'Open'" --select Title,Status --orderby "fields/Created desc" --top 50 --all --fields accepts inline JSON, @file.json, or - (stdin). Credentials come from env / .env: SP_TENANT_ID, SP_CLIENT_ID, SP_CLIENT_SECRET. Optional: SP_SITE_URL to default --site. `, ); }