Skip to content

Commit 5579d16

Browse files
stefanjudisCopilot
andauthored
feat(cli): deprecate rules command in favor of skills (#1361)
* feat(cli): deprecate rules command in favor of skills The rules command read the bundled checkly.rules.md and wrote it into AI IDE config folders. That capability now lives in `checkly skills`, so rules is reduced to a deprecation notice pointing users there. * chore(cli): stop generating checkly.rules.md ai-context artifact The rules command no longer reads checkly.rules.md, so drop its generation from the AI-context pipeline along with the now-dead heading helpers, and remove the step that attached it to GitHub Releases. The finalize-release job keeps marking the release as latest. * test(e2e): update help output for deprecated rules command * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * refactor(cli): present-tense rules deprecation notice; fix GitHub casing --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent fda0b51 commit 5579d16

6 files changed

Lines changed: 45 additions & 190 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -171,22 +171,13 @@ jobs:
171171
SLACK_TITLE: ':white_check_mark: NPM publish succeeded'
172172
SLACK_MESSAGE: by ${{ github.actor }}
173173
SLACK_FOOTER: ''
174-
# Attach checkly.rules.md to the GitHub Release and mark it as latest
174+
# Mark the GitHub Release as latest if it's the highest version
175175
finalize-release:
176176
runs-on: ubuntu-latest
177177
needs: release
178178
permissions:
179179
contents: write
180180
steps:
181-
- name: Download LLM rules artifact
182-
uses: actions/download-artifact@v4
183-
with:
184-
name: llm-rules-release
185-
path: llm-rules-release
186-
- name: Upload checkly.rules.md to GitHub Release
187-
env:
188-
GH_TOKEN: ${{ github.token }}
189-
run: gh release upload ${{ github.event.release.tag_name }} llm-rules-release/checkly.rules.md --repo ${{ github.repository }}
190181
- name: Mark release as latest if version is highest
191182
env:
192183
GH_TOKEN: ${{ github.token }}

CONTRIBUTING.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,18 @@ Both packages [checkly](https://www.npmjs.com/package/checkly) and [create-cli](
105105

106106
To release packages to NPM:
107107

108-
1. Publish a Github Release with a valid tag `#.#.#` (do **not** include a `v` prefix) and click the `Generate release notes` button to auto-generate notes following format defined [here](https://github.com/checkly/checkly-cli/blob/main/.github/release.yml). **Uncheck "Set as the latest release"** — the workflow will mark it as latest automatically after attaching the `checkly.rules.md` asset.
109-
2. When release is published the Github action is triggered. It builds and publishes `#.#.#-prerelease` prereleases (for both packages).
108+
1. Publish a GitHub Release with a valid tag `#.#.#` (do **not** include a `v` prefix) and click the `Generate release notes` button to auto-generate notes following format defined [here](https://github.com/checkly/checkly-cli/blob/main/.github/release.yml). **Uncheck "Set as the latest release"** — the workflow will mark it as latest automatically.
109+
2. When release is published the GitHub action is triggered. It builds and publishes `#.#.#-prerelease` prereleases (for both packages).
110110
3. Test the prerelease version to make sure that it's working.
111111
* To test `npm create checkly`, run `CHECKLY_CLI_VERSION=4.6.2 npm create checkly@4.6.2-prerelease-c6e8165` (substituting `4.6.2` and `4.6.2-prerelease` for your versions, which you can find at https://www.npmjs.com/package/checkly?activeTab=versions). `CHECKLY_CLI_VERSION` is needed since the `create-checkly` package looks up the corresponding tag on GitHub to pull project templates.
112112
* Ensure your project `package.json` has `"checkly": "4.6.2-prerelease-c6e8165"`
113-
4. A `production` deployment will be waiting for approval. After it's approved, the `#.#.#` version will be published. The workflow will then automatically attach `checkly.rules.md` to the GitHub Release and mark it as latest (if the version is higher than the current latest).
113+
4. A `production` deployment will be waiting for approval. After it's approved, the `#.#.#` version will be published. The workflow will then automatically mark the GitHub Release as latest (if the version is higher than the current latest).
114114

115115
### Catching issues in prerelease
116116

117117
If you notice an issue when testing the prerelease you can still roll everything back. Simply delete the GitHub release, and delete the corresponding tags from the GitHub UI (both `#.#.#` and `v#.#.#`).
118118

119-
After resolving the issues, you can create another Github release and go through the process again.
119+
After resolving the issues, you can create another GitHub release and go through the process again.
120120

121121
## Style Guide
122122

packages/cli/e2e/__tests__/help.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('help', () => {
6868
logout Log out and clear any local credentials.
6969
members List account members and pending invites.
7070
rca Trigger and retrieve root cause analyses.
71-
rules Generate a rules file to use with AI IDEs and Copilots.
71+
rules Deprecated. Use \`checkly skills\` instead.
7272
runtimes List all supported runtimes and dependencies.
7373
skills Show Checkly AI skills, actions and their references.
7474
status-pages List and manage status pages in your Checkly account.

packages/cli/scripts/prepare-ai-context.ts

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ACTIONS, EXAMPLE_CONFIGS } from '../src/ai-context/context'
44

55
const EXAMPLES_DIR = join(__dirname, '../gen/')
66
const AI_CONTEXT_DIR = join(__dirname, '../src/ai-context')
7-
const RULES_OUTPUT_DIR = join(__dirname, '../dist/ai-context')
7+
const DIST_AI_CONTEXT_DIR = join(__dirname, '../dist/ai-context')
88

99
// Reference files served by the CLI's `checkly skills [action] [reference]` command
1010
const COMMAND_REFERENCES_DIR = join(__dirname, '../dist/ai-context/skills-command/references')
@@ -13,21 +13,6 @@ const COMMAND_REFERENCES_DIR = join(__dirname, '../dist/ai-context/skills-comman
1313
const PUBLIC_SKILLS_DIR = join(__dirname, '../dist/ai-context/public-skills')
1414
const PUBLIC_SKILL_DIR = join(PUBLIC_SKILLS_DIR, 'checkly')
1515

16-
function stripYamlFrontmatter (content: string): string {
17-
const frontmatterRegex = /^---\r?\n[\s\S]*?\r?\n---\r?\n+/
18-
return content.replace(frontmatterRegex, '')
19-
}
20-
21-
// Demote headings by two levels (# -> ###, ## -> ####) to maintain proper
22-
// heading hierarchy in checkly.rules.md.
23-
function demoteHeadings (content: string): string {
24-
return content.replace(/^(#+)/gm, '##$1')
25-
}
26-
27-
function normalizeBlankLines (content: string): string {
28-
return content.replace(/\n{3,}/g, '\n\n')
29-
}
30-
3116
async function writeOutput (content: string, dir: string, filename: string): Promise<void> {
3217
await mkdir(dir, { recursive: true })
3318
const outputPath = join(dir, filename)
@@ -102,8 +87,6 @@ async function prepareContext () {
10287
const examples = await readExampleCode()
10388

10489
// Process all actions — reference files, action headers, and standalone actions
105-
const configureReferenceContents: string[] = []
106-
10790
for (const action of ACTIONS) {
10891
if ('references' in action) {
10992
for (const ref of action.references) {
@@ -113,10 +96,6 @@ async function prepareContext () {
11396
)
11497
refContent = replaceExamples(refContent, examples)
11598
await writeOutput(refContent, COMMAND_REFERENCES_DIR, `${ref.id}.md`)
116-
117-
if (action.id === 'configure') {
118-
configureReferenceContents.push(refContent)
119-
}
12099
}
121100

122101
let actionContent = await readFile(
@@ -144,29 +123,15 @@ async function prepareContext () {
144123
.replace('<!-- SKILL_COMMANDS -->', generateSkillCommands())
145124
await writeOutput(skillTemplate, PUBLIC_SKILL_DIR, 'SKILL.md')
146125

147-
// Generate checkly.rules.md (configure header + all configure-* references concatenated)
148-
const configureContent = await readFile(
149-
join(COMMAND_REFERENCES_DIR, 'configure.md'),
150-
'utf8',
151-
)
152-
const demotedReferences = configureReferenceContents
153-
.map(demoteHeadings).join('\n\n')
154-
const rulesContent = normalizeBlankLines(stripYamlFrontmatter(
155-
configureContent
156-
+ '\n'
157-
+ demotedReferences,
158-
))
159-
await writeOutput(rulesContent, RULES_OUTPUT_DIR, 'checkly.rules.md')
160-
161126
// Copy README
162127
const readme = await readFile(join(AI_CONTEXT_DIR, 'README.md'), 'utf8')
163128
await writeOutput(readme, PUBLIC_SKILL_DIR, 'README.md')
164129

165130
// Copy onboarding assets to dist
166131
for (const dir of ['onboarding-boilerplate', 'onboarding-prompts']) {
167-
await cp(join(AI_CONTEXT_DIR, dir), join(RULES_OUTPUT_DIR, dir), { recursive: true })
132+
await cp(join(AI_CONTEXT_DIR, dir), join(DIST_AI_CONTEXT_DIR, dir), { recursive: true })
168133
// eslint-disable-next-line no-console
169-
console.log(`Copied ${dir} to ${join(RULES_OUTPUT_DIR, dir)}`)
134+
console.log(`Copied ${dir} to ${join(DIST_AI_CONTEXT_DIR, dir)}`)
170135
}
171136
} catch (error) {
172137
// eslint-disable-next-line no-console
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
3+
import Rules from '../rules.js'
4+
5+
const mockConfig = {
6+
runHook: vi.fn().mockResolvedValue({ successes: [], failures: [] }),
7+
} as any
8+
9+
function createCommand (...args: string[]) {
10+
const cmd = new Rules(args, mockConfig)
11+
cmd.log = vi.fn() as any
12+
return cmd
13+
}
14+
15+
function getLogged (cmd: Rules): string[] {
16+
return (cmd.log as ReturnType<typeof vi.fn>).mock.calls.map(
17+
(call: string[]) => call[0],
18+
)
19+
}
20+
21+
describe('rules', () => {
22+
it('prints the deprecation message pointing to skills', async () => {
23+
const cmd = createCommand()
24+
25+
await cmd.run()
26+
27+
const logged = getLogged(cmd)
28+
expect(logged.some(m => m.includes('is deprecated'))).toBe(true)
29+
expect(logged.some(m => m.includes('checkly skills'))).toBe(true)
30+
})
31+
})

packages/cli/src/commands/rules.ts

Lines changed: 5 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,15 @@
11
import { BaseCommand } from './baseCommand.js'
2-
import { readFile, writeFile, access, mkdir } from 'fs/promises'
3-
import path, { join } from 'path'
4-
import { constants } from 'fs'
5-
import prompts from 'prompts'
6-
import { fileURLToPath } from 'node:url'
7-
8-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
9-
10-
const BASE_RULES_FILE_PATH = join(__dirname, '../ai-context/checkly.rules.md')
11-
12-
// AI IDE configurations mapping
13-
const AI_IDE_CONFIGS = {
14-
'Windsurf': {
15-
rulesFolder: '.windsurf/rules',
16-
rulesFileName: 'checkly.md',
17-
},
18-
'GitHub Copilot': {
19-
rulesFolder: '.github/instructions',
20-
rulesFileName: 'checkly.instructions.md',
21-
},
22-
'Cursor': {
23-
rulesFolder: '.cursor/rules',
24-
rulesFileName: 'checkly.mdc',
25-
},
26-
'Plain Markdown (checkly.md)': {
27-
rulesFolder: '.',
28-
rulesFileName: 'checkly.md',
29-
},
30-
} as const
312

323
export default class Rules extends BaseCommand {
334
static hidden = false
345
static readOnly = true
356
static idempotent = true
7+
static state = 'deprecated'
368
static description =
37-
'Generate a rules file to use with AI IDEs and Copilots.'
38-
39-
async run (): Promise<void> {
40-
// Read the base rules file
41-
const rulesContent = await this.readBaseRulesFile()
42-
if (!rulesContent) {
43-
this.error(`Failed to read rules file at ${BASE_RULES_FILE_PATH}`)
44-
}
45-
46-
// In non-interactive mode, print rules to stdout and exit
47-
const isNonInteractive = !process.stdin.isTTY
48-
|| !process.stdout.isTTY
49-
|| process.env.CI
50-
|| process.env.CHECKLY_NON_INTERACTIVE
51-
if (isNonInteractive) {
52-
this.log(rulesContent)
53-
return
54-
}
55-
56-
try {
57-
// Create options for multiselect - offer all configs from AI_IDE_CONFIGS
58-
const choices = Object.entries(AI_IDE_CONFIGS).map(([ideName, ideConfig]) => {
59-
return {
60-
title: `${ideName} (${path.join(ideConfig.rulesFolder, ideConfig.rulesFileName)})`,
61-
value: ideConfig,
62-
selected: false,
63-
}
64-
})
65-
66-
const isNonInteractive = !process.stdin.isTTY
67-
|| !process.stdout.isTTY
68-
|| process.env.CI
69-
|| process.env.CHECKLY_NON_INTERACTIVE
70-
71-
// Interactive mode - show multiselect
72-
const { configs: selectedConfig } = await prompts({
73-
type: 'select',
74-
name: 'configs',
75-
message: 'Select the AI IDE configurations to generate rules for:',
76-
choices,
77-
initial: 0,
78-
})
79-
80-
if (!selectedConfig) {
81-
this.log('Operation cancelled.')
82-
return
83-
}
84-
85-
this.log(`Generating rules`)
86-
87-
// Create rules directory if it doesn't exist
88-
const rulesDir = join(process.cwd(), selectedConfig.rulesFolder)
89-
try {
90-
await mkdir(rulesDir, { recursive: true })
91-
} catch {
92-
// Directory might already exist, ignore error
93-
}
94-
95-
// Determine the target file path
96-
const rulesFilePath = join(rulesDir, selectedConfig.rulesFileName)
97-
98-
// Check if file already exists and ask for confirmation (only in interactive mode)
99-
let shouldOverwrite = true
100-
if (!isNonInteractive) {
101-
shouldOverwrite = await this.confirmOverwrite(rulesFilePath)
102-
}
103-
104-
if (!shouldOverwrite) {
105-
this.log(`Skipped ${rulesFilePath}`)
106-
return
107-
}
108-
109-
// Save the rules file
110-
await writeFile(rulesFilePath, rulesContent, 'utf8')
111-
112-
this.log(`✅ Successfully saved Checkly rules file to: ${rulesFilePath}`)
113-
} catch (error) {
114-
this.error(`Failed to generate rules file: ${error}`)
115-
}
116-
}
117-
118-
private async readBaseRulesFile (): Promise<string> {
119-
try {
120-
return await readFile(BASE_RULES_FILE_PATH, 'utf8')
121-
} catch (error) {
122-
throw new Error(
123-
`Failed to read base rules file at ${BASE_RULES_FILE_PATH}: ${error}`,
124-
{ cause: error },
125-
)
126-
}
127-
}
128-
129-
private async confirmOverwrite (targetPath: string): Promise<boolean> {
130-
try {
131-
await access(targetPath, constants.F_OK)
132-
133-
// File exists, ask for confirmation
134-
const { overwrite } = await prompts({
135-
type: 'confirm',
136-
name: 'overwrite',
137-
message: `Rules file already exists at ${targetPath}. Do you want to overwrite it?`,
138-
initial: false,
139-
})
9+
'Deprecated. Use `checkly skills` instead.'
14010

141-
return overwrite ?? false
142-
} catch {
143-
// File doesn't exist, no need to confirm
144-
return true
145-
}
11+
run (): Promise<void> {
12+
this.log('The `rules` command is deprecated. Use `checkly skills` instead.')
13+
return Promise.resolve()
14614
}
14715
}

0 commit comments

Comments
 (0)