@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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -2
  3. package/bin/deniscodp-skills.mjs +4 -0
  4. package/package.json +23 -4
  5. package/scripts/catalog.mjs +68 -0
  6. package/scripts/cli.mjs +264 -0
  7. package/scripts/install.mjs +184 -0
  8. package/scripts/recommended.mjs +102 -0
  9. package/skills/denisco-handoff-implementation/SKILL.md +254 -0
  10. package/skills/denisco-handoff-implementation/references/blocker-checklist.md +71 -0
  11. package/skills/denisco-handoff-implementation/references/delegated-implementation.md +168 -0
  12. package/skills/denisco-handoff-implementation/references/ownership.md +105 -0
  13. package/skills/denisco-handoff-implementation/references/session-plan.md +195 -0
  14. package/skills/denisco-handoff-implementation/scripts/ownership.mjs +153 -0
  15. package/skills/denisco-implementation-coordinator/SKILL.md +237 -0
  16. package/skills/denisco-implementation-coordinator/agents/openai.yaml +2 -0
  17. package/skills/denisco-implementation-coordinator/references/documents.md +175 -0
  18. package/skills/denisco-implementation-coordinator/references/runtimes.md +105 -0
  19. package/skills/denisco-interactive-spec-definition/SKILL.md +126 -0
  20. package/skills/denisco-interactive-spec-definition/references/blocker-checklist.md +71 -0
  21. package/skills/denisco-interactive-spec-definition/references/handoff-template.md +124 -0
  22. package/skills/denisco-project-context/SKILL.md +60 -0
  23. package/skills/denisco-project-context/references/project-documents.md +70 -0
  24. package/skills/denisco-visual-check/SKILL.md +136 -0
  25. package/skills/denisco-visual-check/references/judge-prompt.md +55 -0
  26. package/skills/denisco-visual-check/references/project-config.md +118 -0
  27. package/skills/denisco-workflow-benchmark/SKILL.md +113 -0
  28. package/skills/denisco-workflow-benchmark/agents/openai.yaml +2 -0
  29. package/skills/denisco-workflow-benchmark/references/measurement.md +166 -0
  30. package/skills/denisco-workflow-benchmark/references/workflow-contract.md +371 -0
  31. package/skills/denisco-workflow-benchmark/scripts/bench.mjs +156 -0
  32. package/skills/denisco-workflow-benchmark/scripts/cases.mjs +46 -0
  33. package/skills/denisco-workflow-benchmark/scripts/configuration.mjs +71 -0
  34. package/skills/denisco-workflow-benchmark/scripts/discover.mjs +88 -0
  35. package/skills/denisco-workflow-benchmark/scripts/eval-worker.mjs +58 -0
  36. package/skills/denisco-workflow-benchmark/scripts/io.mjs +82 -0
  37. package/skills/denisco-workflow-benchmark/scripts/prepare.mjs +69 -0
  38. package/skills/denisco-workflow-benchmark/scripts/runs.mjs +235 -0
  39. package/skills/denisco-workflow-benchmark/scripts/state-prompt.mjs +96 -0
  40. package/skills/denisco-workflow-benchmark/scripts/tracking.mjs +91 -0
  41. package/skills/denisco-workflow-benchmark/scripts/usage.mjs +175 -0
  42. 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
- # Temporary Holding Version
1
+ # Programming Flow
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../scripts/cli.mjs';
3
+
4
+ process.exitCode = await main(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,25 @@
1
1
  {
2
2
  "name": "@deniscodp/skills",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
@@ -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
+ }