@deniscodp/skills 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +89 -2
- package/bin/deniscodp-skills.mjs +4 -0
- package/package.json +23 -4
- package/scripts/catalog.mjs +68 -0
- package/scripts/cli.mjs +264 -0
- package/scripts/install.mjs +184 -0
- package/scripts/recommended.mjs +102 -0
- package/skills/denisco-handoff-implementation/SKILL.md +254 -0
- package/skills/denisco-handoff-implementation/references/blocker-checklist.md +71 -0
- package/skills/denisco-handoff-implementation/references/delegated-implementation.md +168 -0
- package/skills/denisco-handoff-implementation/references/ownership.md +105 -0
- package/skills/denisco-handoff-implementation/references/session-plan.md +195 -0
- package/skills/denisco-handoff-implementation/scripts/ownership.mjs +153 -0
- package/skills/denisco-implementation-coordinator/SKILL.md +237 -0
- package/skills/denisco-implementation-coordinator/agents/openai.yaml +2 -0
- package/skills/denisco-implementation-coordinator/references/documents.md +175 -0
- package/skills/denisco-implementation-coordinator/references/runtimes.md +105 -0
- package/skills/denisco-interactive-spec-definition/SKILL.md +126 -0
- package/skills/denisco-interactive-spec-definition/references/blocker-checklist.md +71 -0
- package/skills/denisco-interactive-spec-definition/references/handoff-template.md +124 -0
- package/skills/denisco-project-context/SKILL.md +60 -0
- package/skills/denisco-project-context/references/project-documents.md +70 -0
- package/skills/denisco-visual-check/SKILL.md +136 -0
- package/skills/denisco-visual-check/references/judge-prompt.md +55 -0
- package/skills/denisco-visual-check/references/project-config.md +118 -0
- package/skills/denisco-workflow-benchmark/SKILL.md +113 -0
- package/skills/denisco-workflow-benchmark/agents/openai.yaml +2 -0
- package/skills/denisco-workflow-benchmark/references/measurement.md +166 -0
- package/skills/denisco-workflow-benchmark/references/workflow-contract.md +371 -0
- package/skills/denisco-workflow-benchmark/scripts/bench.mjs +156 -0
- package/skills/denisco-workflow-benchmark/scripts/cases.mjs +46 -0
- package/skills/denisco-workflow-benchmark/scripts/configuration.mjs +71 -0
- package/skills/denisco-workflow-benchmark/scripts/discover.mjs +88 -0
- package/skills/denisco-workflow-benchmark/scripts/eval-worker.mjs +58 -0
- package/skills/denisco-workflow-benchmark/scripts/io.mjs +82 -0
- package/skills/denisco-workflow-benchmark/scripts/prepare.mjs +69 -0
- package/skills/denisco-workflow-benchmark/scripts/runs.mjs +235 -0
- package/skills/denisco-workflow-benchmark/scripts/state-prompt.mjs +96 -0
- package/skills/denisco-workflow-benchmark/scripts/tracking.mjs +91 -0
- package/skills/denisco-workflow-benchmark/scripts/usage.mjs +175 -0
- package/skills/denisco-workflow-benchmark/scripts/workflow.mjs +334 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeniscoDp5
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,90 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Programming Flow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Workflow riutilizzabile per bug e feature: esplorazione del progetto, specifica, implementazione
|
|
4
|
+
a sessioni e misurazione dei risultati con Codex CLI e Claude Code CLI.
|
|
5
|
+
|
|
6
|
+
## Installare le skill in un progetto
|
|
7
|
+
|
|
8
|
+
Dalla cartella del progetto, con Node 20 o superiore:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npx @deniscodp/skills install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
In un terminale il comando chiede i provider (claude preselezionato) e le skill opzionali, mostra il
|
|
15
|
+
piano (cartelle, file da creare, aggiornare o rimuovere, conflitti) e scrive solo dopo la conferma.
|
|
16
|
+
Rilanciato, aggiorna le skill già installate. Senza terminale e senza `--yes` stampa il piano e non scrive.
|
|
17
|
+
|
|
18
|
+
| Flag | Effetto |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `--provider <lista>` | `claude`, `codex`, `agy`, `gemini`, separati da virgole (predefinito `claude`) |
|
|
21
|
+
| `--with <lista>` | skill opzionali: `denisco-workflow-benchmark`, `grill-me`, `grilling` |
|
|
22
|
+
| `--target <cartella>` | progetto di destinazione (predefinito la cartella corrente) |
|
|
23
|
+
| `--yes` | nessuna domanda e nessuna conferma: usa flag e valori predefiniti |
|
|
24
|
+
| `--dry-run` | stampa il piano senza scrivere |
|
|
25
|
+
| `--help`, `--version` | uso del comando e versione del pacchetto |
|
|
26
|
+
|
|
27
|
+
Esempi senza domande:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx @deniscodp/skills install --provider claude,codex --yes
|
|
31
|
+
npx @deniscodp/skills install --provider codex,agy,gemini --with grill-me --dry-run
|
|
32
|
+
npx @deniscodp/skills install --with denisco-workflow-benchmark --yes
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Provider | Cartella nel progetto |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `claude` (Claude Code) | `.claude/skills` |
|
|
38
|
+
| `codex` (Codex CLI), `agy` (Antigravity), `gemini` (Gemini CLI) | `.agents/skills`, una sola installazione condivisa |
|
|
39
|
+
|
|
40
|
+
Le 5 skill `denisco-*` del workflow sono sempre installate; `denisco-workflow-benchmark` è spenta
|
|
41
|
+
di default. Ogni cartella ha un manifest `.workflow-install.json`: un file modificato a mano, un file
|
|
42
|
+
gestito che non serve più o la cartella di una consigliata già presente ma non installata dal comando
|
|
43
|
+
fermano tutto prima di scrivere, e il codice d'uscita è diverso da 0.
|
|
44
|
+
|
|
45
|
+
**Skill consigliate esterne.** `grill-me` porta con sé `grilling`, da cui dipende. Entrambe sono di
|
|
46
|
+
Matt Pocock, con licenza MIT, e vengono scaricate al momento dell'installazione dall'ultima versione
|
|
47
|
+
del ramo `main` di [mattpocock/skills](https://github.com/mattpocock/skills): non sono fissate a una
|
|
48
|
+
versione e il loro contenuto può cambiare fra un'installazione e l'altra. Il piano mostra il commit
|
|
49
|
+
scaricato. Se il download fallisce non viene scritto nulla: riprova più tardi o installa senza la skill
|
|
50
|
+
consigliata. Con la variabile `GITHUB_TOKEN` le chiamate all'API di GitHub usano il token, mai stampato.
|
|
51
|
+
|
|
52
|
+
Dettagli su aggiornamenti, migrazione dei nomi e installazione da un clone sono nella
|
|
53
|
+
[guida operativa](docs/guides/workflow.md#installare-le-skill-in-un-progetto).
|
|
54
|
+
|
|
55
|
+
## Da dove iniziare
|
|
56
|
+
|
|
57
|
+
- [Guida operativa](docs/guides/workflow.md)
|
|
58
|
+
- [Preparare ed eseguire i benchmark](docs/guides/benchmarks.md)
|
|
59
|
+
- [Aggiungere skill e documentazione](docs/contributing.md)
|
|
60
|
+
|
|
61
|
+
## Struttura
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
skills/ Skill distribuibili, ciascuna con risorse e script propri
|
|
65
|
+
docs/guides/ Guide trasversali per usare il workflow
|
|
66
|
+
docs/contributing.md Convenzioni per estendere il toolkit
|
|
67
|
+
bin/ Binario deniscodp-skills del pacchetto npm
|
|
68
|
+
scripts/ Comando di installazione, catalogo delle consigliate e strumenti di manutenzione
|
|
69
|
+
tests/ Test automatici del toolkit
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Se prepari prove o misurazioni, i registri locali vanno in `.workflow-bench/` e le prove isolate in
|
|
73
|
+
`.benchmark-work/`, entrambi esclusi da Git: un clone nuovo non li contiene.
|
|
74
|
+
|
|
75
|
+
## Skill disponibili
|
|
76
|
+
|
|
77
|
+
- [denisco-project-context](skills/denisco-project-context/SKILL.md): esplorazione e procedure del progetto.
|
|
78
|
+
- [denisco-interactive-spec-definition](skills/denisco-interactive-spec-definition/SKILL.md): specifica e handoff.
|
|
79
|
+
- [denisco-handoff-implementation](skills/denisco-handoff-implementation/SKILL.md): piano ed esecuzione fino a uno stop reale.
|
|
80
|
+
- [denisco-implementation-coordinator](skills/denisco-implementation-coordinator/SKILL.md): percorso predefinito con almeno due blocchi; porta l'handoff a un piano di coordinamento lanciabile, poi gestisce programmatori sequenziali leggeri con contesti nuovi, incarichi delimitati e un report per incarico.
|
|
81
|
+
- [denisco-visual-check](skills/denisco-visual-check/SKILL.md): verifica dei requisiti percettivi.
|
|
82
|
+
- [denisco-workflow-benchmark](skills/denisco-workflow-benchmark/SKILL.md): misurazione e confronto dei risultati.
|
|
83
|
+
|
|
84
|
+
Con Node 20+: `npm ci --ignore-scripts`, `npm test`, `npm run validate`.
|
|
85
|
+
Il comando stabile del benchmark, dalla radice del repository, è `npm run bench -- help`.
|
|
86
|
+
Installazione e validazione scoprono automaticamente le skill in `skills/`; le consigliate esterne
|
|
87
|
+
sono elencate in `scripts/recommended.mjs`.
|
|
88
|
+
|
|
89
|
+
I test locali non lanciano modelli. Nessun miglioramento di token o qualità è dichiarato prima
|
|
90
|
+
delle prove con agenti: questa versione fornisce gli strumenti per misurarlo.
|
package/package.json
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deniscodp/skills",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Install the denisco agent workflow skills into the current project for Claude Code, Codex, Antigravity and Gemini CLI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": { "node": ">=20" },
|
|
8
|
+
"bin": { "deniscodp-skills": "bin/deniscodp-skills.mjs" },
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/deniscodp-skills.mjs",
|
|
11
|
+
"scripts/catalog.mjs",
|
|
12
|
+
"scripts/cli.mjs",
|
|
13
|
+
"scripts/install.mjs",
|
|
14
|
+
"scripts/recommended.mjs",
|
|
15
|
+
"skills/denisco-*/**"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": { "access": "public" },
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node --test tests/usage.test.mjs tests/workflow.test.mjs tests/catalog.test.mjs tests/handoff.test.mjs tests/ownership.test.mjs tests/cli.test.mjs",
|
|
20
|
+
"test:network": "node --test tests/network.test.mjs",
|
|
21
|
+
"validate": "node scripts/validate.mjs",
|
|
22
|
+
"bench": "node skills/denisco-workflow-benchmark/scripts/bench.mjs"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": { "yaml": "^2.8.1" }
|
|
25
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { parseDocument } from 'yaml';
|
|
5
|
+
import { listFiles, rejectSymlinkPath } from '../skills/denisco-workflow-benchmark/scripts/io.mjs';
|
|
6
|
+
|
|
7
|
+
export const repository = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
|
|
9
|
+
export function resolveSkillsRoot(source = repository) {
|
|
10
|
+
const nested = path.resolve(source, 'skills');
|
|
11
|
+
// Accept the repository root or an already flat source/installed skill bundle.
|
|
12
|
+
return fs.existsSync(nested) && fs.statSync(nested).isDirectory() && !fs.existsSync(path.join(nested, 'SKILL.md'))
|
|
13
|
+
? nested : path.resolve(source);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validateMarkdown(file, skillRoot) {
|
|
17
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
18
|
+
for (const match of text.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
|
|
19
|
+
const destination = match[1].replace(/^<|>$/g, '').split('#')[0];
|
|
20
|
+
if (!destination || /^[a-z]+:/i.test(destination) || destination.startsWith('/') || destination.includes('<')) continue;
|
|
21
|
+
const target = path.resolve(path.dirname(file), destination);
|
|
22
|
+
if (skillRoot) {
|
|
23
|
+
const relative = path.relative(skillRoot, target);
|
|
24
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
25
|
+
throw new Error(`${file}: skill resource must stay inside its package: ${destination}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!fs.existsSync(target)) throw new Error(`${file}: broken link ${destination}`);
|
|
29
|
+
}
|
|
30
|
+
if (/\b(?:TODO|TBD|FIXME)\b/.test(text)) throw new Error(`Unfinished marker: ${file}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Frontmatter rules shared by packaged and downloaded skills: parseable YAML, a name equal to the
|
|
34
|
+
// skill directory and a non-empty description.
|
|
35
|
+
export function skillFrontmatter(text, name, file) {
|
|
36
|
+
const content = text.replace(/^/, '');
|
|
37
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
38
|
+
if (!frontmatter) throw new Error(`Missing frontmatter: ${file}`);
|
|
39
|
+
const parsed = parseDocument(frontmatter[1], { uniqueKeys: true });
|
|
40
|
+
if (parsed.errors.length) throw new Error(`Invalid YAML: ${file}: ${parsed.errors.join('; ')}`);
|
|
41
|
+
const metadata = parsed.toJSON();
|
|
42
|
+
if (!metadata || metadata.name !== name) throw new Error(`Skill name must match directory: ${file}`);
|
|
43
|
+
if (typeof metadata.description !== 'string' || !metadata.description.trim()) throw new Error(`Missing skill description: ${file}`);
|
|
44
|
+
return metadata;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function discoverSkills(source = repository) {
|
|
48
|
+
const root = resolveSkillsRoot(source);
|
|
49
|
+
rejectSymlinkPath(root);
|
|
50
|
+
const skills = [];
|
|
51
|
+
const entries = fs.readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (entry.isSymbolicLink()) throw new Error(`Symlink skill source refused: ${entry.name}`);
|
|
54
|
+
if (!entry.isDirectory()) continue;
|
|
55
|
+
const directory = path.join(root, entry.name), file = path.join(directory, 'SKILL.md');
|
|
56
|
+
if (!fs.existsSync(file)) continue;
|
|
57
|
+
rejectSymlinkPath(file);
|
|
58
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.name) || entry.name.length > 64) throw new Error(`Invalid skill directory name: ${entry.name}`);
|
|
59
|
+
const metadata = skillFrontmatter(fs.readFileSync(file, 'utf8'), entry.name, file);
|
|
60
|
+
if (!/^\d+\.\d+\.\d+$/.test(metadata.metadata?.version ?? '')) throw new Error(`Missing/invalid skill metadata.version: ${file}`);
|
|
61
|
+
const files = listFiles(directory);
|
|
62
|
+
const documents = files.filter(relative => relative.endsWith('.md')).map(relative => path.join(directory, relative));
|
|
63
|
+
for (const document of documents) validateMarkdown(document, directory);
|
|
64
|
+
skills.push({ name: entry.name, directory, metadata, files, documents });
|
|
65
|
+
}
|
|
66
|
+
if (!skills.length) throw new Error(`No valid skill directories found in ${root}`);
|
|
67
|
+
return skills;
|
|
68
|
+
}
|
package/scripts/cli.mjs
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { repository } from './catalog.mjs';
|
|
6
|
+
import { applyPlan, checkPlan, manifestSkills, planInstall, providerRoots } from './install.mjs';
|
|
7
|
+
import { DownloadError, downloadRecommended, isRecommended, recommendedSkills, requiredBy, withDependencies } from './recommended.mjs';
|
|
8
|
+
|
|
9
|
+
// Installed in every chosen root. Every other skill in the package is optional and listed below.
|
|
10
|
+
export const coreSkills = Object.freeze([
|
|
11
|
+
'denisco-handoff-implementation',
|
|
12
|
+
'denisco-implementation-coordinator',
|
|
13
|
+
'denisco-interactive-spec-definition',
|
|
14
|
+
'denisco-project-context',
|
|
15
|
+
'denisco-visual-check'
|
|
16
|
+
]);
|
|
17
|
+
// Packaged optional skills first, then the recommended third-party skills downloaded from GitHub.
|
|
18
|
+
export const optionalSkills = Object.freeze([
|
|
19
|
+
{ name: 'denisco-workflow-benchmark', description: 'workflow measurement and benchmark tooling' },
|
|
20
|
+
...recommendedSkills.map(skill => ({
|
|
21
|
+
name: skill.name,
|
|
22
|
+
description: `${skill.description}${skill.requires.length ? `, also installs ${skill.requires.join(', ')}` : ''} (github.com/${skill.repo}@${skill.ref}, ${skill.license})`
|
|
23
|
+
}))
|
|
24
|
+
]);
|
|
25
|
+
export const providers = Object.freeze(Object.keys(providerRoots));
|
|
26
|
+
const defaultProviders = ['claude'];
|
|
27
|
+
|
|
28
|
+
const usage = `Usage: npx @deniscodp/skills install [options]
|
|
29
|
+
deniscodp-skills install [options]
|
|
30
|
+
|
|
31
|
+
Installs the denisco workflow skills into a project (default: the current directory).
|
|
32
|
+
Without --yes it asks for providers and optional skills when run in a terminal, shows the plan
|
|
33
|
+
and writes only after confirmation.
|
|
34
|
+
|
|
35
|
+
Commands:
|
|
36
|
+
install plan and install or update the skills
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--provider <list> comma-separated: ${providers.join(', ')} (default: ${defaultProviders.join(',')})
|
|
40
|
+
claude -> .claude/skills; codex, agy, gemini -> .agents/skills
|
|
41
|
+
--with <list> comma-separated optional skills: ${optionalSkills.map(s => s.name).join(', ')}
|
|
42
|
+
${recommendedSkills.map(s => s.name).join(', ')}: downloaded from GitHub (latest main)
|
|
43
|
+
--target <dir> project folder (default: current directory)
|
|
44
|
+
--yes no questions and no confirmation: use flags and defaults
|
|
45
|
+
--dry-run print the plan without writing
|
|
46
|
+
-h, --help show this help
|
|
47
|
+
-v, --version show the package version
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
class UsageError extends Error {}
|
|
51
|
+
|
|
52
|
+
function list(value, allowed, label) {
|
|
53
|
+
const items = value.split(',').map(item => item.trim()).filter(Boolean);
|
|
54
|
+
if (!items.length) throw new UsageError(`Empty ${label} list`);
|
|
55
|
+
const unknown = items.filter(item => !allowed.includes(item));
|
|
56
|
+
if (unknown.length) throw new UsageError(`Unknown ${label}: ${unknown.join(', ')} (allowed: ${allowed.join(', ')})`);
|
|
57
|
+
return [...new Set(items)];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseCommand(argv) {
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = parseArgs({
|
|
64
|
+
args: argv, allowPositionals: true, strict: true,
|
|
65
|
+
options: {
|
|
66
|
+
provider: { type: 'string' }, with: { type: 'string' }, target: { type: 'string' },
|
|
67
|
+
yes: { type: 'boolean' }, 'dry-run': { type: 'boolean' },
|
|
68
|
+
help: { type: 'boolean', short: 'h' }, version: { type: 'boolean', short: 'v' }
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
} catch (error) { throw new UsageError(error.message); }
|
|
72
|
+
const { values, positionals } = parsed;
|
|
73
|
+
if (positionals.length > 1 || (positionals[0] && positionals[0] !== 'install')) {
|
|
74
|
+
throw new UsageError(`Unknown command: ${positionals.join(' ')}`);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
command: positionals[0] ?? null,
|
|
78
|
+
help: Boolean(values.help), version: Boolean(values.version),
|
|
79
|
+
providers: values.provider === undefined ? null : list(values.provider, providers, 'provider'),
|
|
80
|
+
with: values.with === undefined ? [] : list(values.with, optionalSkills.map(s => s.name), 'optional skill'),
|
|
81
|
+
target: values.target ?? null, yes: Boolean(values.yes), dryRun: Boolean(values['dry-run'])
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Providers sharing a skills root produce one installation and one manifest.
|
|
86
|
+
export function groupRoots(chosen) {
|
|
87
|
+
const groups = new Map();
|
|
88
|
+
for (const provider of chosen) {
|
|
89
|
+
const root = providerRoots[provider];
|
|
90
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
91
|
+
groups.get(root).push(provider);
|
|
92
|
+
}
|
|
93
|
+
return [...groups].map(([root, members]) => ({ root, providers: members }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Optional skills already installed in a root stay selected by default: dropping them silently
|
|
97
|
+
// would turn their files into stale managed files and stop the installation.
|
|
98
|
+
function installedOptional(target, roots) {
|
|
99
|
+
const names = new Set(optionalSkills.map(s => s.name)), found = new Set();
|
|
100
|
+
for (const { root } of roots) {
|
|
101
|
+
for (const name of manifestSkills(path.join(path.resolve(target), root))) if (names.has(name)) found.add(name);
|
|
102
|
+
}
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function buildPlans({ target, roots, selected, source = repository, downloaded = [] }) {
|
|
107
|
+
const exclude = optionalSkills.map(s => s.name).filter(name => !isRecommended(name) && !selected.includes(name));
|
|
108
|
+
const external = downloaded.filter(skill => selected.includes(skill.name));
|
|
109
|
+
return roots.map(group => ({ ...group, plan: planInstall({ target, provider: group.providers[0], source, exclude, external }) }));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function blocking(plans) {
|
|
113
|
+
const problems = [];
|
|
114
|
+
for (const { plan } of plans) {
|
|
115
|
+
for (const item of [...plan.entries, ...plan.removals]) {
|
|
116
|
+
if (item.action === 'conflict') problems.push(`conflict: ${path.join(plan.root, item.key)} was modified locally after the last install`);
|
|
117
|
+
}
|
|
118
|
+
for (const name of plan.unmanaged) problems.push(`conflict: ${path.join(plan.root, name)} already exists and is not managed by this installer`);
|
|
119
|
+
for (const key of plan.stale) problems.push(`stale: ${path.join(plan.root, key)} is a managed file of a skill that is no longer selected or shipped`);
|
|
120
|
+
}
|
|
121
|
+
return problems;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function describePlans(target, plans, downloaded = []) {
|
|
125
|
+
const lines = [`Target: ${path.resolve(target)}`];
|
|
126
|
+
for (const { name, source } of downloaded) {
|
|
127
|
+
lines.push(`Recommended ${name}: github.com/${source.repo}@${source.ref} (commit ${source.commit}), ${source.license} license, ${source.author}`);
|
|
128
|
+
}
|
|
129
|
+
for (const { root, providers: members, plan } of plans) {
|
|
130
|
+
const items = [...plan.entries, ...plan.removals];
|
|
131
|
+
const count = action => items.filter(item => item.action === action).length;
|
|
132
|
+
const skills = [...new Set(plan.entries.map(entry => entry.key.split('/')[0]))];
|
|
133
|
+
lines.push('', `${root} (${members.join(', ')})`, ` skills: ${skills.join(', ')}`);
|
|
134
|
+
lines.push(` create ${count('create')}, update ${count('update')}, remove ${count('remove')}, unchanged ${count('unchanged')}, conflict ${count('conflict') + plan.unmanaged.length}, stale ${plan.stale.length}`);
|
|
135
|
+
for (const name of plan.unmanaged) lines.push(` ${'conflict'.padEnd(8)} ${name}/ (folder not managed by this installer)`);
|
|
136
|
+
for (const item of items) {
|
|
137
|
+
if (['create', 'update', 'remove', 'conflict'].includes(item.action)) lines.push(` ${item.action.padEnd(8)} ${item.key}`);
|
|
138
|
+
}
|
|
139
|
+
for (const key of plan.stale) lines.push(` ${'stale'.padEnd(8)} ${key}`);
|
|
140
|
+
}
|
|
141
|
+
return lines.join('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Reads answers line by line; works with a terminal and with scripted input alike.
|
|
145
|
+
function lineReader(input, output) {
|
|
146
|
+
const rl = readline.createInterface({ input, output, terminal: Boolean(input.isTTY && output.isTTY) });
|
|
147
|
+
const queue = [], waiting = [];
|
|
148
|
+
let closed = false;
|
|
149
|
+
rl.on('line', line => waiting.length ? waiting.shift()(line) : queue.push(line));
|
|
150
|
+
rl.on('close', () => { closed = true; while (waiting.length) waiting.shift()(null); });
|
|
151
|
+
return {
|
|
152
|
+
ask(question) {
|
|
153
|
+
output.write(question);
|
|
154
|
+
if (queue.length) return Promise.resolve(queue.shift());
|
|
155
|
+
if (closed) return Promise.resolve(null);
|
|
156
|
+
return new Promise(resolve => waiting.push(resolve));
|
|
157
|
+
},
|
|
158
|
+
close: () => rl.close()
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function askProviders(reader, out, preset) {
|
|
163
|
+
for (;;) {
|
|
164
|
+
const answer = await reader.ask(`Providers (${providers.join(', ')}), comma-separated [${preset.join(',')}]: `);
|
|
165
|
+
if (answer === null) return null;
|
|
166
|
+
if (!answer.trim()) return preset;
|
|
167
|
+
try { return list(answer, providers, 'provider'); } catch (error) { out.write(`${error.message}\n`); }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function askOptional(reader, out, preset) {
|
|
172
|
+
if (!optionalSkills.length) return [];
|
|
173
|
+
const chosen = new Set(preset);
|
|
174
|
+
for (;;) {
|
|
175
|
+
out.write('\nOptional skills:\n');
|
|
176
|
+
// A dependency of a chosen skill is shown as selected, with the skill that requires it.
|
|
177
|
+
const effective = withDependencies([...chosen]);
|
|
178
|
+
optionalSkills.forEach((skill, i) => {
|
|
179
|
+
const needers = chosen.has(skill.name) ? [] : requiredBy(skill.name, effective);
|
|
180
|
+
const mark = chosen.has(skill.name) || needers.length ? 'x' : ' ';
|
|
181
|
+
out.write(` ${i + 1}. [${mark}] ${skill.name} - ${skill.description}${needers.length ? ` (required by ${needers.join(', ')})` : ''}\n`);
|
|
182
|
+
});
|
|
183
|
+
const answer = await reader.ask('Numbers to toggle (e.g. 1,2), Enter to continue: ');
|
|
184
|
+
if (answer === null) return null;
|
|
185
|
+
if (!answer.trim()) return optionalSkills.map(s => s.name).filter(name => effective.includes(name));
|
|
186
|
+
for (const token of answer.split(',').map(t => t.trim()).filter(Boolean)) {
|
|
187
|
+
const skill = optionalSkills[Number(token) - 1];
|
|
188
|
+
if (!/^\d+$/.test(token) || !skill) { out.write(`Not an option: ${token}\n`); continue; }
|
|
189
|
+
if (chosen.has(skill.name)) chosen.delete(skill.name); else chosen.add(skill.name);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function packageVersion() {
|
|
195
|
+
return JSON.parse(fs.readFileSync(path.join(repository, 'package.json'), 'utf8')).version;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function main(argv, io = {}) {
|
|
199
|
+
const stdin = io.stdin ?? process.stdin, stdout = io.stdout ?? process.stdout, stderr = io.stderr ?? process.stderr;
|
|
200
|
+
const interactiveTerminal = io.interactive ?? Boolean(stdin.isTTY && stdout.isTTY);
|
|
201
|
+
let reader;
|
|
202
|
+
try {
|
|
203
|
+
const options = parseCommand(argv);
|
|
204
|
+
if (options.help) { stdout.write(usage); return 0; }
|
|
205
|
+
if (options.version) { stdout.write(`${packageVersion()}\n`); return 0; }
|
|
206
|
+
if (!options.command) { stderr.write(usage); return 1; }
|
|
207
|
+
const target = path.resolve(io.cwd ?? process.cwd(), options.target ?? '.');
|
|
208
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) throw new UsageError(`Target folder does not exist: ${target}`);
|
|
209
|
+
const interactive = interactiveTerminal && !options.yes && !options.dryRun;
|
|
210
|
+
let chosenProviders = options.providers ?? defaultProviders;
|
|
211
|
+
if (interactive) {
|
|
212
|
+
reader = lineReader(stdin, stdout);
|
|
213
|
+
chosenProviders = await askProviders(reader, stdout, chosenProviders);
|
|
214
|
+
if (!chosenProviders) { stderr.write('Input closed: nothing written.\n'); return 1; }
|
|
215
|
+
}
|
|
216
|
+
const roots = groupRoots(chosenProviders);
|
|
217
|
+
let selected = [...new Set([...installedOptional(target, roots), ...options.with])];
|
|
218
|
+
if (interactive) {
|
|
219
|
+
selected = await askOptional(reader, stdout, selected);
|
|
220
|
+
if (!selected) { stderr.write('Input closed: nothing written.\n'); return 1; }
|
|
221
|
+
}
|
|
222
|
+
selected = withDependencies(selected);
|
|
223
|
+
// Recommended skills are downloaded before the plan, so a failed download writes nothing.
|
|
224
|
+
const remote = selected.filter(isRecommended);
|
|
225
|
+
let downloaded = [];
|
|
226
|
+
if (remote.length) {
|
|
227
|
+
stdout.write(`Downloading ${remote.join(', ')} from GitHub...\n`);
|
|
228
|
+
try { downloaded = await downloadRecommended(remote, { fetch: io.fetch ?? globalThis.fetch }); }
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (!(error instanceof DownloadError)) throw error;
|
|
231
|
+
stderr.write(`Could not download the recommended skills (${remote.join(', ')}): ${error.message}\n`);
|
|
232
|
+
stderr.write('No files written. Try again later, or run the command without the recommended skill.\n');
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const plans = buildPlans({ target, roots, selected, source: io.source ?? repository, downloaded });
|
|
237
|
+
stdout.write(`${describePlans(target, plans, downloaded)}\n`);
|
|
238
|
+
const problems = blocking(plans);
|
|
239
|
+
if (problems.length) {
|
|
240
|
+
stderr.write(`\nInstallation stopped, no files changed:\n${problems.map(p => ` ${p}`).join('\n')}\n`);
|
|
241
|
+
stderr.write('Restore or move the listed files (or reselect the skill) and run the command again.\n');
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
if (options.dryRun) { stdout.write('\nDry run: no files written.\n'); return 0; }
|
|
245
|
+
if (!interactive && !options.yes) {
|
|
246
|
+
stdout.write('\nNo terminal for confirmation: no files written. Add --yes to install this plan.\n');
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
if (interactive) {
|
|
250
|
+
const answer = await reader.ask('\nInstall this plan? [y/N]: ');
|
|
251
|
+
if (!/^(y|yes|s|si|sì)$/i.test((answer ?? '').trim())) { stdout.write('Cancelled: no files written.\n'); return 0; }
|
|
252
|
+
}
|
|
253
|
+
for (const { plan } of plans) checkPlan(plan, target);
|
|
254
|
+
for (const { plan } of plans) applyPlan(plan);
|
|
255
|
+
stdout.write(`\nInstalled into ${plans.map(p => p.root).join(' and ')}.\n`);
|
|
256
|
+
return 0;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
stderr.write(`${error instanceof UsageError ? 'Error' : 'Installation failed'}: ${error.message}\n`);
|
|
259
|
+
if (error instanceof UsageError) stderr.write('Run with --help for usage. No files written.\n');
|
|
260
|
+
return 1;
|
|
261
|
+
} finally {
|
|
262
|
+
reader?.close();
|
|
263
|
+
}
|
|
264
|
+
}
|