@sdods/cli 0.5.2 → 0.7.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/.tsbuildinfo +1 -1
- package/dist/commands/init.js +7 -5
- package/dist/commands/mcp.js +20 -5
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.js +69 -0
- package/dist/commands/users.js +73 -5
- package/dist/program.js +5 -1
- package/dist/skills-catalog.d.ts +46 -0
- package/dist/skills-catalog.js +75 -0
- package/package.json +10 -9
- package/skills/sdods/SKILL.md +69 -0
- package/{templates/.claude/skills → skills}/sdods-record/SKILL.md +20 -17
- package/{templates/.claude/skills → skills}/sdods-run/SKILL.md +19 -16
- package/templates/.claude/skills/sdods-release-channels/SKILL.md +0 -185
- /package/{templates/.claude/skills → skills}/sdods-start-ui/SKILL.md +0 -0
package/dist/commands/init.js
CHANGED
|
@@ -6,6 +6,7 @@ import pc from 'picocolors';
|
|
|
6
6
|
import { OrganizationSchema, SlugSchema, WorkspaceSchema } from '@sdods/contracts';
|
|
7
7
|
import { SdodsError, VERSION, WORKSPACE_FILE } from '@sdods/core';
|
|
8
8
|
import { globalOptions } from '../context.js';
|
|
9
|
+
import { installSkills } from '../skills-catalog.js';
|
|
9
10
|
import { json, ok, out, warn } from '../ui.js';
|
|
10
11
|
/** Root of the SDODS monorepo this CLI runs from (used by --link, which needs a real checkout). */
|
|
11
12
|
export function sourceRepoRoot() {
|
|
@@ -278,11 +279,12 @@ Docs: https://docs.sdods.com
|
|
|
278
279
|
else {
|
|
279
280
|
write('projects/.gitkeep', '');
|
|
280
281
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
// The user-facing skills, for Claude Code (.claude/skills) and every agent that reads the shared
|
|
283
|
+
// .agents/skills. Copied from the bundled set, never from this repository's own .claude/skills,
|
|
284
|
+
// which also holds skills for releasing SDODS itself.
|
|
285
|
+
const skills = installSkills({ rootDir: target });
|
|
286
|
+
if (skills.some((s) => s.status !== 'skipped'))
|
|
287
|
+
files.push('.claude/skills/', '.agents/skills/');
|
|
286
288
|
if (flags.from) {
|
|
287
289
|
try {
|
|
288
290
|
const analyze = (await import('@sdods/core/analyze'));
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
2
|
import { SdodsError } from '@sdods/core';
|
|
3
|
-
import { ALL_CAPABILITIES, DEFAULT_CAPABILITIES } from '@sdods/mcp';
|
|
3
|
+
import { ALL_CAPABILITIES, DEFAULT_CAPABILITIES, MCP_CLIENTS } from '@sdods/mcp';
|
|
4
4
|
import { createContext } from '../context.js';
|
|
5
5
|
import { json, ok, out } from '../ui.js';
|
|
6
|
-
const CLIENTS =
|
|
6
|
+
const CLIENTS = MCP_CLIENTS;
|
|
7
7
|
/**
|
|
8
8
|
* The default capability list, derived rather than restated. It used to be spelled out in three
|
|
9
9
|
* places in this file — the fallback, the flag default and the "did the user change it" sentinel —
|
|
@@ -80,7 +80,7 @@ export function register(program) {
|
|
|
80
80
|
});
|
|
81
81
|
mcp
|
|
82
82
|
.command('install <client>')
|
|
83
|
-
.description('Register the SDODS MCP server with a client: claude | codex | cursor | vscode | windsurf (merges, never clobbers)')
|
|
83
|
+
.description('Register the SDODS MCP server with a client: claude | codex | cursor | vscode | windsurf | gemini (merges, never clobbers)')
|
|
84
84
|
.option('-p, --project <slug>')
|
|
85
85
|
.option('-e, --env <name>')
|
|
86
86
|
.option('--caps <list>')
|
|
@@ -167,12 +167,27 @@ async function registerViaClientCli(client, o, cwd) {
|
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
169
|
};
|
|
170
|
+
// A remote server without credentials answers 401 to every call, so the token is part of the
|
|
171
|
+
// registration. Neither form writes the secret: Claude Code expands ${SDODS_TOKEN} from the
|
|
172
|
+
// environment when it reads .mcp.json, and Codex reads the variable named by
|
|
173
|
+
// --bearer-token-env-var at connect time.
|
|
170
174
|
const addArgs = client === 'claude'
|
|
171
175
|
? o.httpUrl
|
|
172
|
-
? [
|
|
176
|
+
? [
|
|
177
|
+
'mcp',
|
|
178
|
+
'add',
|
|
179
|
+
'-s',
|
|
180
|
+
'project',
|
|
181
|
+
'--transport',
|
|
182
|
+
'http',
|
|
183
|
+
'sdods',
|
|
184
|
+
o.httpUrl,
|
|
185
|
+
'--header',
|
|
186
|
+
'Authorization: Bearer ${SDODS_TOKEN}',
|
|
187
|
+
]
|
|
173
188
|
: ['mcp', 'add', '-s', 'project', 'sdods', '--', ...stdio]
|
|
174
189
|
: o.httpUrl
|
|
175
|
-
? ['mcp', 'add', 'sdods', '--url', o.httpUrl]
|
|
190
|
+
? ['mcp', 'add', 'sdods', '--url', o.httpUrl, '--bearer-token-env-var', 'SDODS_TOKEN']
|
|
176
191
|
: ['mcp', 'add', 'sdods', '--', ...stdio];
|
|
177
192
|
const ok1 = await run(addArgs);
|
|
178
193
|
if (!ok1)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { SdodsError } from '@sdods/core';
|
|
4
|
+
import { createContext } from '../context.js';
|
|
5
|
+
import { DEFAULT_SKILL_TARGETS, SKILL_TARGETS, installSkills, listBundledSkills, } from '../skills-catalog.js';
|
|
6
|
+
import { info, json, ok, out, table } from '../ui.js';
|
|
7
|
+
function parseList(raw) {
|
|
8
|
+
return (raw ?? '')
|
|
9
|
+
.split(',')
|
|
10
|
+
.map((s) => s.trim())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
export function register(program) {
|
|
14
|
+
const skills = program
|
|
15
|
+
.command('skills')
|
|
16
|
+
.description('Agent Skills for Claude Code, Codex, Cursor, Copilot, Gemini CLI and others');
|
|
17
|
+
skills
|
|
18
|
+
.command('list')
|
|
19
|
+
.description('List the skills bundled with SDODS')
|
|
20
|
+
.action((_opts, cmd) => {
|
|
21
|
+
const ctx = createContext(cmd);
|
|
22
|
+
const rows = listBundledSkills().map(({ name, description }) => ({ name, description }));
|
|
23
|
+
if (ctx.opts.json)
|
|
24
|
+
return json(rows);
|
|
25
|
+
table(rows);
|
|
26
|
+
});
|
|
27
|
+
skills
|
|
28
|
+
.command('install')
|
|
29
|
+
.description('Copy the bundled skills where your coding agents read them (default: .claude/skills and .agents/skills)')
|
|
30
|
+
.option('--agent <list>', `comma-separated: ${Object.keys(SKILL_TARGETS).join(', ')} (default ${DEFAULT_SKILL_TARGETS.join(',')})`)
|
|
31
|
+
.option('-g, --global', 'install into your home directory instead of the project')
|
|
32
|
+
.option('--skill <names>', 'only these skills (comma-separated)')
|
|
33
|
+
.option('--force', 'replace skills that are already installed')
|
|
34
|
+
.action((opts, cmd) => {
|
|
35
|
+
const ctx = createContext(cmd);
|
|
36
|
+
const targets = parseList(opts.agent);
|
|
37
|
+
const bad = targets.filter((t) => !(t in SKILL_TARGETS));
|
|
38
|
+
if (bad.length)
|
|
39
|
+
throw new SdodsError('CONFIG_INVALID', `Unknown agent ${bad.join(', ')}.`, {
|
|
40
|
+
hint: `Use one or more of ${Object.keys(SKILL_TARGETS).join(', ')}.`,
|
|
41
|
+
exitCode: 2,
|
|
42
|
+
});
|
|
43
|
+
let results;
|
|
44
|
+
try {
|
|
45
|
+
results = installSkills({
|
|
46
|
+
rootDir: ctx.rootDir,
|
|
47
|
+
targets: targets,
|
|
48
|
+
global: opts.global === true,
|
|
49
|
+
skills: parseList(opts.skill),
|
|
50
|
+
force: opts.force === true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
throw new SdodsError('CONFIG_INVALID', e.message, { exitCode: 2 });
|
|
55
|
+
}
|
|
56
|
+
if (ctx.opts.json)
|
|
57
|
+
return json(results);
|
|
58
|
+
const base = opts.global ? (process.env.HOME ?? '') : ctx.rootDir;
|
|
59
|
+
for (const r of results) {
|
|
60
|
+
const where = relative(base, r.path) || r.path;
|
|
61
|
+
if (r.status === 'skipped')
|
|
62
|
+
out(pc.dim(`- ${where} (already installed; --force replaces it)`));
|
|
63
|
+
else
|
|
64
|
+
ok(`${r.status === 'replaced' ? 'Replaced' : 'Installed'} ${where}`);
|
|
65
|
+
}
|
|
66
|
+
info('Restart or reload your agent so it picks the skills up.');
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=skills.js.map
|
package/dist/commands/users.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { SdodsError } from '@sdods/core';
|
|
2
2
|
import { MIN_PASSWORD_LENGTH } from '@sdods/contracts/names';
|
|
3
3
|
import { createContext } from '../context.js';
|
|
4
|
-
import { json, ok, table } from '../ui.js';
|
|
4
|
+
import { info, json, ok, table, warn } from '../ui.js';
|
|
5
5
|
async function openDb() {
|
|
6
6
|
const db = await import('@sdods/db');
|
|
7
7
|
return db.openDb();
|
|
8
8
|
}
|
|
9
|
+
function assertPassword(password) {
|
|
10
|
+
if (String(password).length < MIN_PASSWORD_LENGTH)
|
|
11
|
+
throw new SdodsError('CONFIG_INVALID', `Password must be at least ${MIN_PASSWORD_LENGTH} characters.`, { exitCode: 2 });
|
|
12
|
+
}
|
|
9
13
|
export function register(program) {
|
|
10
14
|
const users = program
|
|
11
15
|
.command('users')
|
|
@@ -21,10 +25,7 @@ export function register(program) {
|
|
|
21
25
|
.option('--org-owner', 'grant organization owner on orgs without an owner')
|
|
22
26
|
.action(async (opts, cmd) => {
|
|
23
27
|
const ctx = createContext(cmd);
|
|
24
|
-
|
|
25
|
-
throw new SdodsError('CONFIG_INVALID', 'Password must be at least 8 characters.', {
|
|
26
|
-
exitCode: 2,
|
|
27
|
-
});
|
|
28
|
+
assertPassword(opts.password);
|
|
28
29
|
const adb = await openDb();
|
|
29
30
|
try {
|
|
30
31
|
const db = await import('@sdods/db');
|
|
@@ -132,6 +133,73 @@ export function register(program) {
|
|
|
132
133
|
await adb.close();
|
|
133
134
|
}
|
|
134
135
|
});
|
|
136
|
+
users
|
|
137
|
+
.command('set-password <username>')
|
|
138
|
+
.description('Set a password and sign the user out everywhere; --activate re-enables the account')
|
|
139
|
+
.requiredOption('--password <password>', `new password (min ${MIN_PASSWORD_LENGTH} chars)`)
|
|
140
|
+
.option('--activate', 're-enable a deactivated account')
|
|
141
|
+
.action(async (username, opts, cmd) => {
|
|
142
|
+
const ctx = createContext(cmd);
|
|
143
|
+
assertPassword(opts.password);
|
|
144
|
+
const adb = await openDb();
|
|
145
|
+
try {
|
|
146
|
+
const db = await import('@sdods/db');
|
|
147
|
+
const { hashPassword } = await import('@sdods/server');
|
|
148
|
+
const u = await db.getUserByUsername(adb.db, username);
|
|
149
|
+
if (!u)
|
|
150
|
+
throw new SdodsError('CONFIG_NOT_FOUND', `User ${username} not found.`, {
|
|
151
|
+
exitCode: 2,
|
|
152
|
+
});
|
|
153
|
+
await db.updateUser(adb.db, adb.driver, u.id, {
|
|
154
|
+
passwordHash: await hashPassword(opts.password),
|
|
155
|
+
...(opts.activate ? { active: true } : {}),
|
|
156
|
+
});
|
|
157
|
+
await db.deleteSessionsForUser(adb.db, u.id);
|
|
158
|
+
await db.audit(adb.db, {
|
|
159
|
+
actorType: 'cli',
|
|
160
|
+
action: 'user.password',
|
|
161
|
+
targetType: 'user',
|
|
162
|
+
targetId: u.id,
|
|
163
|
+
details: { username, activated: Boolean(opts.activate) },
|
|
164
|
+
});
|
|
165
|
+
const active = opts.activate ? true : u.active;
|
|
166
|
+
if (ctx.opts.json)
|
|
167
|
+
return json({ id: u.id, username, active });
|
|
168
|
+
ok(`Password set for ${username}; existing sessions signed out`);
|
|
169
|
+
if (!active)
|
|
170
|
+
warn(`${username} is deactivated; pass --activate to let them sign in`);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
await adb.close();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
users
|
|
177
|
+
.command('reset')
|
|
178
|
+
.description('Remove every user, session, API token and membership; projects and runs stay (next serve offers /setup)')
|
|
179
|
+
.option('--yes', 'confirm')
|
|
180
|
+
.action(async (opts, cmd) => {
|
|
181
|
+
const ctx = createContext(cmd);
|
|
182
|
+
if (!opts.yes)
|
|
183
|
+
throw new SdodsError('NOT_SUPPORTED', 'users reset removes every user; pass --yes to confirm.', { exitCode: 2 });
|
|
184
|
+
const adb = await openDb();
|
|
185
|
+
try {
|
|
186
|
+
const db = await import('@sdods/db');
|
|
187
|
+
const removed = await db.deleteAllUsers(adb.db);
|
|
188
|
+
await db.audit(adb.db, {
|
|
189
|
+
actorType: 'cli',
|
|
190
|
+
action: 'users.reset',
|
|
191
|
+
targetType: 'user',
|
|
192
|
+
details: { removed },
|
|
193
|
+
});
|
|
194
|
+
if (ctx.opts.json)
|
|
195
|
+
return json({ removed });
|
|
196
|
+
ok(`Removed ${removed} user${removed === 1 ? '' : 's'}; projects, runs and schedules are kept`);
|
|
197
|
+
info('Restart `sdods serve` for a one-time /setup link, or run: sdods users create --admin --username <name> --password <pw>');
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
await adb.close();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
135
203
|
users
|
|
136
204
|
.command('grant <username>')
|
|
137
205
|
.description('Grant a membership role: --org <slug> --role owner|admin|member, or --workspace <slug> --role admin|editor|viewer')
|
package/dist/program.js
CHANGED
|
@@ -26,6 +26,7 @@ import { register as registerTrace } from './commands/trace.js';
|
|
|
26
26
|
import { register as registerAgent } from './commands/agent.js';
|
|
27
27
|
import { register as registerProposals } from './commands/proposals.js';
|
|
28
28
|
import { register as registerMcp } from './commands/mcp.js';
|
|
29
|
+
import { register as registerSkills } from './commands/skills.js';
|
|
29
30
|
import { register as registerServe } from './commands/serve.js';
|
|
30
31
|
import { register as registerUsers } from './commands/users.js';
|
|
31
32
|
import { register as registerTokens } from './commands/tokens.js';
|
|
@@ -50,7 +51,9 @@ export function buildProgram() {
|
|
|
50
51
|
.option('--no-color', 'disable colours')
|
|
51
52
|
.showHelpAfterError('(add --help for usage)')
|
|
52
53
|
.showSuggestionAfterError()
|
|
53
|
-
.configureHelp({ sortSubcommands: true })
|
|
54
|
+
.configureHelp({ sortSubcommands: true })
|
|
55
|
+
// Root help only: 'after' is not inherited by subcommands.
|
|
56
|
+
.addHelpText('after', '\nDocs: https://docs.sdods.com · Sponsor: https://sdods.com/sponsor/');
|
|
54
57
|
registerProjectCommands(program);
|
|
55
58
|
registerEnvCommands(program);
|
|
56
59
|
registerConfigCommands(program);
|
|
@@ -77,6 +80,7 @@ export function buildProgram() {
|
|
|
77
80
|
registerAgent(program);
|
|
78
81
|
registerProposals(program);
|
|
79
82
|
registerMcp(program);
|
|
83
|
+
registerSkills(program);
|
|
80
84
|
registerServe(program);
|
|
81
85
|
registerUsers(program);
|
|
82
86
|
registerTokens(program);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agent Skills SDODS ships to users. `packages/cli/skills/` is the one source: `sdods init`
|
|
3
|
+
* copies it into new workspaces, `sdods skills install` copies it into any project or home
|
|
4
|
+
* directory, the npm tarball carries it, and scripts/build-agent-plugin.ts builds the public
|
|
5
|
+
* skills repository and Claude Code plugin from it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function bundledSkillsDir(): string;
|
|
8
|
+
export interface BundledSkill {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
dir: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function listBundledSkills(dir?: string): BundledSkill[];
|
|
14
|
+
/** The `name:` and `description:` lines of a SKILL.md frontmatter block (single-line values). */
|
|
15
|
+
export declare function frontmatter(text: string): Record<string, string>;
|
|
16
|
+
/**
|
|
17
|
+
* Where each agent family looks for skills, relative to a project root (or to the home directory
|
|
18
|
+
* with `global`). `.agents/skills` is the shared location read by Codex, Cursor, Gemini CLI,
|
|
19
|
+
* GitHub Copilot, OpenCode, Amp and Goose; Claude Code reads `.claude/skills`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SKILL_TARGETS: {
|
|
22
|
+
readonly claude: ".claude/skills";
|
|
23
|
+
readonly agents: ".agents/skills";
|
|
24
|
+
readonly cursor: ".cursor/skills";
|
|
25
|
+
readonly copilot: ".github/skills";
|
|
26
|
+
readonly gemini: ".gemini/skills";
|
|
27
|
+
};
|
|
28
|
+
export type SkillTarget = keyof typeof SKILL_TARGETS;
|
|
29
|
+
export declare const DEFAULT_SKILL_TARGETS: SkillTarget[];
|
|
30
|
+
export interface InstallSkillsOptions {
|
|
31
|
+
rootDir: string;
|
|
32
|
+
targets?: SkillTarget[];
|
|
33
|
+
global?: boolean;
|
|
34
|
+
skills?: string[];
|
|
35
|
+
force?: boolean;
|
|
36
|
+
home?: string;
|
|
37
|
+
source?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface InstalledSkill {
|
|
40
|
+
skill: string;
|
|
41
|
+
target: SkillTarget;
|
|
42
|
+
path: string;
|
|
43
|
+
status: 'installed' | 'replaced' | 'skipped';
|
|
44
|
+
}
|
|
45
|
+
export declare function installSkills(o: InstallSkillsOptions): InstalledSkill[];
|
|
46
|
+
//# sourceMappingURL=skills-catalog.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { cpSync, existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
/**
|
|
6
|
+
* The Agent Skills SDODS ships to users. `packages/cli/skills/` is the one source: `sdods init`
|
|
7
|
+
* copies it into new workspaces, `sdods skills install` copies it into any project or home
|
|
8
|
+
* directory, the npm tarball carries it, and scripts/build-agent-plugin.ts builds the public
|
|
9
|
+
* skills repository and Claude Code plugin from it.
|
|
10
|
+
*/
|
|
11
|
+
export function bundledSkillsDir() {
|
|
12
|
+
// src/skills-catalog.ts and dist/skills-catalog.js both sit one level below packages/cli
|
|
13
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), '..', 'skills');
|
|
14
|
+
}
|
|
15
|
+
export function listBundledSkills(dir = bundledSkillsDir()) {
|
|
16
|
+
if (!existsSync(dir))
|
|
17
|
+
return [];
|
|
18
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
19
|
+
.filter((e) => e.isDirectory() && existsSync(join(dir, e.name, 'SKILL.md')))
|
|
20
|
+
.map((e) => {
|
|
21
|
+
const fm = frontmatter(readFileSync(join(dir, e.name, 'SKILL.md'), 'utf8'));
|
|
22
|
+
return { name: fm.name ?? e.name, description: fm.description ?? '', dir: join(dir, e.name) };
|
|
23
|
+
})
|
|
24
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
25
|
+
}
|
|
26
|
+
/** The `name:` and `description:` lines of a SKILL.md frontmatter block (single-line values). */
|
|
27
|
+
export function frontmatter(text) {
|
|
28
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const line of (m?.[1] ?? '').split(/\r?\n/)) {
|
|
31
|
+
const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
32
|
+
if (kv)
|
|
33
|
+
out[kv[1]] = kv[2].replace(/^['"]|['"]$/g, '');
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Where each agent family looks for skills, relative to a project root (or to the home directory
|
|
39
|
+
* with `global`). `.agents/skills` is the shared location read by Codex, Cursor, Gemini CLI,
|
|
40
|
+
* GitHub Copilot, OpenCode, Amp and Goose; Claude Code reads `.claude/skills`.
|
|
41
|
+
*/
|
|
42
|
+
export const SKILL_TARGETS = {
|
|
43
|
+
claude: '.claude/skills',
|
|
44
|
+
agents: '.agents/skills',
|
|
45
|
+
cursor: '.cursor/skills',
|
|
46
|
+
copilot: '.github/skills',
|
|
47
|
+
gemini: '.gemini/skills',
|
|
48
|
+
};
|
|
49
|
+
export const DEFAULT_SKILL_TARGETS = ['claude', 'agents'];
|
|
50
|
+
/** GitHub Copilot's user-level directory is `~/.copilot/skills`, not `~/.github/skills`. */
|
|
51
|
+
const GLOBAL_OVERRIDES = { copilot: '.copilot/skills' };
|
|
52
|
+
export function installSkills(o) {
|
|
53
|
+
const available = listBundledSkills(o.source);
|
|
54
|
+
const wanted = o.skills?.length ? o.skills : available.map((s) => s.name);
|
|
55
|
+
const unknown = wanted.filter((w) => !available.some((s) => s.name === w));
|
|
56
|
+
if (unknown.length)
|
|
57
|
+
throw new Error(`Unknown skill ${unknown.join(', ')}. Bundled: ${available.map((s) => s.name).join(', ')}`);
|
|
58
|
+
const base = o.global ? (o.home ?? homedir()) : o.rootDir;
|
|
59
|
+
const results = [];
|
|
60
|
+
for (const target of o.targets?.length ? o.targets : DEFAULT_SKILL_TARGETS) {
|
|
61
|
+
const rel = (o.global && GLOBAL_OVERRIDES[target]) || SKILL_TARGETS[target];
|
|
62
|
+
for (const skill of available.filter((s) => wanted.includes(s.name))) {
|
|
63
|
+
const path = join(base, rel, skill.name);
|
|
64
|
+
const exists = existsSync(join(path, 'SKILL.md'));
|
|
65
|
+
if (exists && !o.force) {
|
|
66
|
+
results.push({ skill: skill.name, target, path, status: 'skipped' });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
cpSync(skill.dir, path, { recursive: true, force: true });
|
|
70
|
+
results.push({ skill: skill.name, target, path, status: exists ? 'replaced' : 'installed' });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=skills-catalog.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "SDODS command line: run, record, analyze, agents, MCP server, scheduler and the web server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SDODS <admin@sdods.com>",
|
|
@@ -17,14 +17,15 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
19
|
"bin",
|
|
20
|
-
"templates"
|
|
20
|
+
"templates",
|
|
21
|
+
"skills"
|
|
21
22
|
],
|
|
22
23
|
"engines": {
|
|
23
24
|
"node": ">=22"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@sdods/contracts": "0.
|
|
27
|
-
"@sdods/core": "0.
|
|
27
|
+
"@sdods/contracts": "0.7.0",
|
|
28
|
+
"@sdods/core": "0.7.0",
|
|
28
29
|
"commander": "^15.0.0",
|
|
29
30
|
"execa": "^10.0.1",
|
|
30
31
|
"picocolors": "^1.1.1",
|
|
@@ -33,12 +34,12 @@
|
|
|
33
34
|
"zod": "^4.5.4",
|
|
34
35
|
"@playwright/test": "^1.63.0",
|
|
35
36
|
"playwright-core": "^1.63.0",
|
|
36
|
-
"@sdods/integrations": "0.
|
|
37
|
-
"@sdods/mcp": "0.
|
|
38
|
-
"@sdods/agents": "0.
|
|
39
|
-
"@sdods/db": "0.
|
|
37
|
+
"@sdods/integrations": "0.7.0",
|
|
38
|
+
"@sdods/mcp": "0.7.0",
|
|
39
|
+
"@sdods/agents": "0.7.0",
|
|
40
|
+
"@sdods/db": "0.7.0",
|
|
40
41
|
"csv-parse": "^7.0.2",
|
|
41
|
-
"@sdods/server": "0.
|
|
42
|
+
"@sdods/server": "0.7.0"
|
|
42
43
|
},
|
|
43
44
|
"homepage": "https://sdods.com",
|
|
44
45
|
"bugs": {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sdods
|
|
3
|
+
description: Work in an SDODS test-automation workspace — BDD features for UI, API and hybrid flows. Use when writing, fixing or reviewing Gherkin features, steps, page objects or project/env YAML, running suites, reading results, or using the sdods CLI or its MCP tools.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SDODS
|
|
7
|
+
|
|
8
|
+
SDODS runs BDD scenarios for UI, API and hybrid flows from one workspace. A workspace has
|
|
9
|
+
`sdods.workspace.yaml` at its root; each project is `projects/<slug>/sdods.project.yaml` plus
|
|
10
|
+
`envs/<env>.yaml`, and features live in `projects/<slug>/features/<module>/`.
|
|
11
|
+
|
|
12
|
+
Commands below use `sdods`. Without a global install use `npx -y @sdods/cli` in its place; inside
|
|
13
|
+
an SDODS source checkout use `bun run sdods`. When the `sdods` MCP server is connected, prefer its
|
|
14
|
+
tools (`project_list`, `feature_list`, `step_find`, `run_tests`, `run_get`, `analyze_failure`, …)
|
|
15
|
+
over shelling out.
|
|
16
|
+
|
|
17
|
+
## Rules
|
|
18
|
+
|
|
19
|
+
- Every scenario carries exactly one layer tag (`@ui`, `@api`, `@hybrid`) and exactly one suite tag
|
|
20
|
+
(`@smoke`, `@regression`, `@sanity`). Optional: `@visual @a11y @perf @mock @data-driven @pool`,
|
|
21
|
+
and value tags `@user:<role> @data:<dataset> @har:<name> @jira:KEY @skip:<browser>`.
|
|
22
|
+
- Reuse existing steps first (`sdods steps list -p <slug>`, or `step_find`). Add a step only when no
|
|
23
|
+
phrasing fits, and name it in the same style.
|
|
24
|
+
- Locator priority: role + name > label > test id > placeholder > text > CSS. Never XPath.
|
|
25
|
+
- Page objects use step decorators (`@Fixture`, `@Given/@When/@Then`) and heal-aware locators:
|
|
26
|
+
`this.heal.locator(primary, { role, name, testId, description })`.
|
|
27
|
+
- Keep scenarios independent and deterministic, with no sleeps. Seed through the API rather than the
|
|
28
|
+
UI, and clean up what you create.
|
|
29
|
+
- Secrets are never literals; reference `${VAR}`.
|
|
30
|
+
- When acting through SDODS agents or MCP write tools, changes are proposals (`feature_write`,
|
|
31
|
+
`proposals`) that a person accepts with `sdods proposals accept <id>`.
|
|
32
|
+
|
|
33
|
+
## Quick reference
|
|
34
|
+
|
|
35
|
+
| Task | Command |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| Projects and hierarchy | `sdods project list`, `sdods workspace tree` |
|
|
38
|
+
| Resolved config, with where each value came from | `sdods config show -p <slug> -e <env> --explain` |
|
|
39
|
+
| Validate features | `sdods lint -p <slug>` |
|
|
40
|
+
| Run a slice | `sdods run -p <slug> -e <env> -l api` · `-l ui -b chromium -t @smoke` · `--process pr-check` |
|
|
41
|
+
| Results | `sdods report --last`, `sdods heal report --last`, `sdods trace --last` |
|
|
42
|
+
| Available steps | `sdods steps list -p <slug>` |
|
|
43
|
+
| Record and replay | `sdods record -p <slug> -e <env> --user <role> --name <name>` |
|
|
44
|
+
| Agents | `sdods agent plan\|generate\|heal\|upgrade\|review -p <slug> --dry-run`, `sdods proposals list` |
|
|
45
|
+
| Health check | `sdods doctor` |
|
|
46
|
+
|
|
47
|
+
## Writing a scenario
|
|
48
|
+
|
|
49
|
+
1. Pick the module directory under `features/` and tag the Feature with one layer and one suite tag.
|
|
50
|
+
2. Reuse steps from `sdods steps list`; add project steps in `steps/<module>.steps.ts` only if needed.
|
|
51
|
+
3. Get data from datasets (`Given I load dataset "users" row 1`) or `@user:<role>`, never literals.
|
|
52
|
+
4. `sdods lint -p <slug>`, then run just that scenario and check the before/after screenshots in
|
|
53
|
+
`.sdods/runs/<runId>/`.
|
|
54
|
+
|
|
55
|
+
## Verify before claiming done
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
sdods lint -p <slug>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
sdods run -p <slug> -e <env> -l api
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
sdods run -p <slug> -e <env> -l ui -b chromium -t @smoke
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Exit codes: `0` passed · `1` test failures · `2` configuration or usage error · `3` lint errors.
|
|
@@ -7,12 +7,15 @@ description: Record a browser flow with SDODS for a given project, environment,
|
|
|
7
7
|
|
|
8
8
|
Re-runnable recipe. Ask for the parameters you do not have, then follow the steps. Never edit `main` directly; recordings land in `projects/<project>/recorded/` and conversions become proposals.
|
|
9
9
|
|
|
10
|
+
Commands below use `sdods`. Without a global install use `npx -y @sdods/cli` in its place; inside
|
|
11
|
+
an SDODS source checkout use `bun run sdods`.
|
|
12
|
+
|
|
10
13
|
## Parameters
|
|
11
14
|
|
|
12
15
|
| Name | Required | Example | Notes |
|
|
13
16
|
|---|---|---|---|
|
|
14
|
-
| `project` | yes | `demo-shop` | `
|
|
15
|
-
| `env` | yes | `staging`, `local` | `
|
|
17
|
+
| `project` | yes | `demo-shop` | `sdods project list` shows slugs |
|
|
18
|
+
| `env` | yes | `staging`, `local` | `sdods env list -p <project>` |
|
|
16
19
|
| `name` | yes | `checkout-standard` | becomes `recorded/<name>.spec.ts`; include the role in the name |
|
|
17
20
|
| `role` | no | `standard`, `problem`, `admin` | leases a pool user of that role and starts logged in (storageState) |
|
|
18
21
|
| `url` | no | `/inventory.html` | route path or absolute URL to open first |
|
|
@@ -24,38 +27,38 @@ Re-runnable recipe. Ask for the parameters you do not have, then follow the step
|
|
|
24
27
|
|
|
25
28
|
1. **Check the environment** (fail fast on missing vars):
|
|
26
29
|
```bash
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
sdods doctor -p <project>
|
|
31
|
+
sdods env list -p <project>
|
|
29
32
|
```
|
|
30
33
|
2. **Make sure the role has login state** (skip when no role):
|
|
31
34
|
```bash
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
sdods auth list -p <project> -e <env>
|
|
36
|
+
sdods auth capture -p <project> -e <env> --user <role> # only if missing or expired
|
|
34
37
|
```
|
|
35
38
|
3. **Record** (opens the recorder; the person performs the flow and closes the window):
|
|
36
39
|
```bash
|
|
37
|
-
|
|
40
|
+
sdods record -p <project> -e <env> --name <name> [--user <role>] [--url <url>] [--device "<device>"] [--browser <browser>] [--save-har]
|
|
38
41
|
```
|
|
39
42
|
Output: `projects/<project>/recorded/<name>.spec.ts`, post-processed (SDODS fixtures, routes instead of absolute URLs, tags `@recorded @ui @regression`, header comment with project/env/role/device).
|
|
40
43
|
4. **Play it back** on the same environment, then on another one:
|
|
41
44
|
```bash
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
sdods run -p <project> -e <env> -l recorded --grep "<name>"
|
|
46
|
+
sdods run -p <project> -e <other-env> -l recorded --grep "<name>"
|
|
44
47
|
```
|
|
45
|
-
Look at `.sdods/runs/<runId>/` for screenshots and the HTML report (`
|
|
48
|
+
Look at `.sdods/runs/<runId>/` for screenshots and the HTML report (`sdods report --last --open`).
|
|
46
49
|
5. **Repeat for other roles, environments or devices** by changing only the parameters. Use distinct names (`checkout-standard`, `checkout-problem`, `checkout-standard-iphone`).
|
|
47
50
|
6. **Convert to Gherkin** when the flow is stable (agent proposal, reviewed by a person):
|
|
48
51
|
```bash
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
sdods record convert projects/<project>/recorded/<name>.spec.ts
|
|
53
|
+
sdods proposals list
|
|
54
|
+
sdods proposals show <id>
|
|
55
|
+
sdods proposals accept <id> --branch sdods/<id> # after review
|
|
56
|
+
sdods lint -p <project>
|
|
54
57
|
```
|
|
55
|
-
The proposal reuses existing steps first (`
|
|
58
|
+
The proposal reuses existing steps first (`sdods steps list -p <project>`), adds only unmatched steps, and creates a page object with heal-aware locators.
|
|
56
59
|
7. **Offline replay** (CI-friendly) when HAR was captured:
|
|
57
60
|
```bash
|
|
58
|
-
|
|
61
|
+
sdods har replay -p <project> -e <env> --strict -t "@har:<name>"
|
|
59
62
|
```
|
|
60
63
|
|
|
61
64
|
## Matrix template
|
|
@@ -5,12 +5,15 @@ description: Run SDODS test suites for a project across environments, layers (ui
|
|
|
5
5
|
|
|
6
6
|
# SDODS run skill
|
|
7
7
|
|
|
8
|
+
Commands below use `sdods`. Without a global install use `npx -y @sdods/cli` in its place; inside
|
|
9
|
+
an SDODS source checkout use `bun run sdods`.
|
|
10
|
+
|
|
8
11
|
## Parameters
|
|
9
12
|
|
|
10
13
|
| Name | Default | Notes |
|
|
11
14
|
|---|---|---|
|
|
12
|
-
| `project` | only project if one exists | `
|
|
13
|
-
| `env` | project `envs.default` | `
|
|
15
|
+
| `project` | only project if one exists | `sdods project list` |
|
|
16
|
+
| `env` | project `envs.default` | `sdods env list -p <project>` |
|
|
14
17
|
| `layer` | all layers in the project | `-l ui`, `-l api`, `-l hybrid`, `-l recorded` (repeatable) |
|
|
15
18
|
| `browser` | project `browsers` | `-b chromium -b edge -b firefox -b webkit`, or `--project-matrix` for all |
|
|
16
19
|
| `tags` | none | Cucumber expression, e.g. `"@smoke and not @mock"`, `@user:admin`, `@jira:DEMO-12` |
|
|
@@ -23,29 +26,29 @@ description: Run SDODS test suites for a project across environments, layers (ui
|
|
|
23
26
|
|
|
24
27
|
1. Validate before running (cheap, catches config and tag mistakes):
|
|
25
28
|
```bash
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
sdods config validate
|
|
30
|
+
sdods lint -p <project>
|
|
31
|
+
sdods run -p <project> -e <env> --list # shows the run targets that would run
|
|
29
32
|
```
|
|
30
33
|
2. Run the requested slice. Typical combinations:
|
|
31
34
|
```bash
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
sdods run -p <project> -e <env> -l api # API only, no browser
|
|
36
|
+
sdods run -p <project> -e <env> -l ui -b chromium -t @smoke # quick UI smoke
|
|
37
|
+
sdods run -p <project> -e <env> -t @regression --project-matrix # every browser in the yaml
|
|
38
|
+
sdods run -p <project> -e <env> -t "@user:admin" # scenarios that lease admin users
|
|
39
|
+
sdods run -p <project> -e <env> -l hybrid -w 2 # API→UI chaining
|
|
40
|
+
sdods run -p <project> -e <env> --har-replay --strict -t @smoke # offline, CI-style
|
|
38
41
|
```
|
|
39
42
|
3. Read results:
|
|
40
43
|
```bash
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
sdods report --last # summary: totals, failed, flaky, report paths
|
|
45
|
+
sdods report --last --open # HTML report + dashboard
|
|
46
|
+
sdods heal report --last # locators that were healed
|
|
44
47
|
```
|
|
45
48
|
Artifacts: `.sdods/runs/<runId>/<project>/<fingerprint>/r<retry>/` holds `scenario-start.png`, `NN-before.png`/`NN-after.png` per UI step (policy by suite tag), `api/*.json` per API call, `heal.jsonl`.
|
|
46
|
-
4. If something failed, collect: the scenario name, the error from `summary.json`, the `failure.png`, and the trace (`
|
|
49
|
+
4. If something failed, collect: the scenario name, the error from `summary.json`, the `failure.png`, and the trace (`sdods trace --last`). Then either fix the feature/page object or hand it to the healer agent:
|
|
47
50
|
```bash
|
|
48
|
-
|
|
51
|
+
sdods agent heal -p <project> --scenario <fingerprint> --dry-run
|
|
49
52
|
```
|
|
50
53
|
5. Repeat for other environments or roles by changing `-e` / `-t "@user:<role>"` only.
|
|
51
54
|
|