From 98fdd974a7ecf25c50fce57dc9660cd6799d990a Mon Sep 17 00:00:00 2001 From: waldekmastykarz Date: Mon, 25 May 2026 11:48:14 +0200 Subject: [PATCH 1/2] Adds a skill for the CLI --- .github/skills/new-command/SKILL.md | 350 +++++++ .github/skills/refresh-clim365-skill/SKILL.md | 30 + .../references/write-skill-commands.js | 18 + .npmignore | 1 + skills/clim365/SKILL.md | 94 ++ skills/clim365/references/commands.txt | 869 ++++++++++++++++++ 6 files changed, 1362 insertions(+) create mode 100644 .github/skills/new-command/SKILL.md create mode 100644 .github/skills/refresh-clim365-skill/SKILL.md create mode 100644 .github/skills/refresh-clim365-skill/references/write-skill-commands.js create mode 100644 skills/clim365/SKILL.md create mode 100644 skills/clim365/references/commands.txt diff --git a/.github/skills/new-command/SKILL.md b/.github/skills/new-command/SKILL.md new file mode 100644 index 00000000000..3e3b7ebf944 --- /dev/null +++ b/.github/skills/new-command/SKILL.md @@ -0,0 +1,350 @@ +--- +name: new-command +description: >- + This skill should be used when the user asks to "build a new command", + "create a command", "implement a command", "add a new CLI command", + or needs to build a new command for CLI for Microsoft 365 from a + GitHub issue spec. It covers the full workflow: command logic, + unit tests, documentation, sidebar registration, and PR checklist + verification. +--- + +# Building a New Command for CLI for Microsoft 365 + +Build a complete, production-ready CLI command from a GitHub issue spec. The workflow produces four artifacts: command implementation, unit tests, documentation page, and sidebar registration — then verifies everything against the PR checklist. + +## Prerequisites + +A GitHub issue containing the command spec (name, description, options, examples, API details). If no issue is provided, **STOP — ask the user for the issue URL or spec before proceeding.** + +## Workflow + +Execute each phase in order. Do not skip phases. + +### Phase 1: Parse the Spec + +1. Read the GitHub issue thoroughly. +2. Extract: command name, description, service/workload, options (required/optional, types, aliases, allowed values, option sets), API endpoints used, example usage, and expected response shape. +3. **STOP — Verify API details are complete.** The spec must include the full API endpoint(s), HTTP method(s), request payloads, and response shapes. If any of these are missing, **ask the user** to provide them or point to API documentation. **NEVER fabricate or infer API request/response shapes** — even if similar commands exist in the codebase. +4. Identify the base class. Look at existing commands in `src/m365//commands/` to determine which base class to extend (`SpoCommand`, `GraphCommand`, `GraphApplicationCommand`, `AzmgmtCommand`, etc.). +5. Check that every word in the command name exists in the dictionary in `eslint.config.mjs`. If a word is missing, add it to the `dictionary` array (keep alphabetical order). + +### Phase 2: Implement the Command + +Create `src/m365//commands//-.ts`. + +#### Structure + +```typescript +import { globalOptionsZod } from '../../../../Command.js'; +import { z } from 'zod'; +import { Logger } from '../../../../cli/Logger.js'; +import commands from '../../commands.js'; +import from '../../../base/.js'; +import request, { CliRequestOptions } from '../../../../request.js'; +// additional imports as needed + +// Enums for options with predefined values +// enum Foo { Bar = 'bar', Baz = 'baz' } + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + // command-specific options +}); +declare type Options = z.infer; + +interface CommandArgs { + options: Options; +} + +class Command extends { + public get name(): string { + return commands._; + } + + public get description(): string { + return ''; + } + + public get schema(): z.ZodType { + return options; + } + + // getRefinedSchema — only if option sets or cross-field validation needed + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + try { + if (this.verbose) { + await logger.logToStderr(`...`); + } + + const requestOptions: CliRequestOptions = { + url: ``, + headers: { accept: 'application/json;odata.metadata=none' }, + responseType: 'json' + }; + + const result = await request.get(requestOptions); + await logger.log(result); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new Command(); +``` + +#### Key rules + +- Class name: `Command` in PascalCase. +- Options: use `z.strictObject` spreading `globalOptionsZod.shape`. +- Aliases: `.alias('x')` on the Zod property. +- Enums: `zod.coercedEnum(MyEnum)` for case-insensitive matching. Import `{ zod }` from `../../utils/zod.js`. +- Validation: Zod refinements on properties (`.refine()`), not custom validate methods. +- URL validation for SharePoint: `.refine(url => validation.isValidSharePointUrl(url) === true, { error: '...' })`. +- Option sets: implement `getRefinedSchema(schema)` returning `schema.refine(...)`. +- Async/await only — no `.then()`. +- Verbose/debug logging → `logger.logToStderr`. +- Error handling → `this.handleRejectedODataJsonPromise(err)`. +- SPO file/folder endpoints: use `GetFileByServerRelativePath` / `GetFolderByServerRelativePath`. +- Remove commands: include a `force` option and confirmation prompt using `cli.handleMultipleResultsFound` or `cli.promptForConfirmation`. +- No `any` types (except the catch clause). Use specific interfaces/types. +- No commented-out code. + +#### Register the command name + +Add the command constant to `src/m365//commands.ts`, keeping groups alphabetically sorted: + +```typescript +export default { + // ...existing commands... + _: `${prefix} `, + // ... +}; +``` + +### Phase 3: Write Unit Tests + +Create `src/m365//commands//-.spec.ts`. + +#### Skeleton + +```typescript +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.js'; +import { CommandError } from '../../../../Command.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { telemetry } from '../../../../telemetry.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import request from '../../../../request.js'; +import commands from '../../commands.js'; +import command, { options as commandOptionsSchema } from './-.js'; + +describe(commands._, () => { + let log: any[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').resolves(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { log.push(msg); }, + logRaw: async (msg: string) => { log.push(msg); }, + logToStderr: async (msg: string) => { log.push(msg); } + }; + loggerLogSpy = sinon.spy(logger, 'log'); + }); + + afterEach(() => { + sinonUtil.restore([ + request.get, + request.post, + request.put, + request.patch, + request.delete + // restore only the HTTP methods actually stubbed + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + }); + + it('has the correct name', () => { + assert.strictEqual(command.name, commands._); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + // Validation tests — one pass and one fail per validation rule + // Option set tests — valid combos and invalid combos + // commandAction tests — one per branch/code path + // API error test +}); +``` + +#### Required test categories + +1. **Name and description** — always. +2. **Validation** — each Zod refinement tested for pass and fail using `commandOptionsSchema.safeParse(...)`. +3. **Option sets** — valid single option, invalid multiple options, missing required option. +4. **Command action** — one test per logical branch. Stub `request.get`/`post`/etc. with `callsFake` matching URL patterns. +5. **Error handling** — stub request to reject, assert `CommandError`. +6. **Coverage** — every `if`, `switch`, `catch` branch hit. Target 100% code and branch coverage. + +#### Run tests + +```bash +npm test +``` + +Check coverage in `coverage/lcov-report/index.html`. If coverage is below 100% on the new command file, add tests for missed branches. + +### Phase 4: Write Documentation + +Create `docs/docs/cmd///-.mdx`. + +#### Template + +````mdx +import Global from '../../_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# + + + +## Usage + +```sh +m365 [options] +``` + +## Options + +```md definition-list +`-, --