@sdods/cli 0.19.0 → 0.21.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/agent.js +11 -7
- package/dist/commands/doctor.js +24 -2
- package/dist/commands/mcp.js +32 -19
- package/dist/commands/record.js +1 -1
- package/dist/commands/upgrade.d.ts +47 -2
- package/dist/commands/upgrade.js +223 -48
- package/dist/install-method.d.ts +29 -0
- package/dist/install-method.js +62 -0
- package/dist/skills-catalog.d.ts +7 -0
- package/dist/skills-catalog.js +14 -2
- package/package.json +8 -8
- package/skills/sdods-start-ui/SKILL.md +4 -9
package/dist/commands/agent.js
CHANGED
|
@@ -36,7 +36,7 @@ export function register(program) {
|
|
|
36
36
|
.option('--run-id <id>', 'run id (heal), default last', 'last')
|
|
37
37
|
.option('--diff <range>', 'git range (upgrade), e.g. main..HEAD')
|
|
38
38
|
.option('--files <list>', 'feature files (review)', collect, [])
|
|
39
|
-
.option('--adapter <name>', 'claude (API key) | claude-code (logged-in Claude Code CLI) | codex (logged-in Codex CLI) | openai | ollama (a model on this machine) | fake; default: auto-detect')
|
|
39
|
+
.option('--adapter <name>', 'claude (API key) | claude-code (logged-in Claude Code CLI) | codex (logged-in Codex CLI) | copilot (logged-in GitHub Copilot CLI) | openai | ollama (a model on this machine) | fake; default: auto-detect')
|
|
40
40
|
.option('--model <id>', 'model override')
|
|
41
41
|
.option('--profile <name>', 'auto | full | small — how many tools and how much prompt the model is given')
|
|
42
42
|
.option('--browser', 'attach the browser MCP server even under the small profile (which omits it)')
|
|
@@ -162,13 +162,13 @@ export function register(program) {
|
|
|
162
162
|
const installAction = async (opts, cmd, forced) => {
|
|
163
163
|
const ctx = createContext(cmd);
|
|
164
164
|
const target = (forced ?? opts.for ?? 'all');
|
|
165
|
-
if (!['claude', 'codex', 'all'].includes(target)) {
|
|
165
|
+
if (!['claude', 'codex', 'copilot', 'all'].includes(target)) {
|
|
166
166
|
throw new SdodsError('NOT_SUPPORTED', `Unknown target "${target}".`, {
|
|
167
|
-
hint: 'Use --for claude, --for codex or --for all.',
|
|
167
|
+
hint: 'Use --for claude, --for codex, --for copilot or --for all.',
|
|
168
168
|
exitCode: 2,
|
|
169
169
|
});
|
|
170
170
|
}
|
|
171
|
-
const { installCodingAgents } = await import('@sdods/agents');
|
|
171
|
+
const { installCodingAgents, findOnPath } = await import('@sdods/agents');
|
|
172
172
|
const written = installCodingAgents(ctx.rootDir, {
|
|
173
173
|
for: target,
|
|
174
174
|
project: opts.project,
|
|
@@ -177,7 +177,11 @@ export function register(program) {
|
|
|
177
177
|
});
|
|
178
178
|
if (opts.mcp !== false) {
|
|
179
179
|
const { execa } = await import('execa');
|
|
180
|
-
|
|
180
|
+
// `all` registers with Copilot only when it is installed: its config lives in the home
|
|
181
|
+
// directory, and writing one for someone who does not use Copilot would be clutter.
|
|
182
|
+
const clients = target === 'all'
|
|
183
|
+
? ['claude', 'codex', ...(findOnPath('copilot') ? ['copilot'] : [])]
|
|
184
|
+
: [target];
|
|
181
185
|
for (const client of clients) {
|
|
182
186
|
const args = ['mcp', 'install', client];
|
|
183
187
|
if (opts.project)
|
|
@@ -200,8 +204,8 @@ export function register(program) {
|
|
|
200
204
|
};
|
|
201
205
|
agent
|
|
202
206
|
.command('install')
|
|
203
|
-
.description('Set up coding-agent CLIs: --for claude writes .claude/agents/sdods-*.md + CLAUDE.md, --for codex writes AGENTS.md;
|
|
204
|
-
.option('--for <client>', 'claude | codex | all', 'all')
|
|
207
|
+
.description('Set up coding-agent CLIs: --for claude writes .claude/agents/sdods-*.md + CLAUDE.md, --for codex writes AGENTS.md, --for copilot writes .github/agents/sdods-*.agent.md + AGENTS.md; all get AGENT.md/SKILL.md and the MCP registration')
|
|
208
|
+
.option('--for <client>', 'claude | codex | copilot | all', 'all')
|
|
205
209
|
.option('-p, --project <slug>')
|
|
206
210
|
.option('-e, --env <name>')
|
|
207
211
|
.option('--force', 'overwrite existing files')
|
package/dist/commands/doctor.js
CHANGED
|
@@ -173,6 +173,7 @@ function tokenMatrix(cliStatus, localLlm) {
|
|
|
173
173
|
const has = (k) => Boolean(process.env[k]);
|
|
174
174
|
const claudeOk = cliStatus.find((c) => c.name === 'cli:claude')?.ok ?? false;
|
|
175
175
|
const codexOk = cliStatus.find((c) => c.name === 'cli:codex')?.ok ?? false;
|
|
176
|
+
const copilotOk = cliStatus.find((c) => c.name === 'cli:copilot')?.ok ?? false;
|
|
176
177
|
const agentsGroup = [
|
|
177
178
|
{
|
|
178
179
|
name: 'ANTHROPIC_API_KEY',
|
|
@@ -192,6 +193,12 @@ function tokenMatrix(cliStatus, localLlm) {
|
|
|
192
193
|
detail: 'adapter codex: uses your Codex login (ChatGPT account)',
|
|
193
194
|
hint: '`npm i -g @openai/codex && codex login`',
|
|
194
195
|
},
|
|
196
|
+
{
|
|
197
|
+
name: 'copilot CLI login',
|
|
198
|
+
present: copilotOk,
|
|
199
|
+
detail: 'adapter copilot: uses your GitHub Copilot subscription',
|
|
200
|
+
hint: '`npm i -g @github/copilot && copilot login` (or COPILOT_GITHUB_TOKEN)',
|
|
201
|
+
},
|
|
195
202
|
{
|
|
196
203
|
name: 'OPENAI_API_KEY (+OPENAI_BASE_URL)',
|
|
197
204
|
present: has('OPENAI_API_KEY'),
|
|
@@ -296,11 +303,11 @@ async function localModelStatus() {
|
|
|
296
303
|
return { name, ok: false, optional: true, detail: 'agents package unavailable' };
|
|
297
304
|
}
|
|
298
305
|
}
|
|
299
|
-
/** `cli:claude` / `cli:codex` rows: installed + logged in. Missing CLIs are not failures. */
|
|
306
|
+
/** `cli:claude` / `cli:codex` / `cli:copilot` rows: installed + logged in. Missing CLIs are not failures. */
|
|
300
307
|
async function codingCliStatus() {
|
|
301
308
|
const rows = [];
|
|
302
309
|
try {
|
|
303
|
-
const { ClaudeCodeCliAdapter, CodexCliAdapter } = await import('@sdods/agents');
|
|
310
|
+
const { ClaudeCodeCliAdapter, CodexCliAdapter, CopilotCliAdapter } = await import('@sdods/agents');
|
|
304
311
|
const c = ClaudeCodeCliAdapter.loginStatus();
|
|
305
312
|
rows.push({
|
|
306
313
|
name: 'cli:claude',
|
|
@@ -317,6 +324,15 @@ async function codingCliStatus() {
|
|
|
317
324
|
detail: x.detail,
|
|
318
325
|
fix: x.installed ? 'codex login' : 'npm i -g @openai/codex (optional)',
|
|
319
326
|
});
|
|
327
|
+
const g = CopilotCliAdapter.loginStatus();
|
|
328
|
+
rows.push({
|
|
329
|
+
name: 'cli:copilot',
|
|
330
|
+
// Copilot cannot report its login without a prompt; installed counts until a run says otherwise
|
|
331
|
+
ok: g.installed && g.loggedIn !== false,
|
|
332
|
+
optional: true,
|
|
333
|
+
detail: g.detail,
|
|
334
|
+
fix: g.installed ? 'copilot login' : 'npm i -g @github/copilot (optional)',
|
|
335
|
+
});
|
|
320
336
|
}
|
|
321
337
|
catch {
|
|
322
338
|
rows.push({
|
|
@@ -331,6 +347,12 @@ async function codingCliStatus() {
|
|
|
331
347
|
optional: true,
|
|
332
348
|
detail: 'agents package unavailable',
|
|
333
349
|
});
|
|
350
|
+
rows.push({
|
|
351
|
+
name: 'cli:copilot',
|
|
352
|
+
ok: false,
|
|
353
|
+
optional: true,
|
|
354
|
+
detail: 'agents package unavailable',
|
|
355
|
+
});
|
|
334
356
|
}
|
|
335
357
|
return rows;
|
|
336
358
|
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -81,14 +81,14 @@ export function register(program) {
|
|
|
81
81
|
});
|
|
82
82
|
mcp
|
|
83
83
|
.command('install <client>')
|
|
84
|
-
.description('Register the SDODS MCP server with a client: claude | codex | cursor | vscode | windsurf | gemini (merges, never clobbers)')
|
|
84
|
+
.description('Register the SDODS MCP server with a client: claude | codex | copilot | cursor | vscode | windsurf | gemini (merges, never clobbers)')
|
|
85
85
|
.option('-p, --project <slug>')
|
|
86
86
|
.option('-e, --env <name>')
|
|
87
87
|
.option('--caps <list>')
|
|
88
88
|
.option('--http-url <url>', 'configure the HTTP transport instead of stdio')
|
|
89
89
|
.option('--token-placeholder <text>', 'placeholder written for the bearer token')
|
|
90
90
|
.option('--with-playwright', 'also register the raw upstream Playwright MCP server (ungoverned; the sdods server already wraps it as browser_*)')
|
|
91
|
-
.option('--file', 'always write the config file instead of calling `claude
|
|
91
|
+
.option('--file', 'always write the config file instead of calling `claude` / `codex` / `copilot mcp add`')
|
|
92
92
|
.option('--print', 'print the snippet instead of writing files')
|
|
93
93
|
.action(async (client, opts, cmd) => {
|
|
94
94
|
const ctx = createContext(cmd);
|
|
@@ -120,11 +120,11 @@ export function register(program) {
|
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
122
|
// Prefer the client's own CLI so its config format stays authoritative.
|
|
123
|
-
if (!opts.file && (client === 'claude' || client === 'codex')) {
|
|
123
|
+
if (!opts.file && (client === 'claude' || client === 'codex' || client === 'copilot')) {
|
|
124
124
|
const done = await registerViaClientCli(client, o, ctx.rootDir);
|
|
125
125
|
if (done) {
|
|
126
126
|
ok(`Registered the sdods MCP server with ${client} (${cliCommands(o)[client]})`);
|
|
127
|
-
if (o.withPlaywright === true && client
|
|
127
|
+
if (o.withPlaywright === true && client !== 'claude')
|
|
128
128
|
out(pc.dim('Also registered the bundled Playwright MCP server as "playwright".'));
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
@@ -135,7 +135,7 @@ export function register(program) {
|
|
|
135
135
|
out(pc.dim(`Alternative: ${s.cli}`));
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
|
-
/** `claude mcp add` (project scope) or `
|
|
138
|
+
/** `claude mcp add` (project scope), `codex mcp add` or `copilot mcp add` (user config); false when the CLI is unavailable. */
|
|
139
139
|
async function registerViaClientCli(client, o, cwd) {
|
|
140
140
|
const { execa } = await import('execa');
|
|
141
141
|
// `npx sdods` resolves only when the cwd happens to sit inside a checkout that has the binary in
|
|
@@ -148,7 +148,7 @@ async function registerViaClientCli(client, o, cwd) {
|
|
|
148
148
|
stdio.push('--env', o.env);
|
|
149
149
|
if (o.caps)
|
|
150
150
|
stdio.push('--caps', o.caps);
|
|
151
|
-
const run = async (args) => {
|
|
151
|
+
const run = async (args, name = 'sdods') => {
|
|
152
152
|
try {
|
|
153
153
|
await execa(client, args, { cwd, timeout: 30_000 });
|
|
154
154
|
return true;
|
|
@@ -159,7 +159,7 @@ async function registerViaClientCli(client, o, cwd) {
|
|
|
159
159
|
return false;
|
|
160
160
|
// already registered → remove and re-add so the args are current
|
|
161
161
|
if (/already exists/i.test(msg)) {
|
|
162
|
-
await execa(client, ['mcp', 'remove',
|
|
162
|
+
await execa(client, ['mcp', 'remove', name], { cwd, reject: false });
|
|
163
163
|
await execa(client, args, { cwd, timeout: 30_000 });
|
|
164
164
|
return true;
|
|
165
165
|
}
|
|
@@ -170,26 +170,39 @@ async function registerViaClientCli(client, o, cwd) {
|
|
|
170
170
|
};
|
|
171
171
|
// A remote server without credentials answers 401 to every call, so the token is part of the
|
|
172
172
|
// registration. Neither form writes the secret: Claude Code expands ${SDODS_TOKEN} from the
|
|
173
|
-
// environment when it reads .mcp.json,
|
|
174
|
-
// --bearer-token-env-var at connect time.
|
|
175
|
-
const addArgs = client === '
|
|
173
|
+
// environment when it reads .mcp.json, Copilot keeps the ${SDODS_TOKEN} reference as written,
|
|
174
|
+
// and Codex reads the variable named by --bearer-token-env-var at connect time.
|
|
175
|
+
const addArgs = client === 'copilot'
|
|
176
176
|
? o.httpUrl
|
|
177
177
|
? [
|
|
178
178
|
'mcp',
|
|
179
179
|
'add',
|
|
180
|
-
'-s',
|
|
181
|
-
'project',
|
|
182
180
|
'--transport',
|
|
183
181
|
'http',
|
|
184
|
-
'sdods',
|
|
185
|
-
o.httpUrl,
|
|
186
182
|
'--header',
|
|
187
183
|
'Authorization: Bearer ${SDODS_TOKEN}',
|
|
184
|
+
'sdods',
|
|
185
|
+
o.httpUrl,
|
|
188
186
|
]
|
|
189
|
-
: ['mcp', 'add', '
|
|
190
|
-
:
|
|
191
|
-
?
|
|
192
|
-
|
|
187
|
+
: ['mcp', 'add', 'sdods', '--', ...stdio]
|
|
188
|
+
: client === 'claude'
|
|
189
|
+
? o.httpUrl
|
|
190
|
+
? [
|
|
191
|
+
'mcp',
|
|
192
|
+
'add',
|
|
193
|
+
'-s',
|
|
194
|
+
'project',
|
|
195
|
+
'--transport',
|
|
196
|
+
'http',
|
|
197
|
+
'sdods',
|
|
198
|
+
o.httpUrl,
|
|
199
|
+
'--header',
|
|
200
|
+
'Authorization: Bearer ${SDODS_TOKEN}',
|
|
201
|
+
]
|
|
202
|
+
: ['mcp', 'add', '-s', 'project', 'sdods', '--', ...stdio]
|
|
203
|
+
: o.httpUrl
|
|
204
|
+
? ['mcp', 'add', 'sdods', '--url', o.httpUrl, '--bearer-token-env-var', 'SDODS_TOKEN']
|
|
205
|
+
: ['mcp', 'add', 'sdods', '--', ...stdio];
|
|
193
206
|
const ok1 = await run(addArgs);
|
|
194
207
|
if (!ok1)
|
|
195
208
|
return false;
|
|
@@ -208,7 +221,7 @@ async function registerViaClientCli(client, o, cwd) {
|
|
|
208
221
|
'--headless',
|
|
209
222
|
]
|
|
210
223
|
: ['mcp', 'add', 'playwright', '--', 'npx', 'playwright', 'mcp', '--headless'];
|
|
211
|
-
await run(pw).catch(() => false);
|
|
224
|
+
await run(pw, 'playwright').catch(() => false);
|
|
212
225
|
}
|
|
213
226
|
return true;
|
|
214
227
|
}
|
package/dist/commands/record.js
CHANGED
|
@@ -132,7 +132,7 @@ export function register(program) {
|
|
|
132
132
|
.description('Ask the generator agent to turn a recorded spec into a feature + steps proposal (reviewed, never auto-applied)')
|
|
133
133
|
.option('-p, --project <slug>', 'project slug (default: inferred from the spec path)')
|
|
134
134
|
.option('--dry-run', 'print the prompt and plan without calling a model')
|
|
135
|
-
.option('--adapter <name>', 'claude | claude-code | codex | openai | ollama | fake; default: auto-detect')
|
|
135
|
+
.option('--adapter <name>', 'claude | claude-code | codex | copilot | openai | ollama | fake; default: auto-detect')
|
|
136
136
|
.option('--model <id>', 'model override')
|
|
137
137
|
.option('--profile <name>', 'auto | full | small — how much of the platform the model is shown')
|
|
138
138
|
.action(async (spec, opts, cmd) => {
|
|
@@ -1,14 +1,59 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Command } from 'commander';
|
|
2
|
+
import { type InstallInfo } from '../install-method.js';
|
|
2
3
|
export declare const SDODS_PACKAGES: readonly ["@sdods/cli", "@sdods/core", "@sdods/contracts", "@sdods/db", "@sdods/mcp", "@sdods/agents", "@sdods/integrations", "@sdods/server"];
|
|
4
|
+
export type UpgradeStatus = 'up-to-date' | 'outdated' | 'not-installed' | 'unavailable';
|
|
3
5
|
export interface UpgradeRow {
|
|
4
6
|
package: string;
|
|
5
7
|
installed: string | null;
|
|
6
8
|
latest: string | null;
|
|
7
|
-
status:
|
|
9
|
+
status: UpgradeStatus;
|
|
8
10
|
}
|
|
9
11
|
export declare function installedVersion(rootDir: string, pkg: string): string | null;
|
|
10
12
|
export declare function latestVersion(pkg: string, fetchImpl?: typeof fetch, registry?: string): Promise<string | null>;
|
|
13
|
+
export declare function compare(installed: string | null, latest: string | null): UpgradeStatus;
|
|
14
|
+
/** The @sdods/* packages a project depends on, compared with the registry. */
|
|
11
15
|
export declare function checkUpgrades(rootDir: string, fetchImpl?: typeof fetch): Promise<UpgradeRow[]>;
|
|
16
|
+
export interface CliStatus extends InstallInfo {
|
|
17
|
+
installed: string;
|
|
18
|
+
latest: string | null;
|
|
19
|
+
status: UpgradeStatus;
|
|
20
|
+
}
|
|
21
|
+
/** The running CLI: how it was installed and whether npm has a newer one. */
|
|
22
|
+
export declare function checkCli(rootDir: string, fetchImpl?: typeof fetch, packageDir?: string): Promise<CliStatus>;
|
|
23
|
+
export interface RefreshReport {
|
|
24
|
+
skills: Array<{
|
|
25
|
+
base: string;
|
|
26
|
+
targets: string[];
|
|
27
|
+
replaced: number;
|
|
28
|
+
}>;
|
|
29
|
+
/** Generated role agent files that differ from what this version writes. */
|
|
30
|
+
agents: {
|
|
31
|
+
stale: string[];
|
|
32
|
+
replaced: string[];
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Bring the files SDODS put into a project and the home directory up to this version: the bundled
|
|
37
|
+
* skills, wherever they already are, and (with `force`) the generated `sdods-<role>` agent files.
|
|
38
|
+
* CLAUDE.md, AGENTS.md and other hand-editable files are never touched.
|
|
39
|
+
*/
|
|
40
|
+
export declare function refreshAssets(rootDir: string, o?: {
|
|
41
|
+
force?: boolean;
|
|
42
|
+
home?: string;
|
|
43
|
+
dryRun?: boolean;
|
|
44
|
+
}): Promise<RefreshReport>;
|
|
45
|
+
export interface PluginUpdate {
|
|
46
|
+
client: 'claude' | 'copilot';
|
|
47
|
+
installed: boolean;
|
|
48
|
+
command: string[];
|
|
49
|
+
}
|
|
50
|
+
/** The SDODS plugin in Claude Code (`sdods@sdods`) and Copilot CLI (`sdods` from marketplace `sdods`). */
|
|
51
|
+
export declare function detectPlugins(): Promise<PluginUpdate[]>;
|
|
52
|
+
/**
|
|
53
|
+
* The sdods to re-run after an upgrade, so the new version refreshes the assets. npm, bun and pnpm
|
|
54
|
+
* replace the package in place; a Homebrew upgrade moves to a new keg that `brew` links as `sdods`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function upgradedBin(rootDir: string, cli: InstallInfo): string[];
|
|
12
57
|
export declare function register(program: Command): void;
|
|
13
58
|
export declare function detectPm(rootDir: string): 'bun' | 'pnpm' | 'npm';
|
|
14
59
|
//# sourceMappingURL=upgrade.d.ts.map
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
import { Option } from 'commander';
|
|
3
5
|
import { execa } from 'execa';
|
|
4
6
|
import pc from 'picocolors';
|
|
5
7
|
import semver from 'semver';
|
|
6
8
|
import { VERSION } from '@sdods/core';
|
|
7
9
|
import { createContext } from '../context.js';
|
|
10
|
+
import { detectInstallMethod, runningPackageDir } from '../install-method.js';
|
|
11
|
+
import { installSkills, installedSkillTargets } from '../skills-catalog.js';
|
|
8
12
|
import { json, ok, out, table, warn } from '../ui.js';
|
|
9
13
|
export const SDODS_PACKAGES = [
|
|
10
14
|
'@sdods/cli',
|
|
@@ -42,67 +46,238 @@ export async function latestVersion(pkg, fetchImpl = fetch, registry = process.e
|
|
|
42
46
|
return null;
|
|
43
47
|
}
|
|
44
48
|
}
|
|
49
|
+
export function compare(installed, latest) {
|
|
50
|
+
if (!latest)
|
|
51
|
+
return 'unavailable';
|
|
52
|
+
if (!installed)
|
|
53
|
+
return 'not-installed';
|
|
54
|
+
return semver.valid(installed) && semver.lt(installed, latest) ? 'outdated' : 'up-to-date';
|
|
55
|
+
}
|
|
56
|
+
/** The @sdods/* packages a project depends on, compared with the registry. */
|
|
45
57
|
export async function checkUpgrades(rootDir, fetchImpl) {
|
|
46
|
-
|
|
47
|
-
const installed = installedVersion(rootDir, pkg)
|
|
58
|
+
return Promise.all(SDODS_PACKAGES.map(async (pkg) => {
|
|
59
|
+
const installed = installedVersion(rootDir, pkg);
|
|
48
60
|
const latest = await latestVersion(pkg, fetchImpl);
|
|
49
|
-
|
|
50
|
-
if (!latest)
|
|
51
|
-
status = 'unavailable';
|
|
52
|
-
else if (!installed)
|
|
53
|
-
status = 'not-installed';
|
|
54
|
-
else
|
|
55
|
-
status =
|
|
56
|
-
semver.valid(installed) && semver.lt(installed, latest) ? 'outdated' : 'up-to-date';
|
|
57
|
-
return { package: pkg, installed, latest, status };
|
|
61
|
+
return { package: pkg, installed, latest, status: compare(installed, latest) };
|
|
58
62
|
}));
|
|
59
|
-
|
|
63
|
+
}
|
|
64
|
+
/** The running CLI: how it was installed and whether npm has a newer one. */
|
|
65
|
+
export async function checkCli(rootDir, fetchImpl, packageDir = runningPackageDir()) {
|
|
66
|
+
const info = detectInstallMethod(packageDir, { rootDir });
|
|
67
|
+
const latest = await latestVersion('@sdods/cli', fetchImpl);
|
|
68
|
+
return { ...info, installed: VERSION, latest, status: compare(VERSION, latest) };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Bring the files SDODS put into a project and the home directory up to this version: the bundled
|
|
72
|
+
* skills, wherever they already are, and (with `force`) the generated `sdods-<role>` agent files.
|
|
73
|
+
* CLAUDE.md, AGENTS.md and other hand-editable files are never touched.
|
|
74
|
+
*/
|
|
75
|
+
export async function refreshAssets(rootDir, o = {}) {
|
|
76
|
+
const home = o.home ?? homedir();
|
|
77
|
+
const report = { skills: [], agents: { stale: [], replaced: [] } };
|
|
78
|
+
for (const [base, global] of [
|
|
79
|
+
[rootDir, false],
|
|
80
|
+
[home, true],
|
|
81
|
+
]) {
|
|
82
|
+
if (global && base === rootDir)
|
|
83
|
+
continue;
|
|
84
|
+
const targets = installedSkillTargets(base, global);
|
|
85
|
+
if (!targets.length)
|
|
86
|
+
continue;
|
|
87
|
+
const done = o.dryRun ? [] : installSkills({ rootDir, home, global, targets, force: true });
|
|
88
|
+
report.skills.push({
|
|
89
|
+
base,
|
|
90
|
+
targets,
|
|
91
|
+
replaced: done.filter((d) => d.status !== 'skipped').length,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const { codingAgentFiles } = await import('@sdods/agents');
|
|
95
|
+
for (const { file, content } of codingAgentFiles(rootDir)) {
|
|
96
|
+
if (!existsSync(file) || readFileSync(file, 'utf8') === content)
|
|
97
|
+
continue;
|
|
98
|
+
if (o.force && !o.dryRun) {
|
|
99
|
+
writeFileSync(file, content);
|
|
100
|
+
report.agents.replaced.push(file);
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
report.agents.stale.push(file);
|
|
104
|
+
}
|
|
105
|
+
return report;
|
|
106
|
+
}
|
|
107
|
+
/** The SDODS plugin in Claude Code (`sdods@sdods`) and Copilot CLI (`sdods` from marketplace `sdods`). */
|
|
108
|
+
export async function detectPlugins() {
|
|
109
|
+
const listJson = async (bin, args) => {
|
|
110
|
+
try {
|
|
111
|
+
const r = await execa(bin, args, { timeout: 20_000, reject: false, stdin: 'ignore' });
|
|
112
|
+
return r.exitCode === 0 ? JSON.parse(r.stdout) : null;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const claude = (await listJson('claude', ['plugin', 'list', '--json']));
|
|
119
|
+
const copilot = (await listJson('copilot', ['plugin', 'list', '--json']));
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
client: 'claude',
|
|
123
|
+
installed: Array.isArray(claude) && claude.some((p) => p.id === 'sdods@sdods'),
|
|
124
|
+
command: ['claude', 'plugin', 'update', 'sdods@sdods'],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
client: 'copilot',
|
|
128
|
+
installed: Array.isArray(copilot) &&
|
|
129
|
+
copilot.some((p) => p.name === 'sdods' && p.marketplace === 'sdods'),
|
|
130
|
+
command: ['copilot', 'plugin', 'update', 'sdods'],
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
const statusColor = (s) => s === 'outdated' ? pc.yellow(s) : s === 'up-to-date' ? pc.green(s) : pc.dim(s);
|
|
135
|
+
/**
|
|
136
|
+
* The sdods to re-run after an upgrade, so the new version refreshes the assets. npm, bun and pnpm
|
|
137
|
+
* replace the package in place; a Homebrew upgrade moves to a new keg that `brew` links as `sdods`.
|
|
138
|
+
*/
|
|
139
|
+
export function upgradedBin(rootDir, cli) {
|
|
140
|
+
if (cli.method === 'homebrew')
|
|
141
|
+
return ['sdods'];
|
|
142
|
+
const dir = cli.method === 'project' ? join(rootDir, 'node_modules', '@sdods', 'cli') : cli.packageDir;
|
|
143
|
+
return [process.execPath, join(dir, 'bin', 'sdods.js')];
|
|
144
|
+
}
|
|
145
|
+
async function runRefresh(rootDir, o) {
|
|
146
|
+
const rel = (p) => relative(rootDir, p) || '.';
|
|
147
|
+
const report = await refreshAssets(rootDir, { force: o.force });
|
|
148
|
+
const plugins = await detectPlugins();
|
|
149
|
+
const ran = [];
|
|
150
|
+
for (const p of plugins.filter((x) => x.installed)) {
|
|
151
|
+
const r = await execa(p.command[0], p.command.slice(1), {
|
|
152
|
+
reject: false,
|
|
153
|
+
stdin: 'ignore',
|
|
154
|
+
timeout: 120_000,
|
|
155
|
+
}).catch(() => ({ exitCode: 1 }));
|
|
156
|
+
ran.push({ client: p.client, ok: r.exitCode === 0, command: p.command.join(' ') });
|
|
157
|
+
}
|
|
158
|
+
if (o.json) {
|
|
159
|
+
json({ version: VERSION, ...report, plugins: ran });
|
|
160
|
+
return ran.every((r) => r.ok);
|
|
161
|
+
}
|
|
162
|
+
for (const s of report.skills)
|
|
163
|
+
ok(`Skills refreshed in ${s.targets.join(', ')} (${s.base === rootDir ? 'project' : 'home directory'})`);
|
|
164
|
+
for (const f of report.agents.replaced)
|
|
165
|
+
ok(`Replaced ${rel(f)}`);
|
|
166
|
+
if (report.agents.stale.length)
|
|
167
|
+
out(pc.dim(`${report.agents.stale.length} generated agent file(s) differ from ${VERSION}: ${report.agents.stale.map(rel).join(', ')}. \`sdods upgrade --apply --force\` replaces them, edits included.`));
|
|
168
|
+
for (const r of ran) {
|
|
169
|
+
if (r.ok)
|
|
170
|
+
ok(`Updated the SDODS plugin in ${r.client === 'claude' ? 'Claude Code (restart it to load)' : 'Copilot CLI'}`);
|
|
171
|
+
else
|
|
172
|
+
warn(`Could not update the ${r.client} plugin; run: ${r.command}`);
|
|
173
|
+
}
|
|
174
|
+
return ran.every((r) => r.ok);
|
|
60
175
|
}
|
|
61
176
|
export function register(program) {
|
|
62
177
|
program
|
|
63
178
|
.command('upgrade')
|
|
64
|
-
.description('
|
|
65
|
-
.option('--apply', '
|
|
66
|
-
.option('--pm <manager>', 'bun | pnpm | npm (default:
|
|
179
|
+
.description('Upgrade the SDODS CLI the way it was installed (Homebrew, npm, bun, pnpm, project) and refresh the skills and plugins it set up')
|
|
180
|
+
.option('--apply', 'run the upgrades and refresh skills and plugins (default: only report)')
|
|
181
|
+
.option('--pm <manager>', 'bun | pnpm | npm for project packages (default: from the lockfile)')
|
|
182
|
+
.option('--force', 'also replace generated sdods-<role> agent files that differ')
|
|
183
|
+
.option('--no-refresh', 'skip refreshing skills, agent files and plugins')
|
|
184
|
+
.addOption(new Option('--refresh-assets', 'refresh with this version only').hideHelp())
|
|
67
185
|
.action(async (opts, cmd) => {
|
|
68
186
|
const ctx = createContext(cmd);
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
package: r.package,
|
|
74
|
-
installed: r.installed ?? pc.dim('—'),
|
|
75
|
-
latest: r.latest ?? pc.dim('?'),
|
|
76
|
-
status: r.status === 'outdated'
|
|
77
|
-
? pc.yellow(r.status)
|
|
78
|
-
: r.status === 'up-to-date'
|
|
79
|
-
? pc.green(r.status)
|
|
80
|
-
: pc.dim(r.status),
|
|
81
|
-
})));
|
|
82
|
-
const outdated = rows.filter((r) => r.status === 'outdated');
|
|
83
|
-
if (!outdated.length) {
|
|
84
|
-
if (rows.every((r) => r.status === 'unavailable'))
|
|
85
|
-
warn('Registry unreachable (offline?) — nothing compared.');
|
|
86
|
-
else
|
|
87
|
-
ok('Everything installed is up to date.');
|
|
187
|
+
// Second half of `--apply`: runs in the freshly installed binary.
|
|
188
|
+
if (opts.refreshAssets) {
|
|
189
|
+
if (!(await runRefresh(ctx.rootDir, { force: opts.force, json: ctx.opts.json })))
|
|
190
|
+
process.exitCode = 1;
|
|
88
191
|
return;
|
|
89
192
|
}
|
|
193
|
+
const [cli, rows] = await Promise.all([checkCli(ctx.rootDir), checkUpgrades(ctx.rootDir)]);
|
|
194
|
+
const projectRows = rows.filter((r) => r.installed);
|
|
195
|
+
const outdated = projectRows.filter((r) => r.status === 'outdated');
|
|
196
|
+
const cliOutdated = cli.status === 'outdated' && cli.method !== 'project';
|
|
197
|
+
if (ctx.opts.json && !opts.apply) {
|
|
198
|
+
const [assets, plugins] = await Promise.all([
|
|
199
|
+
opts.refresh === false ? null : refreshAssets(ctx.rootDir, { dryRun: true }),
|
|
200
|
+
opts.refresh === false ? null : detectPlugins(),
|
|
201
|
+
]);
|
|
202
|
+
return json({ cli, packages: projectRows, assets, plugins });
|
|
203
|
+
}
|
|
204
|
+
out(`${pc.bold('sdods')} ${cli.installed} ${pc.dim(`(${cli.method})`)} → ${cli.latest ?? pc.dim('?')} ${statusColor(cli.status)}`);
|
|
205
|
+
if (projectRows.length)
|
|
206
|
+
table(projectRows.map((r) => ({
|
|
207
|
+
package: r.package,
|
|
208
|
+
installed: r.installed ?? pc.dim('—'),
|
|
209
|
+
latest: r.latest ?? pc.dim('?'),
|
|
210
|
+
status: statusColor(r.status),
|
|
211
|
+
})));
|
|
212
|
+
if (cli.status === 'unavailable' && rows.every((r) => r.status === 'unavailable'))
|
|
213
|
+
warn('Registry unreachable (offline?): nothing compared.');
|
|
90
214
|
if (!opts.apply) {
|
|
91
|
-
|
|
215
|
+
const todo = [];
|
|
216
|
+
if (cliOutdated)
|
|
217
|
+
todo.push(cli.upgrade ? cli.upgrade.join(' ') : cli.hint);
|
|
218
|
+
if (outdated.length)
|
|
219
|
+
todo.push(`project: ${outdated.map((r) => r.package).join(', ')}`);
|
|
220
|
+
if (todo.length)
|
|
221
|
+
out(pc.dim(`\`sdods upgrade --apply\` will run: ${todo.join('; ')}`));
|
|
222
|
+
else
|
|
223
|
+
ok('Everything installed is up to date.');
|
|
224
|
+
if (opts.refresh !== false)
|
|
225
|
+
out(pc.dim('`--apply` also refreshes the SDODS skills and plugins you installed.'));
|
|
92
226
|
return;
|
|
93
227
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
228
|
+
let failed = false;
|
|
229
|
+
if (cliOutdated) {
|
|
230
|
+
if (cli.upgrade) {
|
|
231
|
+
out(pc.dim(`$ ${cli.upgrade.join(' ')}`));
|
|
232
|
+
const r = await execa(cli.upgrade[0], cli.upgrade.slice(1), {
|
|
233
|
+
stdio: 'inherit',
|
|
234
|
+
reject: false,
|
|
235
|
+
});
|
|
236
|
+
if (r.exitCode !== 0) {
|
|
237
|
+
failed = true;
|
|
238
|
+
warn(`The CLI upgrade failed; run it yourself: ${cli.upgrade.join(' ')}`);
|
|
239
|
+
}
|
|
240
|
+
else
|
|
241
|
+
ok(`Upgraded the CLI (${cli.method}).`);
|
|
242
|
+
}
|
|
243
|
+
else
|
|
244
|
+
warn(cli.hint);
|
|
245
|
+
}
|
|
246
|
+
if (outdated.length) {
|
|
247
|
+
const pm = opts.pm ?? detectPm(ctx.rootDir);
|
|
248
|
+
const specs = outdated.map((r) => `${r.package}@${r.latest}`);
|
|
249
|
+
const args = pm === 'npm' ? ['install', ...specs] : ['add', ...specs];
|
|
250
|
+
const r = await execa(pm, args, { cwd: ctx.rootDir, stdio: 'inherit', reject: false });
|
|
251
|
+
if (r.exitCode !== 0)
|
|
252
|
+
failed = true;
|
|
253
|
+
else
|
|
254
|
+
ok(`Upgraded ${specs.length} project package(s) with ${pm}.`);
|
|
255
|
+
}
|
|
256
|
+
if (opts.refresh !== false) {
|
|
257
|
+
const upgraded = (cliOutdated && cli.upgrade && !failed) || outdated.length > 0;
|
|
258
|
+
if (!upgraded) {
|
|
259
|
+
if (!(await runRefresh(ctx.rootDir, { force: opts.force })))
|
|
260
|
+
failed = true;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// The running process is the old version; the new one knows the new skills.
|
|
264
|
+
const [bin, ...pre] = upgradedBin(ctx.rootDir, cli);
|
|
265
|
+
const args = [...pre, 'upgrade', '--refresh-assets', ...(opts.force ? ['--force'] : [])];
|
|
266
|
+
const r = await execa(bin, args, {
|
|
267
|
+
cwd: ctx.rootDir,
|
|
268
|
+
stdio: 'inherit',
|
|
269
|
+
reject: false,
|
|
270
|
+
}).catch(() => ({ exitCode: 1 }));
|
|
271
|
+
if (r.exitCode !== 0) {
|
|
272
|
+
failed = true;
|
|
273
|
+
warn('Could not refresh skills and plugins; run: sdods upgrade --apply');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (failed)
|
|
103
278
|
process.exitCode = 1;
|
|
104
|
-
else
|
|
105
|
-
ok(
|
|
279
|
+
else if (!cliOutdated && !outdated.length)
|
|
280
|
+
ok('Everything installed is up to date.');
|
|
106
281
|
});
|
|
107
282
|
}
|
|
108
283
|
export function detectPm(rootDir) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the running `sdods` got onto this machine. `sdods upgrade` uses it to upgrade the CLI the same
|
|
3
|
+
* way it was installed, instead of only looking at a project's node_modules.
|
|
4
|
+
*/
|
|
5
|
+
export type InstallMethod = 'project' | 'homebrew' | 'npm' | 'bun' | 'pnpm' | 'npx' | 'installer-git' | 'checkout';
|
|
6
|
+
export interface InstallInfo {
|
|
7
|
+
method: InstallMethod;
|
|
8
|
+
/** Directory of the installed @sdods/cli package (packages/cli in a checkout). */
|
|
9
|
+
packageDir: string;
|
|
10
|
+
/** The command that upgrades this install, or null when there is nothing to run. */
|
|
11
|
+
upgrade: string[] | null;
|
|
12
|
+
/** One line for people: what to do when `upgrade` is null, or what `--apply` will run. */
|
|
13
|
+
hint: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DetectOptions {
|
|
16
|
+
rootDir: string;
|
|
17
|
+
/** $SDODS_HOME, default ~/.sdods (where the install script keeps a git checkout). */
|
|
18
|
+
sdodsHome?: string;
|
|
19
|
+
/** Test hook: does `<checkout>/.git` exist. */
|
|
20
|
+
isGitCheckout?: (dir: string) => boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Directory of the @sdods/cli package this code is running from (src/ and dist/ sit one below it). */
|
|
23
|
+
export declare function runningPackageDir(): string;
|
|
24
|
+
/**
|
|
25
|
+
* Classify an installed package directory. Specific markers are checked before generic ones: a
|
|
26
|
+
* Homebrew keg and an npx cache both contain `node_modules/@sdods/cli` too.
|
|
27
|
+
*/
|
|
28
|
+
export declare function detectInstallMethod(packageDir: string, o: DetectOptions): InstallInfo;
|
|
29
|
+
//# sourceMappingURL=install-method.d.ts.map
|