viepilot 1.3.1 → 1.8.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/CHANGELOG.md +51 -1
- package/README.md +23 -19
- package/bin/viepilot.cjs +54 -22
- package/bin/vp-tools.cjs +152 -0
- package/dev-install.sh +29 -8
- package/docs/README.md +4 -2
- package/docs/dev/cli-reference.md +70 -1
- package/docs/dev/contributing.md +4 -0
- package/docs/dev/deployment.md +3 -4
- package/docs/skills-reference.md +44 -0
- package/docs/troubleshooting.md +37 -0
- package/docs/user/features/brainstorm.md +28 -0
- package/docs/user/features/product-horizon.md +18 -0
- package/docs/user/features/ui-direction.md +63 -12
- package/docs/user/quick-start.md +22 -0
- package/install.sh +24 -90
- package/lib/viepilot-info.cjs +196 -0
- package/lib/viepilot-install.cjs +427 -0
- package/lib/viepilot-update.cjs +156 -0
- package/package.json +2 -1
- package/skills/vp-audit/SKILL.md +7 -1
- package/skills/vp-brainstorm/SKILL.md +9 -5
- package/skills/vp-crystallize/SKILL.md +25 -12
- package/skills/vp-debug/SKILL.md +12 -5
- package/skills/vp-info/SKILL.md +62 -0
- package/skills/vp-update/SKILL.md +59 -0
- package/templates/project/AI-GUIDE.md +23 -11
- package/templates/project/ARCHITECTURE.md +53 -0
- package/templates/project/PROJECT-CONTEXT.md +22 -0
- package/templates/project/ROADMAP.md +27 -0
- package/workflows/autonomous.md +7 -0
- package/workflows/brainstorm.md +80 -11
- package/workflows/crystallize.md +53 -9
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViePilot bundle metadata for `vp-tools info` (FEAT-008).
|
|
3
|
+
* Resolves the viepilot package root from the CLI location — no `.viepilot/` project required.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execFileSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Walk upward from startDir (and optionally cwd) for package.json with name "viepilot".
|
|
12
|
+
* @param {string} startDir - e.g. path.join(__dirname, '..') from bin/vp-tools.cjs
|
|
13
|
+
* @returns {string|null} absolute package root or null
|
|
14
|
+
*/
|
|
15
|
+
function resolveViepilotPackageRoot(startDir) {
|
|
16
|
+
const tryRoots = [path.resolve(startDir), path.resolve(process.cwd())];
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
for (const base of tryRoots) {
|
|
19
|
+
if (seen.has(base)) continue;
|
|
20
|
+
seen.add(base);
|
|
21
|
+
let dir = base;
|
|
22
|
+
while (dir && dir !== path.dirname(dir)) {
|
|
23
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
24
|
+
if (fs.existsSync(pkgPath)) {
|
|
25
|
+
try {
|
|
26
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
27
|
+
if (pkg && pkg.name === 'viepilot') {
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
} catch (_e) {
|
|
31
|
+
/* ignore */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
dir = path.dirname(dir);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} root - viepilot package root
|
|
42
|
+
* @returns {string|null}
|
|
43
|
+
*/
|
|
44
|
+
function readInstalledVersion(root) {
|
|
45
|
+
const pkgPath = path.join(root, 'package.json');
|
|
46
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
49
|
+
return pkg.version || null;
|
|
50
|
+
} catch (_e) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Query npm registry for latest published version (requires network + npm on PATH).
|
|
57
|
+
* @returns {{ ok: true, version: string } | { ok: false, error: string }}
|
|
58
|
+
*/
|
|
59
|
+
function fetchLatestNpmVersion() {
|
|
60
|
+
try {
|
|
61
|
+
const out = execFileSync('npm', ['view', 'viepilot', 'version', '--json'], {
|
|
62
|
+
encoding: 'utf8',
|
|
63
|
+
timeout: 15000,
|
|
64
|
+
windowsHide: true,
|
|
65
|
+
}).trim();
|
|
66
|
+
const parsed = JSON.parse(out);
|
|
67
|
+
const version = typeof parsed === 'string' ? parsed : String(parsed);
|
|
68
|
+
return { ok: true, version };
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return { ok: false, error: e.message || String(e) };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse `version:` from YAML-like frontmatter (first --- block).
|
|
76
|
+
* @param {string} content
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
function parseSkillFileVersion(content) {
|
|
80
|
+
if (typeof content !== 'string' || !content.startsWith('---')) {
|
|
81
|
+
return 'unspecified';
|
|
82
|
+
}
|
|
83
|
+
const end = content.indexOf('\n---', 3);
|
|
84
|
+
if (end === -1) {
|
|
85
|
+
return 'unspecified';
|
|
86
|
+
}
|
|
87
|
+
const block = content.slice(3, end);
|
|
88
|
+
const m = block.match(/^version:\s*["']?([^"'\r\n]+)["']?/m);
|
|
89
|
+
return m ? m[1].trim() : 'unspecified';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} root - viepilot package root
|
|
94
|
+
* @returns {Array<{ id: string, version: string, relativePath: string }>}
|
|
95
|
+
*/
|
|
96
|
+
function listSkillsWithVersions(root) {
|
|
97
|
+
const skillsDir = path.join(root, 'skills');
|
|
98
|
+
if (!fs.existsSync(skillsDir)) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const ent of entries) {
|
|
104
|
+
if (!ent.isDirectory()) continue;
|
|
105
|
+
const skillFile = path.join(skillsDir, ent.name, 'SKILL.md');
|
|
106
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
107
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
108
|
+
out.push({
|
|
109
|
+
id: ent.name,
|
|
110
|
+
version: parseSkillFileVersion(content),
|
|
111
|
+
relativePath: path.join('skills', ent.name, 'SKILL.md').replace(/\\/g, '/'),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
out.sort((a, b) => a.id.localeCompare(b.id));
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const WORKFLOW_SEMVER_NOTE = 'no semver in workflow markdown';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} root
|
|
122
|
+
* @returns {Array<{ id: string, relativePath: string, semverInFile: null, note: string }>}
|
|
123
|
+
*/
|
|
124
|
+
function listWorkflows(root) {
|
|
125
|
+
const wfDir = path.join(root, 'workflows');
|
|
126
|
+
if (!fs.existsSync(wfDir)) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const files = fs
|
|
130
|
+
.readdirSync(wfDir)
|
|
131
|
+
.filter((f) => f.endsWith('.md'))
|
|
132
|
+
.sort((a, b) => a.localeCompare(b));
|
|
133
|
+
return files.map((f) => ({
|
|
134
|
+
id: f.replace(/\.md$/i, ''),
|
|
135
|
+
relativePath: path.join('workflows', f).replace(/\\/g, '/'),
|
|
136
|
+
semverInFile: null,
|
|
137
|
+
note: WORKFLOW_SEMVER_NOTE,
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} root
|
|
143
|
+
* @returns {string|null}
|
|
144
|
+
*/
|
|
145
|
+
function tryGitHead(root) {
|
|
146
|
+
try {
|
|
147
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
148
|
+
cwd: root,
|
|
149
|
+
encoding: 'utf8',
|
|
150
|
+
timeout: 8000,
|
|
151
|
+
windowsHide: true,
|
|
152
|
+
}).trim();
|
|
153
|
+
} catch (_e) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {string} root
|
|
160
|
+
* @param {{ includeLatestNpm?: boolean }} [options]
|
|
161
|
+
*/
|
|
162
|
+
function buildInfoReport(root, options = {}) {
|
|
163
|
+
const includeLatestNpm = options.includeLatestNpm !== false;
|
|
164
|
+
const installedVersion = readInstalledVersion(root);
|
|
165
|
+
let pkgName = 'viepilot';
|
|
166
|
+
try {
|
|
167
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
168
|
+
if (pkg.name) pkgName = pkg.name;
|
|
169
|
+
} catch (_e) {
|
|
170
|
+
/* keep default */
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const report = {
|
|
174
|
+
packageRoot: root,
|
|
175
|
+
packageName: pkgName,
|
|
176
|
+
installedVersion: installedVersion || 'unknown',
|
|
177
|
+
latestNpm: includeLatestNpm ? fetchLatestNpmVersion() : { ok: false, error: 'skipped' },
|
|
178
|
+
gitHead: tryGitHead(root),
|
|
179
|
+
skills: listSkillsWithVersions(root),
|
|
180
|
+
workflows: listWorkflows(root),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return report;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
resolveViepilotPackageRoot,
|
|
188
|
+
readInstalledVersion,
|
|
189
|
+
fetchLatestNpmVersion,
|
|
190
|
+
parseSkillFileVersion,
|
|
191
|
+
listSkillsWithVersions,
|
|
192
|
+
listWorkflows,
|
|
193
|
+
tryGitHead,
|
|
194
|
+
buildInfoReport,
|
|
195
|
+
WORKFLOW_SEMVER_NOTE,
|
|
196
|
+
};
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViePilot filesystem install plan — Node implementation (ENH-017 / Phase 28).
|
|
3
|
+
* Mirrors install.sh steps as a structured plan for dry-run and (later) execution.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const { resolveViepilotPackageRoot } = require('./viepilot-info.cjs');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {Record<string, string | undefined>} [envSource]
|
|
13
|
+
* @returns {{ autoYes: boolean, profile: string, addPath: boolean, symlinkSkills: boolean }}
|
|
14
|
+
*/
|
|
15
|
+
function normalizeInstallEnv(envSource = process.env) {
|
|
16
|
+
return {
|
|
17
|
+
autoYes: envSource.VIEPILOT_AUTO_YES === '1',
|
|
18
|
+
profile: envSource.VIEPILOT_INSTALL_PROFILE || 'cursor-ide',
|
|
19
|
+
addPath: envSource.VIEPILOT_ADD_PATH === '1',
|
|
20
|
+
symlinkSkills: envSource.VIEPILOT_SYMLINK_SKILLS === '1',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Same guard as install.sh: README.md + skills/ directory.
|
|
26
|
+
* @param {string} root - viepilot package root
|
|
27
|
+
*/
|
|
28
|
+
function validateViepilotPackageRoot(root) {
|
|
29
|
+
const readme = path.join(root, 'README.md');
|
|
30
|
+
const skillsDir = path.join(root, 'skills');
|
|
31
|
+
if (!fs.existsSync(readme)) {
|
|
32
|
+
const err = new Error('Please run this from the viepilot package root (README.md missing)');
|
|
33
|
+
err.code = 'VIEPILOT_LAYOUT';
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
if (!fs.existsSync(skillsDir) || !fs.statSync(skillsDir).isDirectory()) {
|
|
37
|
+
const err = new Error('Please run this from the viepilot package root (skills/ missing)');
|
|
38
|
+
err.code = 'VIEPILOT_LAYOUT';
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} packageRoot
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
47
|
+
function listSkillDirNames(packageRoot) {
|
|
48
|
+
const skillsDir = path.join(packageRoot, 'skills');
|
|
49
|
+
return fs
|
|
50
|
+
.readdirSync(skillsDir, { withFileTypes: true })
|
|
51
|
+
.filter((d) => d.isDirectory())
|
|
52
|
+
.map((d) => d.name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} packageRoot
|
|
57
|
+
* @param {string} subdir - relative to package root
|
|
58
|
+
*/
|
|
59
|
+
function listDirEntries(packageRoot, subdir) {
|
|
60
|
+
const full = path.join(packageRoot, subdir);
|
|
61
|
+
if (!fs.existsSync(full)) return [];
|
|
62
|
+
return fs.readdirSync(full, { withFileTypes: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build an ordered install plan (no I/O besides reading source tree layout).
|
|
67
|
+
*
|
|
68
|
+
* @param {string} packageRoot - absolute viepilot package root
|
|
69
|
+
* @param {Record<string, string | undefined>} [envSource] - defaults to process.env
|
|
70
|
+
* @param {{ wantPathShim?: boolean, overrideHomedir?: string }} [opts] - `overrideHomedir`: absolute fake home for tests only. `wantPathShim`: append /usr/local/bin steps (skipped on Windows at apply time).
|
|
71
|
+
* @returns {{ version: number, packageRoot: string, env: ReturnType<typeof normalizeInstallEnv>, home: string, paths: object, steps: object[] }}
|
|
72
|
+
*/
|
|
73
|
+
function buildInstallPlan(packageRoot, envSource = process.env, opts = {}) {
|
|
74
|
+
const root = path.resolve(packageRoot);
|
|
75
|
+
validateViepilotPackageRoot(root);
|
|
76
|
+
const env = normalizeInstallEnv(envSource);
|
|
77
|
+
const home =
|
|
78
|
+
opts.overrideHomedir != null ? path.resolve(opts.overrideHomedir) : os.homedir();
|
|
79
|
+
const cursorSkillsDir = path.join(home, '.cursor', 'skills');
|
|
80
|
+
const viepilotDir = path.join(home, '.cursor', 'viepilot');
|
|
81
|
+
|
|
82
|
+
let wantPathShim = opts.wantPathShim;
|
|
83
|
+
if (wantPathShim === undefined) {
|
|
84
|
+
wantPathShim = env.autoYes && env.addPath;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @type {object[]} */
|
|
88
|
+
const steps = [];
|
|
89
|
+
|
|
90
|
+
const mkdirTargets = [
|
|
91
|
+
cursorSkillsDir,
|
|
92
|
+
path.join(viepilotDir, 'workflows'),
|
|
93
|
+
path.join(viepilotDir, 'templates', 'project'),
|
|
94
|
+
path.join(viepilotDir, 'templates', 'phase'),
|
|
95
|
+
path.join(viepilotDir, 'bin'),
|
|
96
|
+
path.join(viepilotDir, 'lib'),
|
|
97
|
+
path.join(viepilotDir, 'ui-components'),
|
|
98
|
+
];
|
|
99
|
+
for (const dir of mkdirTargets) {
|
|
100
|
+
steps.push({ kind: 'mkdir', path: dir });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const name of listSkillDirNames(root)) {
|
|
104
|
+
const src = path.join(root, 'skills', name);
|
|
105
|
+
const dest = path.join(cursorSkillsDir, name);
|
|
106
|
+
if (env.symlinkSkills) {
|
|
107
|
+
steps.push({
|
|
108
|
+
kind: 'symlink_dir',
|
|
109
|
+
target: dest,
|
|
110
|
+
sourceAbsolute: path.resolve(src),
|
|
111
|
+
});
|
|
112
|
+
} else {
|
|
113
|
+
steps.push({ kind: 'copy_dir', from: src, to: dest });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const ent of listDirEntries(root, 'workflows')) {
|
|
118
|
+
const src = path.join(root, 'workflows', ent.name);
|
|
119
|
+
const dest = path.join(viepilotDir, 'workflows', ent.name);
|
|
120
|
+
if (ent.isDirectory()) {
|
|
121
|
+
steps.push({ kind: 'copy_dir', from: src, to: dest });
|
|
122
|
+
} else if (ent.isFile()) {
|
|
123
|
+
steps.push({ kind: 'copy_file', from: src, to: dest });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const ent of listDirEntries(root, path.join('templates', 'project'))) {
|
|
128
|
+
const src = path.join(root, 'templates', 'project', ent.name);
|
|
129
|
+
const dest = path.join(viepilotDir, 'templates', 'project', ent.name);
|
|
130
|
+
if (ent.isDirectory()) {
|
|
131
|
+
steps.push({ kind: 'copy_dir', from: src, to: dest });
|
|
132
|
+
} else if (ent.isFile()) {
|
|
133
|
+
steps.push({ kind: 'copy_file', from: src, to: dest });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const ent of listDirEntries(root, path.join('templates', 'phase'))) {
|
|
138
|
+
const src = path.join(root, 'templates', 'phase', ent.name);
|
|
139
|
+
const dest = path.join(viepilotDir, 'templates', 'phase', ent.name);
|
|
140
|
+
if (ent.isDirectory()) {
|
|
141
|
+
steps.push({ kind: 'copy_dir', from: src, to: dest });
|
|
142
|
+
} else if (ent.isFile()) {
|
|
143
|
+
steps.push({ kind: 'copy_file', from: src, to: dest });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const uiRoot = path.join(root, 'ui-components');
|
|
148
|
+
if (fs.existsSync(uiRoot)) {
|
|
149
|
+
for (const ent of listDirEntries(root, 'ui-components')) {
|
|
150
|
+
const src = path.join(root, 'ui-components', ent.name);
|
|
151
|
+
const dest = path.join(viepilotDir, 'ui-components', ent.name);
|
|
152
|
+
if (ent.isDirectory()) {
|
|
153
|
+
steps.push({ kind: 'copy_dir', from: src, to: dest });
|
|
154
|
+
} else if (ent.isFile()) {
|
|
155
|
+
steps.push({ kind: 'copy_file', from: src, to: dest });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const binFiles = ['vp-tools.cjs', 'viepilot.cjs'];
|
|
161
|
+
for (const f of binFiles) {
|
|
162
|
+
steps.push({
|
|
163
|
+
kind: 'copy_file',
|
|
164
|
+
from: path.join(root, 'bin', f),
|
|
165
|
+
to: path.join(viepilotDir, 'bin', f),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
steps.push({
|
|
169
|
+
kind: 'copy_file',
|
|
170
|
+
from: path.join(root, 'lib', 'cli-shared.cjs'),
|
|
171
|
+
to: path.join(viepilotDir, 'lib', 'cli-shared.cjs'),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
for (const f of binFiles) {
|
|
175
|
+
steps.push({
|
|
176
|
+
kind: 'chmod',
|
|
177
|
+
path: path.join(viepilotDir, 'bin', f),
|
|
178
|
+
mode: 0o755,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
steps.push({
|
|
183
|
+
kind: 'note',
|
|
184
|
+
id: 'cloc_optional',
|
|
185
|
+
message:
|
|
186
|
+
'Check optional cloc for README metrics (brew/apt/dnf/choco); installation continues if missing.',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (wantPathShim) {
|
|
190
|
+
steps.push({
|
|
191
|
+
kind: 'path_shim',
|
|
192
|
+
links: [
|
|
193
|
+
{ path: '/usr/local/bin/vp-tools', target: path.join(viepilotDir, 'bin', 'vp-tools.cjs') },
|
|
194
|
+
{ path: '/usr/local/bin/viepilot', target: path.join(viepilotDir, 'bin', 'viepilot.cjs') },
|
|
195
|
+
],
|
|
196
|
+
note: 'Unix typical; on Windows native, PATH shim may be skipped or manual.',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
version: 1,
|
|
202
|
+
packageRoot: root,
|
|
203
|
+
env,
|
|
204
|
+
home,
|
|
205
|
+
paths: {
|
|
206
|
+
cursorSkillsDir,
|
|
207
|
+
viepilotDir,
|
|
208
|
+
},
|
|
209
|
+
steps,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Human-readable lines for dry-run output.
|
|
215
|
+
* @param {ReturnType<typeof buildInstallPlan>} plan
|
|
216
|
+
* @returns {string[]}
|
|
217
|
+
*/
|
|
218
|
+
function formatPlanLines(plan) {
|
|
219
|
+
const lines = [];
|
|
220
|
+
lines.push('ViePilot install plan (dry-run)');
|
|
221
|
+
lines.push(` packageRoot: ${plan.packageRoot}`);
|
|
222
|
+
lines.push(` profile: ${plan.env.profile} (informational; same file set as install.sh)`);
|
|
223
|
+
lines.push(` skills: ${plan.paths.cursorSkillsDir}`);
|
|
224
|
+
lines.push(` viepilot: ${plan.paths.viepilotDir}`);
|
|
225
|
+
lines.push('');
|
|
226
|
+
for (let i = 0; i < plan.steps.length; i++) {
|
|
227
|
+
const s = plan.steps[i];
|
|
228
|
+
const n = i + 1;
|
|
229
|
+
switch (s.kind) {
|
|
230
|
+
case 'mkdir':
|
|
231
|
+
lines.push(`${n}. mkdir -p ${s.path}`);
|
|
232
|
+
break;
|
|
233
|
+
case 'copy_file':
|
|
234
|
+
lines.push(`${n}. copy ${s.from} -> ${s.to}`);
|
|
235
|
+
break;
|
|
236
|
+
case 'copy_dir':
|
|
237
|
+
lines.push(`${n}. copyDir ${s.from} -> ${s.to}`);
|
|
238
|
+
break;
|
|
239
|
+
case 'symlink_dir':
|
|
240
|
+
lines.push(`${n}. symlink ${s.target} -> ${s.sourceAbsolute}`);
|
|
241
|
+
break;
|
|
242
|
+
case 'chmod':
|
|
243
|
+
lines.push(`${n}. chmod ${s.mode.toString(8)} ${s.path}`);
|
|
244
|
+
break;
|
|
245
|
+
case 'note':
|
|
246
|
+
lines.push(`${n}. note: ${s.message}`);
|
|
247
|
+
break;
|
|
248
|
+
case 'path_shim':
|
|
249
|
+
lines.push(`${n}. path shim: ${s.links.map((l) => `${l.path} -> ${l.target}`).join('; ')}`);
|
|
250
|
+
if (s.note) lines.push(` (${s.note})`);
|
|
251
|
+
break;
|
|
252
|
+
default:
|
|
253
|
+
lines.push(`${n}. ${JSON.stringify(s)}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return lines;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function removePathIfExists(p) {
|
|
260
|
+
if (fs.existsSync(p)) {
|
|
261
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function ensureDirForFile(filePath) {
|
|
266
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function copyDirRecursive(src, dest) {
|
|
270
|
+
removePathIfExists(dest);
|
|
271
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
272
|
+
if (typeof fs.cpSync === 'function') {
|
|
273
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
277
|
+
for (const ent of fs.readdirSync(src, { withFileTypes: true })) {
|
|
278
|
+
const s = path.join(src, ent.name);
|
|
279
|
+
const d = path.join(dest, ent.name);
|
|
280
|
+
if (ent.isDirectory()) {
|
|
281
|
+
copyDirRecursive(s, d);
|
|
282
|
+
} else {
|
|
283
|
+
fs.copyFileSync(s, d);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function symlinkSkillDir(sourceAbsolute, target) {
|
|
289
|
+
removePathIfExists(target);
|
|
290
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
291
|
+
const linkType = process.platform === 'win32' ? 'dir' : 'dir';
|
|
292
|
+
fs.symlinkSync(sourceAbsolute, target, linkType);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Same messaging as install.sh install_cloc_best_effort (non-blocking; no interactive install here).
|
|
297
|
+
* @param {ReturnType<typeof normalizeInstallEnv>} env
|
|
298
|
+
* @returns {string[]}
|
|
299
|
+
*/
|
|
300
|
+
function getClocGuidanceLines(env) {
|
|
301
|
+
const lines = [];
|
|
302
|
+
try {
|
|
303
|
+
const { execFileSync } = require('child_process');
|
|
304
|
+
execFileSync('cloc', ['--version'], { stdio: 'ignore' });
|
|
305
|
+
lines.push(' ✓ cloc detected');
|
|
306
|
+
return lines;
|
|
307
|
+
} catch (_e) {
|
|
308
|
+
/* continue */
|
|
309
|
+
}
|
|
310
|
+
lines.push(' cloc not found.');
|
|
311
|
+
lines.push(' README metric auto-sync can still run with fallback, but LOC refresh will be skipped.');
|
|
312
|
+
lines.push(' Suggested install:');
|
|
313
|
+
lines.push(' - macOS: brew install cloc');
|
|
314
|
+
lines.push(' - Ubuntu/Debian: sudo apt-get install -y cloc');
|
|
315
|
+
lines.push(' - Windows: choco install cloc');
|
|
316
|
+
if (!env.autoYes) {
|
|
317
|
+
lines.push(' (Interactive cloc install omitted in Node installer; set AUTO_YES and use OS package manager.)');
|
|
318
|
+
}
|
|
319
|
+
return lines;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Apply install plan to disk (or dry-run log only).
|
|
324
|
+
*
|
|
325
|
+
* @param {ReturnType<typeof buildInstallPlan>} plan
|
|
326
|
+
* @param {{ dryRun?: boolean }} [options]
|
|
327
|
+
* @returns {{ ok: boolean, logs: string[], errors: { step: object, error: Error }[] }}
|
|
328
|
+
*/
|
|
329
|
+
function applyInstallPlan(plan, options = {}) {
|
|
330
|
+
const dryRun = !!options.dryRun;
|
|
331
|
+
/** @type {string[]} */
|
|
332
|
+
const logs = [];
|
|
333
|
+
/** @type {{ step: object, error: Error }[]} */
|
|
334
|
+
const errors = [];
|
|
335
|
+
|
|
336
|
+
for (const step of plan.steps) {
|
|
337
|
+
try {
|
|
338
|
+
switch (step.kind) {
|
|
339
|
+
case 'mkdir':
|
|
340
|
+
if (dryRun) logs.push(`[dry-run] mkdir -p ${step.path}`);
|
|
341
|
+
else fs.mkdirSync(step.path, { recursive: true });
|
|
342
|
+
break;
|
|
343
|
+
case 'copy_file':
|
|
344
|
+
if (dryRun) logs.push(`[dry-run] copy ${step.from} -> ${step.to}`);
|
|
345
|
+
else {
|
|
346
|
+
if (!fs.existsSync(step.from)) {
|
|
347
|
+
throw new Error(`Missing source: ${step.from}`);
|
|
348
|
+
}
|
|
349
|
+
ensureDirForFile(step.to);
|
|
350
|
+
fs.copyFileSync(step.from, step.to);
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
case 'copy_dir':
|
|
354
|
+
if (dryRun) logs.push(`[dry-run] copyDir ${step.from} -> ${step.to}`);
|
|
355
|
+
else {
|
|
356
|
+
if (!fs.existsSync(step.from)) {
|
|
357
|
+
throw new Error(`Missing source dir: ${step.from}`);
|
|
358
|
+
}
|
|
359
|
+
copyDirRecursive(step.from, step.to);
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
case 'symlink_dir':
|
|
363
|
+
if (dryRun) {
|
|
364
|
+
logs.push(`[dry-run] symlink ${step.target} -> ${step.sourceAbsolute}`);
|
|
365
|
+
} else {
|
|
366
|
+
symlinkSkillDir(step.sourceAbsolute, step.target);
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
case 'chmod':
|
|
370
|
+
if (dryRun) logs.push(`[dry-run] chmod ${step.mode.toString(8)} ${step.path}`);
|
|
371
|
+
else {
|
|
372
|
+
try {
|
|
373
|
+
fs.chmodSync(step.path, step.mode);
|
|
374
|
+
} catch (e) {
|
|
375
|
+
logs.push(`chmod skipped: ${step.path} (${e.message})`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
case 'note':
|
|
380
|
+
if (step.id === 'cloc_optional') {
|
|
381
|
+
logs.push(...getClocGuidanceLines(plan.env));
|
|
382
|
+
} else {
|
|
383
|
+
logs.push(`note: ${step.message}`);
|
|
384
|
+
}
|
|
385
|
+
break;
|
|
386
|
+
case 'path_shim':
|
|
387
|
+
if (process.platform === 'win32') {
|
|
388
|
+
logs.push('path_shim: skipped on Windows (add ~/.cursor/viepilot/bin to PATH manually if needed).');
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
if (dryRun) {
|
|
392
|
+
logs.push(`[dry-run] path_shim: ${step.links.map((l) => `${l.path} -> ${l.target}`).join('; ')}`);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
for (const L of step.links) {
|
|
396
|
+
try {
|
|
397
|
+
removePathIfExists(L.path);
|
|
398
|
+
fs.mkdirSync(path.dirname(L.path), { recursive: true });
|
|
399
|
+
fs.symlinkSync(L.target, L.path, 'file');
|
|
400
|
+
logs.push(`path_shim: ${L.path} -> ${L.target}`);
|
|
401
|
+
} catch (e) {
|
|
402
|
+
logs.push(`path_shim failed ${L.path}: ${e.message} (try sudo or adjust permissions)`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
default:
|
|
407
|
+
logs.push(`unknown step kind: ${JSON.stringify(step)}`);
|
|
408
|
+
}
|
|
409
|
+
} catch (e) {
|
|
410
|
+
errors.push({ step, error: e });
|
|
411
|
+
return { ok: false, logs, errors };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return { ok: true, logs, errors };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
module.exports = {
|
|
419
|
+
resolveViepilotPackageRoot,
|
|
420
|
+
normalizeInstallEnv,
|
|
421
|
+
validateViepilotPackageRoot,
|
|
422
|
+
listSkillDirNames,
|
|
423
|
+
buildInstallPlan,
|
|
424
|
+
formatPlanLines,
|
|
425
|
+
applyInstallPlan,
|
|
426
|
+
getClocGuidanceLines,
|
|
427
|
+
};
|