@sdods/cli 0.20.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/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
|
@@ -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
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const INSTALLER_GIT_HINT = 'Re-run the install script, which updates its checkout in place: curl -fsSL https://sdods.com/install.sh | sh -s -- --source git';
|
|
6
|
+
/** Directory of the @sdods/cli package this code is running from (src/ and dist/ sit one below it). */
|
|
7
|
+
export function runningPackageDir() {
|
|
8
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const dir = resolve(here, '..');
|
|
10
|
+
try {
|
|
11
|
+
return realpathSync(dir);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Forward slashes, so one set of markers works on macOS, Linux and Windows paths. */
|
|
18
|
+
function norm(p) {
|
|
19
|
+
return p.split(sep).join('/').replace(/\\/g, '/');
|
|
20
|
+
}
|
|
21
|
+
function within(child, parent) {
|
|
22
|
+
const c = norm(child).replace(/\/+$/, '');
|
|
23
|
+
const p = norm(parent).replace(/\/+$/, '');
|
|
24
|
+
return c === p || c.startsWith(`${p}/`);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Classify an installed package directory. Specific markers are checked before generic ones: a
|
|
28
|
+
* Homebrew keg and an npx cache both contain `node_modules/@sdods/cli` too.
|
|
29
|
+
*/
|
|
30
|
+
export function detectInstallMethod(packageDir, o) {
|
|
31
|
+
const p = norm(packageDir);
|
|
32
|
+
const home = o.sdodsHome ?? process.env.SDODS_HOME ?? join(homedir(), '.sdods');
|
|
33
|
+
const isGit = o.isGitCheckout ?? ((dir) => existsSync(join(dir, '.git')));
|
|
34
|
+
const make = (method, upgrade, hint) => ({
|
|
35
|
+
method,
|
|
36
|
+
packageDir,
|
|
37
|
+
upgrade,
|
|
38
|
+
hint,
|
|
39
|
+
});
|
|
40
|
+
if (within(packageDir, join(o.rootDir, 'node_modules')))
|
|
41
|
+
return make('project', null, 'Upgraded with the project package step below.');
|
|
42
|
+
if (/\/_npx\//.test(p))
|
|
43
|
+
return make('npx', null, 'npx fetches @sdods/cli on each run; use `npx -y @sdods/cli@latest`.');
|
|
44
|
+
if (/\/Cellar\/sdods\//i.test(p) || /\/(?:homebrew|linuxbrew)\/.*\/libexec\//i.test(p))
|
|
45
|
+
return make('homebrew', ['brew', 'upgrade', 'sdods'], 'brew upgrade sdods');
|
|
46
|
+
if (/\/\.bun\/install\/global\//.test(p))
|
|
47
|
+
return make('bun', ['bun', 'add', '-g', '@sdods/cli@latest'], 'bun add -g @sdods/cli@latest');
|
|
48
|
+
if (/\/pnpm\/global\//i.test(p) || /\/\.pnpm\/@sdods\+cli@/.test(p))
|
|
49
|
+
return make('pnpm', ['pnpm', 'add', '-g', '@sdods/cli@latest'], 'pnpm add -g @sdods/cli@latest');
|
|
50
|
+
if (/\/node_modules\/@sdods\/cli$/.test(p.replace(/\/+$/, '')))
|
|
51
|
+
return make('npm', ['npm', 'install', '-g', '@sdods/cli@latest'], 'npm install -g @sdods/cli@latest');
|
|
52
|
+
// packages/cli inside a clone of the monorepo
|
|
53
|
+
const checkout = resolve(packageDir, '..', '..');
|
|
54
|
+
if (/\/packages\/cli$/.test(p.replace(/\/+$/, ''))) {
|
|
55
|
+
if (within(checkout, join(home, 'app')))
|
|
56
|
+
return make('installer-git', null, INSTALLER_GIT_HINT);
|
|
57
|
+
if (isGit(checkout))
|
|
58
|
+
return make('checkout', null, `Development checkout: git -C ${checkout} pull, then install.`);
|
|
59
|
+
}
|
|
60
|
+
return make('npm', ['npm', 'install', '-g', '@sdods/cli@latest'], 'npm install -g @sdods/cli@latest');
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=install-method.js.map
|
package/dist/skills-catalog.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ export declare const SKILL_TARGETS: {
|
|
|
27
27
|
};
|
|
28
28
|
export type SkillTarget = keyof typeof SKILL_TARGETS;
|
|
29
29
|
export declare const DEFAULT_SKILL_TARGETS: SkillTarget[];
|
|
30
|
+
/** The directory a target resolves to, under a project root or (with `global`) the home directory. */
|
|
31
|
+
export declare function skillTargetDir(base: string, target: SkillTarget, global?: boolean): string;
|
|
32
|
+
/**
|
|
33
|
+
* Targets that already hold at least one bundled SDODS skill. `sdods upgrade` refreshes exactly
|
|
34
|
+
* these, so it updates what someone installed without spreading skills into new folders.
|
|
35
|
+
*/
|
|
36
|
+
export declare function installedSkillTargets(base: string, global?: boolean, source?: string): SkillTarget[];
|
|
30
37
|
export interface InstallSkillsOptions {
|
|
31
38
|
rootDir: string;
|
|
32
39
|
targets?: SkillTarget[];
|
package/dist/skills-catalog.js
CHANGED
|
@@ -49,6 +49,18 @@ export const SKILL_TARGETS = {
|
|
|
49
49
|
export const DEFAULT_SKILL_TARGETS = ['claude', 'agents'];
|
|
50
50
|
/** GitHub Copilot's user-level directory is `~/.copilot/skills`, not `~/.github/skills`. */
|
|
51
51
|
const GLOBAL_OVERRIDES = { copilot: '.copilot/skills' };
|
|
52
|
+
/** The directory a target resolves to, under a project root or (with `global`) the home directory. */
|
|
53
|
+
export function skillTargetDir(base, target, global = false) {
|
|
54
|
+
return join(base, (global && GLOBAL_OVERRIDES[target]) || SKILL_TARGETS[target]);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Targets that already hold at least one bundled SDODS skill. `sdods upgrade` refreshes exactly
|
|
58
|
+
* these, so it updates what someone installed without spreading skills into new folders.
|
|
59
|
+
*/
|
|
60
|
+
export function installedSkillTargets(base, global = false, source) {
|
|
61
|
+
const names = listBundledSkills(source).map((s) => s.name);
|
|
62
|
+
return Object.keys(SKILL_TARGETS).filter((t) => names.some((n) => existsSync(join(skillTargetDir(base, t, global), n, 'SKILL.md'))));
|
|
63
|
+
}
|
|
52
64
|
export function installSkills(o) {
|
|
53
65
|
const available = listBundledSkills(o.source);
|
|
54
66
|
const wanted = o.skills?.length ? o.skills : available.map((s) => s.name);
|
|
@@ -58,9 +70,9 @@ export function installSkills(o) {
|
|
|
58
70
|
const base = o.global ? (o.home ?? homedir()) : o.rootDir;
|
|
59
71
|
const results = [];
|
|
60
72
|
for (const target of o.targets?.length ? o.targets : DEFAULT_SKILL_TARGETS) {
|
|
61
|
-
const
|
|
73
|
+
const dir = skillTargetDir(base, target, o.global);
|
|
62
74
|
for (const skill of available.filter((s) => wanted.includes(s.name))) {
|
|
63
|
-
const path = join(
|
|
75
|
+
const path = join(dir, skill.name);
|
|
64
76
|
const exists = existsSync(join(path, 'SKILL.md'));
|
|
65
77
|
if (exists && !o.force) {
|
|
66
78
|
results.push({ skill: skill.name, target, path, status: 'skipped' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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
|
"funding": "https://sdods.com/sponsor/",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"node": ">=22"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@sdods/contracts": "0.
|
|
30
|
-
"@sdods/core": "0.
|
|
29
|
+
"@sdods/contracts": "0.21.0",
|
|
30
|
+
"@sdods/core": "0.21.0",
|
|
31
31
|
"commander": "^15.0.0",
|
|
32
32
|
"execa": "^10.2.1",
|
|
33
33
|
"picocolors": "^1.1.1",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"zod": "^4.6.7",
|
|
37
37
|
"@playwright/test": "^1.64.0",
|
|
38
38
|
"playwright-core": "^1.64.0",
|
|
39
|
-
"@sdods/integrations": "0.
|
|
40
|
-
"@sdods/mcp": "0.
|
|
41
|
-
"@sdods/agents": "0.
|
|
42
|
-
"@sdods/db": "0.
|
|
39
|
+
"@sdods/integrations": "0.21.0",
|
|
40
|
+
"@sdods/mcp": "0.21.0",
|
|
41
|
+
"@sdods/agents": "0.21.0",
|
|
42
|
+
"@sdods/db": "0.21.0",
|
|
43
43
|
"csv-parse": "^7.0.3",
|
|
44
|
-
"@sdods/server": "0.
|
|
44
|
+
"@sdods/server": "0.21.0"
|
|
45
45
|
},
|
|
46
46
|
"homepage": "https://sdods.com",
|
|
47
47
|
"bugs": {
|
|
@@ -101,7 +101,7 @@ Read the rows in order and take the first that matches:
|
|
|
101
101
|
|---|---|---|
|
|
102
102
|
| `package.json` says `sdods-monorepo` — **even if `sdods` is on PATH** | a **source checkout** | `bun run web:build`, then `bun run sdods serve --open` |
|
|
103
103
|
| `sdods` on PATH, anywhere else | a **global install** (installer, npm, Homebrew) | `sdods serve --open` |
|
|
104
|
-
| neither, Docker is running | the **container** | `docker run --rm -p 8080:8080 -v sdods-data:/data
|
|
104
|
+
| neither, Docker is running | the **container** | `docker run --rm -p 8080:8080 -v sdods-data:/data sdods/sdods-server` |
|
|
105
105
|
| you want no terminal at all | the **desktop app** | install from <https://sdods.com/download>, launch it |
|
|
106
106
|
|
|
107
107
|
> **Inside a checkout, always use `bun run sdods`, never the global `sdods`.** The two drift, and
|
|
@@ -155,19 +155,14 @@ The CLI is identical on every OS; only the shell and the paths differ.
|
|
|
155
155
|
| Windows (PowerShell) | `sdods serve --open` |
|
|
156
156
|
| Windows (cmd) | `sdods serve --open` — the installer writes both `sdods.cmd` and `sdods.ps1` |
|
|
157
157
|
| WSL | use the Linux command inside WSL, not the PowerShell one |
|
|
158
|
-
| Docker (any host) | `docker run --rm -p 8080:8080 -v sdods-data:/data
|
|
158
|
+
| Docker (any host) | `docker run --rm -p 8080:8080 -v sdods-data:/data sdods/sdods-server` |
|
|
159
159
|
|
|
160
160
|
Defaults: **`http://127.0.0.1:4444`** for the CLI, **`http://localhost:8080`** for Docker (the
|
|
161
161
|
image binds `0.0.0.0:8080`). `--open` launches the browser (`open` / `start` / `xdg-open` by
|
|
162
162
|
platform). `--host 0.0.0.0` exposes it on the LAN — only do that deliberately.
|
|
163
163
|
|
|
164
|
-
The image is published for `linux/amd64` and `linux/arm64
|
|
165
|
-
|
|
166
|
-
on arm64 rather than emulating; for those, add the platform flag:
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
docker run --rm --platform linux/amd64 -p 8080:8080 -v sdods-data:/data ghcr.io/yarlisaisolutions/sdods-server:0.9.0
|
|
170
|
-
```
|
|
164
|
+
The image is published on Docker Hub for `linux/amd64` and `linux/arm64` (tags from `0.20.0`), so
|
|
165
|
+
the same command runs natively on Apple silicon and arm64 servers.
|
|
171
166
|
|
|
172
167
|
If the run must come from a source checkout, `bun run web:build` is not optional — see
|
|
173
168
|
Troubleshooting.
|