Initial import: SharePoint Lists skill
Claude Code skill for SharePoint Lists CRUD via Microsoft Graph (app-only auth): reusable graph.mjs client, sp.mjs CLI, and setup + API reference docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
197
.claude/skills/sharepoint-lists/scripts/sp.mjs
Normal file
197
.claude/skills/sharepoint-lists/scripts/sp.mjs
Normal file
@@ -0,0 +1,197 @@
|
||||
#!/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 } 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"),
|
||||
});
|
||||
|
||||
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);
|
||||
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.
|
||||
`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user