@chenguangyao/devflow-kit 0.1.43
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/CHANGELOG.md +232 -0
- package/LICENSE +21 -0
- package/README.md +539 -0
- package/bin/devflow.js +9 -0
- package/docs/RFC-001-devflow-kit.md +617 -0
- package/docs/RFC-002-workflow-kernel.md +134 -0
- package/docs/enterprise-integration-supplement.md +274 -0
- package/docs/internal-gitlab-setup.md +426 -0
- package/docs/marketplace-skills.md +231 -0
- package/docs/migration-from-arb.md +232 -0
- package/docs/tooling-overview.md +774 -0
- package/docs/workflow-orchestration.md +695 -0
- package/docs/workflow-ui-prototype.html +271 -0
- package/package.json +52 -0
- package/schemas/config.schema.json +51 -0
- package/schemas/delta.schema.json +22 -0
- package/schemas/state.schema.json +130 -0
- package/schemas/status-surface.schema.json +197 -0
- package/schemas/workflow-confirmation-surface.schema.json +70 -0
- package/schemas/workflow-picker.schema.json +94 -0
- package/scripts/postinstall.js +101 -0
- package/scripts/render-workflow-ui-prototype.js +271 -0
- package/skills/apply/SKILL.md +313 -0
- package/skills/apply/references/discipline-checklist.md +145 -0
- package/skills/apply/references/subagent-implementer-prompt.md +113 -0
- package/skills/apply/references/subagent-orchestration.md +150 -0
- package/skills/apply/references/subagent-reviewer-prompt.md +180 -0
- package/skills/apply/references/tdd-loop.md +287 -0
- package/skills/apply/references/when-plan-is-wrong.md +279 -0
- package/skills/apply/references/worktree-swarm.md +292 -0
- package/skills/archive/SKILL.md +229 -0
- package/skills/archive/references/conflict-resolution.md +336 -0
- package/skills/archive/references/knowledge-deposit.md +381 -0
- package/skills/archive/references/spec-merge.md +365 -0
- package/skills/brainstorm/SKILL.md +123 -0
- package/skills/brainstorm/references/proposal-template.md +244 -0
- package/skills/brainstorm/references/question-catalog.md +168 -0
- package/skills/brainstorm/references/session-template.md +184 -0
- package/skills/ci-fix/SKILL.md +63 -0
- package/skills/ci-fix/references/loop.md +25 -0
- package/skills/code-review/SKILL.md +279 -0
- package/skills/code-review/references/escalation-playbook.md +192 -0
- package/skills/code-review/references/language-cheatsheets/go.md +175 -0
- package/skills/code-review/references/language-cheatsheets/java-spring-mybatis.md +246 -0
- package/skills/code-review/references/language-cheatsheets/python.md +170 -0
- package/skills/code-review/references/language-cheatsheets/vue.md +199 -0
- package/skills/code-review/references/output-template.md +275 -0
- package/skills/code-review/references/review-checklist.md +251 -0
- package/skills/complexity-grading/SKILL.md +259 -0
- package/skills/deliver/SKILL.md +271 -0
- package/skills/deliver/references/delivery-modes.md +299 -0
- package/skills/deliver/references/notify.md +359 -0
- package/skills/deliver/references/pr-description.md +319 -0
- package/skills/dependency-upgrade/SKILL.md +57 -0
- package/skills/dependency-upgrade/references/risk-matrix.md +38 -0
- package/skills/df-orchestrator/SKILL.md +407 -0
- package/skills/df-orchestrator/references/complexity-grading.md +177 -0
- package/skills/df-orchestrator/references/escalation-matrix.md +191 -0
- package/skills/df-orchestrator/references/routing-rules.md +290 -0
- package/skills/df-orchestrator/references/workflow-state-machine.md +208 -0
- package/skills/frontend-quality/SKILL.md +61 -0
- package/skills/frontend-quality/references/checklist.md +35 -0
- package/skills/handoff-resume/SKILL.md +59 -0
- package/skills/handoff-resume/references/handoff-template.md +54 -0
- package/skills/plan/SKILL.md +166 -0
- package/skills/plan/references/task-breakdown.md +207 -0
- package/skills/plan/references/task-sequencing.md +143 -0
- package/skills/plan/references/task-template.md +248 -0
- package/skills/requirement-analysis/SKILL.md +499 -0
- package/skills/requirement-analysis/references/acceptance-criteria.md +183 -0
- package/skills/requirement-analysis/references/code-recon.md +151 -0
- package/skills/requirement-analysis/references/edge-case-catalog.md +164 -0
- package/skills/requirement-analysis/references/requirement-template.md +339 -0
- package/skills/requirement-analysis/references/scope-negotiation.md +162 -0
- package/skills/security-hardening/SKILL.md +60 -0
- package/skills/security-hardening/references/checklist.md +42 -0
- package/skills/tech-spec/SKILL.md +388 -0
- package/skills/tech-spec/references/api-contract-design.md +172 -0
- package/skills/tech-spec/references/decision-records.md +110 -0
- package/skills/tech-spec/references/design-template.md +301 -0
- package/skills/tech-spec/references/rollout-and-rollback.md +203 -0
- package/skills/tech-spec/references/spec-delta-conventions.md +250 -0
- package/skills/tech-spec/references/transaction-patterns.md +212 -0
- package/skills/test-spec/SKILL.md +219 -0
- package/skills/test-spec/references/coverage-strategy.md +218 -0
- package/skills/test-spec/references/edge-case-to-test.md +143 -0
- package/skills/test-spec/references/test-case-template.md +276 -0
- package/skills/verify/SKILL.md +232 -0
- package/skills/verify/references/nfr-verification.md +292 -0
- package/skills/verify/references/report-templates.md +510 -0
- package/skills/verify/references/self-test-guide.md +240 -0
- package/skills/verify/references/verify-rollback-map.md +247 -0
- package/src/cli/commands/_helpers.js +108 -0
- package/src/cli/commands/_submit.js +718 -0
- package/src/cli/commands/apply.js +198 -0
- package/src/cli/commands/archive.js +180 -0
- package/src/cli/commands/checkpoint.js +113 -0
- package/src/cli/commands/deliver.js +377 -0
- package/src/cli/commands/deploy.js +504 -0
- package/src/cli/commands/design.js +158 -0
- package/src/cli/commands/disable.js +21 -0
- package/src/cli/commands/doctor.js +178 -0
- package/src/cli/commands/enable.js +21 -0
- package/src/cli/commands/flow.js +645 -0
- package/src/cli/commands/help.js +93 -0
- package/src/cli/commands/ingest.js +602 -0
- package/src/cli/commands/init.js +341 -0
- package/src/cli/commands/knowledge.js +523 -0
- package/src/cli/commands/logs.js +43 -0
- package/src/cli/commands/new.js +202 -0
- package/src/cli/commands/plan.js +49 -0
- package/src/cli/commands/propose.js +27 -0
- package/src/cli/commands/provider.js +698 -0
- package/src/cli/commands/report.js +143 -0
- package/src/cli/commands/requirement.js +227 -0
- package/src/cli/commands/review.js +301 -0
- package/src/cli/commands/skills.js +457 -0
- package/src/cli/commands/status.js +925 -0
- package/src/cli/commands/switch.js +27 -0
- package/src/cli/commands/sync.js +47 -0
- package/src/cli/commands/test.js +366 -0
- package/src/cli/commands/uninstall.js +32 -0
- package/src/cli/commands/update.js +74 -0
- package/src/cli/commands/verify.js +354 -0
- package/src/cli/commands/worktree.js +78 -0
- package/src/cli/index.js +72 -0
- package/src/cli/parse-args.js +102 -0
- package/src/core/autodetect.js +271 -0
- package/src/core/change.js +208 -0
- package/src/core/checkpoint.js +217 -0
- package/src/core/config.js +60 -0
- package/src/core/delta.js +290 -0
- package/src/core/markers.js +59 -0
- package/src/core/paths.js +173 -0
- package/src/core/plan-tasks.js +36 -0
- package/src/core/project-routing.js +285 -0
- package/src/core/projects.js +200 -0
- package/src/core/state.js +200 -0
- package/src/core/workflow-check.js +177 -0
- package/src/core/workflow-init.js +34 -0
- package/src/core/workflow-picker.js +154 -0
- package/src/core/workflow-policy.js +119 -0
- package/src/core/workflow-suggest.js +181 -0
- package/src/core/workflow-verify.js +88 -0
- package/src/core/workflow.js +433 -0
- package/src/core/worktree.js +241 -0
- package/src/knowledge/categories.js +107 -0
- package/src/knowledge/classify.js +125 -0
- package/src/knowledge/deposit.js +414 -0
- package/src/knowledge/migrate.js +149 -0
- package/src/knowledge/mr.js +219 -0
- package/src/knowledge/query.js +131 -0
- package/src/knowledge/registry.js +151 -0
- package/src/knowledge/sync.js +179 -0
- package/src/providers/base.js +74 -0
- package/src/providers/drivers/api-yapi.js +78 -0
- package/src/providers/drivers/ci-jenkins.js +109 -0
- package/src/providers/drivers/intake-confluence.js +544 -0
- package/src/providers/drivers/kb-git.js +549 -0
- package/src/providers/drivers/kb-weknora.js +472 -0
- package/src/providers/drivers/notify-smtp.js +515 -0
- package/src/providers/drivers/observability-oss.js +43 -0
- package/src/providers/drivers/observability-sls.js +50 -0
- package/src/providers/lifecycle.js +135 -0
- package/src/providers/loader.js +132 -0
- package/src/providers/local.js +190 -0
- package/src/providers/userconfig.js +283 -0
- package/src/reports/aggregate.js +185 -0
- package/src/reports/coverage.js +163 -0
- package/src/reports/detect.js +143 -0
- package/src/reports/parse.js +236 -0
- package/src/templates/files/ci/github.yml +38 -0
- package/src/templates/files/ci/gitlab.yml +27 -0
- package/src/templates/files/design.md +63 -0
- package/src/templates/files/ide/devflow-workflow.md +58 -0
- package/src/templates/files/ide/project-overview-reference.md +1 -0
- package/src/templates/files/ide/project-overview.md +27 -0
- package/src/templates/files/knowledge-index.json +17 -0
- package/src/templates/files/knowledge.md +28 -0
- package/src/templates/files/meta.json +8 -0
- package/src/templates/files/plan.md +38 -0
- package/src/templates/files/proposal.md +33 -0
- package/src/templates/files/reports/contract-test.md +40 -0
- package/src/templates/files/reports/e2e-test.md +30 -0
- package/src/templates/files/reports/integration-test.md +36 -0
- package/src/templates/files/reports/joint-test.md +58 -0
- package/src/templates/files/reports/perf.md +24 -0
- package/src/templates/files/reports/regression.md +20 -0
- package/src/templates/files/reports/remote-test.md +55 -0
- package/src/templates/files/reports/self-test.md +43 -0
- package/src/templates/files/reports/smoke-test.md +22 -0
- package/src/templates/files/reports/unit-test.md +36 -0
- package/src/templates/files/requirement.md +51 -0
- package/src/templates/files/review.md +38 -0
- package/src/templates/files/tests.md +36 -0
- package/src/templates/files/verify.md +32 -0
- package/src/templates/index.js +21 -0
- package/src/utils/log.js +37 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs/promises');
|
|
3
|
+
const fsSync = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const log = require('../../utils/log.js');
|
|
6
|
+
|
|
7
|
+
const SKILLS_DIR = path.join(__dirname, '..', '..', '..', 'skills');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* NAMESPACE — every skill we ship is grouped under this folder so the IDE
|
|
11
|
+
* skill directory stays tidy:
|
|
12
|
+
*
|
|
13
|
+
* ~/.cursor/skills/
|
|
14
|
+
* ├── devflow-kit/ ← symlink to ~/.devflow/skills/devflow-kit for user installs
|
|
15
|
+
* │ ├── apply/SKILL.md
|
|
16
|
+
* │ ├── archive/SKILL.md
|
|
17
|
+
* │ └── ...
|
|
18
|
+
* └── superpowers/ ← unrelated 3rd-party skills, untouched
|
|
19
|
+
*
|
|
20
|
+
* Cursor & Claude Code both walk subdirectories looking for SKILL.md, so
|
|
21
|
+
* nesting one level deeper is transparent at runtime.
|
|
22
|
+
*
|
|
23
|
+
* The set of ALL skill directory names we ship — used by both userSkillsInstalled
|
|
24
|
+
* (probe) and the legacy migration path. Keep in sync with skills/ folder.
|
|
25
|
+
*/
|
|
26
|
+
const NAMESPACE = 'devflow-kit';
|
|
27
|
+
const USER_SKILLS_SOURCE = '~/.devflow/skills/devflow-kit';
|
|
28
|
+
const SHIPPED_SKILLS = [
|
|
29
|
+
'apply',
|
|
30
|
+
'archive',
|
|
31
|
+
'brainstorm',
|
|
32
|
+
'ci-fix',
|
|
33
|
+
'code-review',
|
|
34
|
+
'complexity-grading',
|
|
35
|
+
'deliver',
|
|
36
|
+
'dependency-upgrade',
|
|
37
|
+
'df-orchestrator',
|
|
38
|
+
'frontend-quality',
|
|
39
|
+
'handoff-resume',
|
|
40
|
+
'plan',
|
|
41
|
+
'requirement-analysis',
|
|
42
|
+
'security-hardening',
|
|
43
|
+
'tech-spec',
|
|
44
|
+
'test-spec',
|
|
45
|
+
'verify',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const PROJECT_TARGET_MAP = {
|
|
49
|
+
claude: '.claude/skills',
|
|
50
|
+
cursor: '.cursor/skills',
|
|
51
|
+
agents: '.agents/skills',
|
|
52
|
+
cwd: 'skills',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const USER_TARGET_MAP = {
|
|
56
|
+
'claude-user': '~/.claude/skills',
|
|
57
|
+
'cursor-user': '~/.cursor/skills',
|
|
58
|
+
'agents-user': '~/.agents/skills',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const TARGET_MAP = { ...PROJECT_TARGET_MAP, ...USER_TARGET_MAP, user: '~/.claude/skills' };
|
|
62
|
+
|
|
63
|
+
const USER_SCOPE_TARGETS = ['cursor-user', 'claude-user', 'agents-user'];
|
|
64
|
+
const OWNERSHIP_MARKER = '.devflow-skill';
|
|
65
|
+
const DEVFLOW_OWNED_RE = /\bdevflow(?:-kit)?\b|\/df:|`devflow\b/i;
|
|
66
|
+
|
|
67
|
+
function expand(p) {
|
|
68
|
+
return p.startsWith('~') ? path.join(process.env.HOME || '', p.slice(1)) : p;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function displayPath(root, dest, absolute) {
|
|
72
|
+
if (typeof dest === 'string' && dest.startsWith('~')) {
|
|
73
|
+
const tail = absolute.slice(expand(dest).length);
|
|
74
|
+
return dest + tail;
|
|
75
|
+
}
|
|
76
|
+
return path.relative(root, absolute) || absolute;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Detect whether user-wide skills are already present.
|
|
81
|
+
*
|
|
82
|
+
* Probes both layouts (older flat installs from 0.1.x, and current namespaced
|
|
83
|
+
* installs) so existing users aren't told they have nothing installed just
|
|
84
|
+
* because the layout changed.
|
|
85
|
+
*/
|
|
86
|
+
function userSkillsInstalled() {
|
|
87
|
+
return USER_SCOPE_TARGETS.every((k) => {
|
|
88
|
+
const root = expand(TARGET_MAP[k]);
|
|
89
|
+
return (
|
|
90
|
+
fsSync.existsSync(path.join(root, NAMESPACE, 'df-orchestrator', 'SKILL.md')) ||
|
|
91
|
+
fsSync.existsSync(path.join(root, 'df-orchestrator', 'SKILL.md'))
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Detect a legacy flat install (skills sitting at the root of `parent`).
|
|
98
|
+
* Returns the list of shipped skills found at the flat layer.
|
|
99
|
+
*/
|
|
100
|
+
function detectLegacyFlat(parent) {
|
|
101
|
+
if (!fsSync.existsSync(parent)) return [];
|
|
102
|
+
return SHIPPED_SKILLS.filter((name) =>
|
|
103
|
+
isDevflowOwnedFlatSkill(parent, name)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isDevflowOwnedFlatSkill(parent, name) {
|
|
108
|
+
const dir = path.join(parent, name);
|
|
109
|
+
const skillFile = path.join(dir, 'SKILL.md');
|
|
110
|
+
if (!fsSync.existsSync(skillFile)) return false;
|
|
111
|
+
if (fsSync.existsSync(path.join(dir, OWNERSHIP_MARKER))) return true;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const body = fsSync.readFileSync(skillFile, 'utf8');
|
|
115
|
+
return DEVFLOW_OWNED_RE.test(body);
|
|
116
|
+
} catch (_) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Move a flat install into the namespace folder. Conservative:
|
|
123
|
+
* - Only touches directories whose names are in SHIPPED_SKILLS.
|
|
124
|
+
* - Refuses to overwrite a pre-existing namespace dir (safer to delete first).
|
|
125
|
+
*/
|
|
126
|
+
async function migrateFlatToNamespace(parent, { quiet = false } = {}) {
|
|
127
|
+
const flat = detectLegacyFlat(parent);
|
|
128
|
+
if (!flat.length) return { moved: 0, skills: [] };
|
|
129
|
+
const nsRoot = path.join(parent, NAMESPACE);
|
|
130
|
+
await fs.mkdir(nsRoot, { recursive: true });
|
|
131
|
+
let moved = 0;
|
|
132
|
+
for (const name of flat) {
|
|
133
|
+
const from = path.join(parent, name);
|
|
134
|
+
const to = path.join(nsRoot, name);
|
|
135
|
+
if (fsSync.existsSync(to)) {
|
|
136
|
+
// Namespace already has this skill — drop the flat copy to dedupe
|
|
137
|
+
await fs.rm(from, { recursive: true, force: true });
|
|
138
|
+
if (!quiet) log.dim(`migrated: ${name} (already in ${NAMESPACE}, removed flat copy)`);
|
|
139
|
+
} else {
|
|
140
|
+
await fs.rename(from, to);
|
|
141
|
+
await writeOwnershipMarker(to);
|
|
142
|
+
if (!quiet) log.ok(`migrated: ${name} → ${NAMESPACE}/${name}`);
|
|
143
|
+
}
|
|
144
|
+
moved++;
|
|
145
|
+
}
|
|
146
|
+
return { moved, skills: flat };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function run({ sub, flags = {}, positional = [], cwd }) {
|
|
150
|
+
const action = sub || positional[0] || 'install';
|
|
151
|
+
if (action === 'list') {
|
|
152
|
+
const entries = await fs.readdir(SKILLS_DIR, { withFileTypes: true });
|
|
153
|
+
for (const e of entries) {
|
|
154
|
+
if (e.isDirectory()) log.raw(` - ${e.name}`);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (action === 'status') return runStatus();
|
|
159
|
+
if (action === 'uninstall') return runUninstall(flags);
|
|
160
|
+
if (action === 'migrate') return runMigrate(flags);
|
|
161
|
+
if (action !== 'install') {
|
|
162
|
+
log.error('usage: devflow skills <install|status|uninstall|migrate> [--scope=user|project] [--target=<...>]');
|
|
163
|
+
process.exitCode = 2; return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const root = cwd || process.cwd();
|
|
167
|
+
const scope = flags.scope;
|
|
168
|
+
|
|
169
|
+
let targetKeys;
|
|
170
|
+
if (flags.target) {
|
|
171
|
+
targetKeys = String(flags.target).split(',').map((s) => s.trim()).filter(Boolean);
|
|
172
|
+
} else if (scope === 'user') {
|
|
173
|
+
targetKeys = USER_SCOPE_TARGETS;
|
|
174
|
+
} else if (scope === 'project') {
|
|
175
|
+
targetKeys = ['cursor', 'claude', 'agents'];
|
|
176
|
+
} else {
|
|
177
|
+
targetKeys = ['cursor', 'claude', 'agents'];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const dests = [];
|
|
181
|
+
if (flags.dest) {
|
|
182
|
+
dests.push({ key: '(custom)', dest: flags.dest });
|
|
183
|
+
} else {
|
|
184
|
+
for (const k of targetKeys) {
|
|
185
|
+
const d = TARGET_MAP[k];
|
|
186
|
+
if (!d) { log.error(`unknown target: ${k}`); process.exitCode = 2; return; }
|
|
187
|
+
dests.push({ key: k, dest: d });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const linkMode = !flags.copy && (scope === 'user' || targetKeys.some((k) => k.endsWith('-user')))
|
|
192
|
+
? 'central'
|
|
193
|
+
: 'copy';
|
|
194
|
+
const summary = await installToDests(root, dests, { quiet: flags.quiet, linkMode });
|
|
195
|
+
for (const s of summary) log.info(`${s.count} skills → ${s.path}`);
|
|
196
|
+
|
|
197
|
+
if (scope === 'user' || targetKeys.some((k) => k.endsWith('-user'))) {
|
|
198
|
+
log.raw('');
|
|
199
|
+
if (linkMode === 'central') {
|
|
200
|
+
log.dim(`user-wide install complete. skills are stored once at ${USER_SKILLS_SOURCE} and linked into IDE dirs.`);
|
|
201
|
+
} else {
|
|
202
|
+
log.dim('user-wide install complete. you can now run `devflow init` in any repo without re-installing skills.');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function runStatus() {
|
|
208
|
+
log.raw('skill installation status:');
|
|
209
|
+
const checks = [];
|
|
210
|
+
for (const [key, p] of Object.entries(TARGET_MAP)) {
|
|
211
|
+
if (key === 'cwd' || key === 'user') continue;
|
|
212
|
+
const parent = expand(p);
|
|
213
|
+
const nsRoot = path.join(parent, NAMESPACE);
|
|
214
|
+
const ns = fsSync.existsSync(path.join(nsRoot, 'df-orchestrator', 'SKILL.md'));
|
|
215
|
+
const flat = fsSync.existsSync(path.join(parent, 'df-orchestrator', 'SKILL.md'));
|
|
216
|
+
const linked = ns && isSymlink(nsRoot);
|
|
217
|
+
const layout = ns ? `${NAMESPACE}/${linked ? ` -> ${fsSync.readlinkSync(nsRoot)}` : ''}` : (flat ? '(flat — legacy)' : '');
|
|
218
|
+
checks.push({ key, path: parent, exists: ns || flat, layout });
|
|
219
|
+
}
|
|
220
|
+
for (const c of checks) {
|
|
221
|
+
const tag = c.exists ? '[ok]' : '[ — ]';
|
|
222
|
+
const layoutStr = c.layout ? ` ${c.layout}` : '';
|
|
223
|
+
log.raw(` ${tag.padEnd(6)} ${c.key.padEnd(14)} ${c.path}${layoutStr}`);
|
|
224
|
+
}
|
|
225
|
+
log.raw('');
|
|
226
|
+
if (userSkillsInstalled()) {
|
|
227
|
+
log.ok('user-wide skills detected — `devflow init` will skip per-project skill copy.');
|
|
228
|
+
// Surface migration recommendation if any flat layout was detected
|
|
229
|
+
const anyFlat = checks.some((c) => c.exists && c.layout.includes('legacy'));
|
|
230
|
+
if (anyFlat) {
|
|
231
|
+
log.warn(`legacy flat layout detected; run \`devflow skills migrate --scope=user\` to consolidate into ${NAMESPACE}/`);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
log.dim('user-wide skills NOT installed. recommend: `devflow skills install --scope=user` (one-time).');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function runUninstall(flags) {
|
|
239
|
+
const scope = flags.scope || 'user';
|
|
240
|
+
const targetKeys = scope === 'user' ? USER_SCOPE_TARGETS : ['cursor', 'claude', 'agents'];
|
|
241
|
+
const root = process.cwd();
|
|
242
|
+
for (const k of targetKeys) {
|
|
243
|
+
const dest = TARGET_MAP[k];
|
|
244
|
+
if (!dest) continue;
|
|
245
|
+
const parent = dest.startsWith('~') ? expand(dest) : path.resolve(root, dest);
|
|
246
|
+
if (!fsSync.existsSync(parent)) { log.dim(`${k}: not present (skip)`); continue; }
|
|
247
|
+
|
|
248
|
+
const nsDir = path.join(parent, NAMESPACE);
|
|
249
|
+
const removedNs = fsSync.existsSync(nsDir) || isSymlink(nsDir);
|
|
250
|
+
if (removedNs) await removePath(nsDir);
|
|
251
|
+
|
|
252
|
+
let removedFlat = 0;
|
|
253
|
+
for (const name of SHIPPED_SKILLS) {
|
|
254
|
+
const flatDir = path.join(parent, name);
|
|
255
|
+
if (fsSync.existsSync(flatDir)) {
|
|
256
|
+
await fs.rm(flatDir, { recursive: true, force: true });
|
|
257
|
+
removedFlat++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (removedNs || removedFlat) {
|
|
261
|
+
const parts = [];
|
|
262
|
+
if (removedNs) parts.push(`${NAMESPACE}/`);
|
|
263
|
+
if (removedFlat) parts.push(`${removedFlat} flat skill(s)`);
|
|
264
|
+
log.ok(`removed: ${k} → ${parent} (${parts.join(' + ')})`);
|
|
265
|
+
} else {
|
|
266
|
+
log.dim(`${k}: nothing to remove`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (scope === 'user') {
|
|
270
|
+
const central = expand(USER_SKILLS_SOURCE);
|
|
271
|
+
const removedCentral = fsSync.existsSync(central) || isSymlink(central);
|
|
272
|
+
if (removedCentral) {
|
|
273
|
+
await removePath(central);
|
|
274
|
+
log.ok(`removed: central skills → ${central}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function runMigrate(flags) {
|
|
280
|
+
const scope = flags.scope || 'user';
|
|
281
|
+
const targetKeys = scope === 'user' ? USER_SCOPE_TARGETS : ['cursor', 'claude', 'agents'];
|
|
282
|
+
const root = process.cwd();
|
|
283
|
+
let totalMoved = 0;
|
|
284
|
+
for (const k of targetKeys) {
|
|
285
|
+
const dest = TARGET_MAP[k];
|
|
286
|
+
if (!dest) continue;
|
|
287
|
+
const parent = dest.startsWith('~') ? expand(dest) : path.resolve(root, dest);
|
|
288
|
+
if (!fsSync.existsSync(parent)) { log.dim(`${k}: parent dir not present (skip)`); continue; }
|
|
289
|
+
const flat = detectLegacyFlat(parent);
|
|
290
|
+
if (!flat.length) { log.dim(`${k}: no legacy flat skills under ${parent} (skip)`); continue; }
|
|
291
|
+
log.info(`${k}: migrating ${flat.length} flat skill(s) under ${parent} → ${NAMESPACE}/`);
|
|
292
|
+
const { moved } = await migrateFlatToNamespace(parent, { quiet: flags.quiet });
|
|
293
|
+
totalMoved += moved;
|
|
294
|
+
}
|
|
295
|
+
if (totalMoved === 0) {
|
|
296
|
+
log.dim('nothing to migrate.');
|
|
297
|
+
} else {
|
|
298
|
+
log.ok(`migration complete: ${totalMoved} skill(s) moved into ${NAMESPACE}/`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Install all shipped skills into each destination, grouped under NAMESPACE.
|
|
304
|
+
*
|
|
305
|
+
* If a destination already has a flat (legacy) layout, auto-migrate it first
|
|
306
|
+
* so users don't end up with duplicate skills (one flat + one namespaced).
|
|
307
|
+
*
|
|
308
|
+
* linkMode:
|
|
309
|
+
* - copy: each destination gets a real namespace directory
|
|
310
|
+
* - central: skills are copied once into ~/.devflow/skills/devflow-kit and
|
|
311
|
+
* every destination namespace becomes a symlink to that source
|
|
312
|
+
*/
|
|
313
|
+
async function installToDests(root, dests, { quiet = false, linkMode = 'copy' } = {}) {
|
|
314
|
+
const entries = (await fs.readdir(SKILLS_DIR, { withFileTypes: true }))
|
|
315
|
+
.filter((e) => e.isDirectory());
|
|
316
|
+
const useCentral = linkMode === 'central';
|
|
317
|
+
const centralDir = expand(USER_SKILLS_SOURCE);
|
|
318
|
+
let centralCount = entries.length;
|
|
319
|
+
if (useCentral) {
|
|
320
|
+
centralCount = await copyShippedSkills(centralDir, entries);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const out = [];
|
|
324
|
+
for (const { key, dest } of dests) {
|
|
325
|
+
const parent = dest.startsWith('~')
|
|
326
|
+
? expand(dest)
|
|
327
|
+
: path.resolve(root, dest);
|
|
328
|
+
await fs.mkdir(parent, { recursive: true });
|
|
329
|
+
|
|
330
|
+
// Auto-migrate flat → namespace before installing fresh, to avoid duplicates
|
|
331
|
+
if (detectLegacyFlat(parent).length) {
|
|
332
|
+
if (!quiet) log.warn(`flat layout detected under ${displayPath(root, dest, parent)} — auto-migrating into ${NAMESPACE}/`);
|
|
333
|
+
await migrateFlatToNamespace(parent, { quiet });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const nsDir = path.join(parent, NAMESPACE);
|
|
337
|
+
if (useCentral) {
|
|
338
|
+
const linked = await replaceWithSymlink(nsDir, centralDir);
|
|
339
|
+
if (!quiet) {
|
|
340
|
+
const action = linked ? 'skill-link' : 'skill-copy';
|
|
341
|
+
log.ok(`${action}: ${displayPath(root, dest, nsDir)} → ${centralDir}`);
|
|
342
|
+
}
|
|
343
|
+
out.push({ key, path: displayPath(root, dest, nsDir), count: centralCount, linked });
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (isSymlink(nsDir)) await fs.unlink(nsDir);
|
|
348
|
+
await fs.mkdir(nsDir, { recursive: true });
|
|
349
|
+
|
|
350
|
+
let count = 0;
|
|
351
|
+
for (const e of entries) {
|
|
352
|
+
const src = path.join(SKILLS_DIR, e.name);
|
|
353
|
+
const dst = path.join(nsDir, e.name);
|
|
354
|
+
await copyDir(src, dst);
|
|
355
|
+
await writeOwnershipMarker(dst);
|
|
356
|
+
count++;
|
|
357
|
+
if (!quiet) log.ok(`skill: ${e.name} → ${displayPath(root, dest, dst)}`);
|
|
358
|
+
}
|
|
359
|
+
out.push({ key, path: displayPath(root, dest, nsDir), count });
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function installForInit(root, targets, { quiet = false } = {}) {
|
|
365
|
+
const dests = targets
|
|
366
|
+
.map((k) => ({ key: k, dest: TARGET_MAP[k] }))
|
|
367
|
+
.filter((x) => !!x.dest);
|
|
368
|
+
if (!dests.length) return [];
|
|
369
|
+
return installToDests(root, dests, { quiet, linkMode: 'copy' });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function copyShippedSkills(dst, entries) {
|
|
373
|
+
const tmp = `${dst}.tmp-${process.pid}-${Date.now()}`;
|
|
374
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
375
|
+
await fs.mkdir(tmp, { recursive: true });
|
|
376
|
+
|
|
377
|
+
let count = 0;
|
|
378
|
+
for (const e of entries) {
|
|
379
|
+
const src = path.join(SKILLS_DIR, e.name);
|
|
380
|
+
const skillDst = path.join(tmp, e.name);
|
|
381
|
+
await copyDir(src, skillDst);
|
|
382
|
+
await writeOwnershipMarker(skillDst);
|
|
383
|
+
count++;
|
|
384
|
+
}
|
|
385
|
+
await fs.writeFile(path.join(tmp, OWNERSHIP_MARKER), 'devflow-kit\n');
|
|
386
|
+
|
|
387
|
+
await removePath(dst);
|
|
388
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
389
|
+
await fs.rename(tmp, dst);
|
|
390
|
+
return count;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function replaceWithSymlink(target, source) {
|
|
394
|
+
await removePath(target);
|
|
395
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
396
|
+
try {
|
|
397
|
+
await fs.symlink(source, target, process.platform === 'win32' ? 'junction' : 'dir');
|
|
398
|
+
return true;
|
|
399
|
+
} catch (_) {
|
|
400
|
+
await copyDir(source, target);
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function removePath(p) {
|
|
406
|
+
let st;
|
|
407
|
+
try {
|
|
408
|
+
st = await fs.lstat(p);
|
|
409
|
+
} catch (_) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (st.isSymbolicLink()) {
|
|
413
|
+
await fs.unlink(p);
|
|
414
|
+
} else {
|
|
415
|
+
await fs.rm(p, { recursive: true, force: true });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function isSymlink(p) {
|
|
420
|
+
try {
|
|
421
|
+
return fsSync.lstatSync(p).isSymbolicLink();
|
|
422
|
+
} catch (_) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function copyDir(src, dst) {
|
|
428
|
+
await fs.mkdir(dst, { recursive: true });
|
|
429
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
430
|
+
for (const e of entries) {
|
|
431
|
+
const s = path.join(src, e.name);
|
|
432
|
+
const d = path.join(dst, e.name);
|
|
433
|
+
if (e.isDirectory()) await copyDir(s, d);
|
|
434
|
+
else await fs.copyFile(s, d);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function writeOwnershipMarker(dir) {
|
|
439
|
+
await fs.writeFile(path.join(dir, OWNERSHIP_MARKER), 'devflow-kit\n');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
module.exports = {
|
|
443
|
+
run,
|
|
444
|
+
installForInit,
|
|
445
|
+
installToDests,
|
|
446
|
+
userSkillsInstalled,
|
|
447
|
+
detectLegacyFlat,
|
|
448
|
+
migrateFlatToNamespace,
|
|
449
|
+
isDevflowOwnedFlatSkill,
|
|
450
|
+
NAMESPACE,
|
|
451
|
+
SHIPPED_SKILLS,
|
|
452
|
+
TARGET_MAP,
|
|
453
|
+
PROJECT_TARGET_MAP,
|
|
454
|
+
USER_TARGET_MAP,
|
|
455
|
+
USER_SCOPE_TARGETS,
|
|
456
|
+
USER_SKILLS_SOURCE,
|
|
457
|
+
};
|