msdevflow 0.7.4 → 0.7.6
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/README.md +39 -40
- package/lib/bootstrap.js +485 -103
- package/package.json +2 -2
- package/skill/{msdevflow → msd}/README.md +54 -39
- package/skill/{msdevflow → msd}/SKILL.md +26 -35
- package/skill/{msdevflow → msd}/references/ci-and-review.md +2 -2
- package/skill/{msdevflow → msd}/references/command-capabilities.md +1 -1
- package/skill/{msdevflow → msd}/references/create-issue.md +1 -1
- package/skill/{msdevflow → msd}/references/design-and-development.md +1 -1
- package/skill/{msdevflow → msd}/references/openlibing-ci.md +9 -2
- package/skill/{msdevflow → msd}/references/pr-and-ci.md +2 -2
- package/skill/{msdevflow → msd}/references/recovery.md +1 -1
- package/skill/{msdevflow → msd}/references/review-and-merge.md +2 -2
- package/skill/{msdevflow → msd}/references/setup-and-issue.md +2 -2
- package/skill/{msdevflow → msd}/references/state-and-safety.md +1 -1
- package/skill/{msdevflow → msd}/scripts/openlibing_ci.py +62 -21
- package/skill/msd-ci/SKILL.md +15 -0
- package/skill/msd-code-review/SKILL.md +15 -0
- package/skill/msd-create-issue/SKILL.md +15 -0
- package/skill/msd-develop/SKILL.md +15 -0
- package/skill/msd-discover/SKILL.md +15 -0
- package/skill/msd-feedback/SKILL.md +15 -0
- package/skill/msd-issue/SKILL.md +15 -0
- package/skill/msd-merge/SKILL.md +15 -0
- package/skill/msd-openlibing-auth/SKILL.md +15 -0
- package/skill/msd-pr/SKILL.md +15 -0
- /package/skill/{msdevflow → msd}/references/code-review.md +0 -0
- /package/skill/{msdevflow → msd}/scripts/requirements.txt +0 -0
package/lib/bootstrap.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
readdirSync,
|
|
12
12
|
readSync,
|
|
13
13
|
renameSync,
|
|
14
|
+
rmdirSync,
|
|
14
15
|
rmSync,
|
|
15
16
|
unlinkSync,
|
|
16
17
|
writeFileSync,
|
|
@@ -24,7 +25,27 @@ import { fileURLToPath } from "node:url";
|
|
|
24
25
|
const REGISTRY = "https://registry.npmjs.org";
|
|
25
26
|
const GITCODE_PACKAGE = "@gitcode-cli/cli@latest";
|
|
26
27
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
|
-
const
|
|
28
|
+
const BUNDLED_SKILLS_DIR = path.join(PACKAGE_ROOT, "skill");
|
|
29
|
+
const CORE_SKILL_NAME = "msd";
|
|
30
|
+
const LEGACY_SKILL_NAME = "msdevflow";
|
|
31
|
+
const PYTHON_RUNTIME_MARKER = ".msdevflow-python-runtime";
|
|
32
|
+
const PYTHON_RUNTIME_MARKER_CONTENT = "managed-by=msdevflow\n";
|
|
33
|
+
export const ACTIONS = [
|
|
34
|
+
"discover",
|
|
35
|
+
"create-issue",
|
|
36
|
+
"issue",
|
|
37
|
+
"develop",
|
|
38
|
+
"pr",
|
|
39
|
+
"ci",
|
|
40
|
+
"openlibing-auth",
|
|
41
|
+
"feedback",
|
|
42
|
+
"code-review",
|
|
43
|
+
"merge",
|
|
44
|
+
];
|
|
45
|
+
export const MANAGED_SKILL_NAMES = [
|
|
46
|
+
CORE_SKILL_NAME,
|
|
47
|
+
...ACTIONS.map((action) => `${CORE_SKILL_NAME}-${action}`),
|
|
48
|
+
];
|
|
28
49
|
const SUPPORTED_CLIENTS = ["claude", "codex", "opencode"];
|
|
29
50
|
const REQUIRED_SCHEMAS = {
|
|
30
51
|
"auth login": ["web"],
|
|
@@ -70,8 +91,8 @@ client is detected, interactive setup asks which clients to install; non-interac
|
|
|
70
91
|
setup requires --targets. OpenCode reuses a Claude or shared Agent Skills target
|
|
71
92
|
instead of creating a third copy.
|
|
72
93
|
|
|
73
|
-
The setup installs or updates the bundled
|
|
74
|
-
the official GitCode npm CLI, and installs all reviewed Python runtime
|
|
94
|
+
The setup installs or updates the bundled msd core and msd-<action> skills,
|
|
95
|
+
installs or upgrades the official GitCode npm CLI, and installs all reviewed Python runtime
|
|
75
96
|
dependencies. It never downloads or launches Chromium. If GitCode is not
|
|
76
97
|
authenticated, it starts the CLI's official browser login; setup never reads or
|
|
77
98
|
prints a token.`;
|
|
@@ -167,29 +188,33 @@ function defaultAgentsSkillsDir(options, environment, platform) {
|
|
|
167
188
|
|| path.join(environmentHome(environment, platform), ".agents", "skills");
|
|
168
189
|
}
|
|
169
190
|
|
|
170
|
-
function createSkillLayout(kind, clients, skillsDir,
|
|
191
|
+
function createSkillLayout(kind, clients, skillsDir, bundledSkillsDir) {
|
|
171
192
|
const resolvedSkillsDir = path.resolve(skillsDir);
|
|
172
|
-
const
|
|
173
|
-
const
|
|
193
|
+
const resolvedBundledSkillsDir = path.resolve(bundledSkillsDir);
|
|
194
|
+
const coreDir = path.join(resolvedSkillsDir, CORE_SKILL_NAME);
|
|
195
|
+
const bundledCoreDir = path.join(resolvedBundledSkillsDir, CORE_SKILL_NAME);
|
|
174
196
|
return {
|
|
175
197
|
kind,
|
|
176
198
|
clients,
|
|
177
199
|
skillsDir: resolvedSkillsDir,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
pythonRequirements: path.join(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
200
|
+
coreDir,
|
|
201
|
+
coreManifest: path.join(coreDir, "SKILL.md"),
|
|
202
|
+
pythonRequirements: path.join(coreDir, "scripts", "requirements.txt"),
|
|
203
|
+
legacyDir: path.join(resolvedSkillsDir, LEGACY_SKILL_NAME),
|
|
204
|
+
legacyManifest: path.join(resolvedSkillsDir, LEGACY_SKILL_NAME, "SKILL.md"),
|
|
205
|
+
bundledSkillsDir: resolvedBundledSkillsDir,
|
|
206
|
+
bundledCoreDir,
|
|
207
|
+
bundledCoreManifest: path.join(bundledCoreDir, "SKILL.md"),
|
|
208
|
+
bundledPythonRequirements: path.join(bundledCoreDir, "scripts", "requirements.txt"),
|
|
184
209
|
};
|
|
185
210
|
}
|
|
186
211
|
|
|
187
|
-
export function resolveLayout(options, environment = process.env,
|
|
212
|
+
export function resolveLayout(options, environment = process.env, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
|
|
188
213
|
return createSkillLayout(
|
|
189
214
|
"claude",
|
|
190
215
|
["claude"],
|
|
191
216
|
defaultClaudeSkillsDir(options, environment, process.platform),
|
|
192
|
-
|
|
217
|
+
bundledSkillsDir,
|
|
193
218
|
);
|
|
194
219
|
}
|
|
195
220
|
|
|
@@ -197,7 +222,7 @@ export function resolveLayouts(
|
|
|
197
222
|
options,
|
|
198
223
|
clients,
|
|
199
224
|
environment = process.env,
|
|
200
|
-
|
|
225
|
+
bundledSkillsDir = BUNDLED_SKILLS_DIR,
|
|
201
226
|
platform = process.platform,
|
|
202
227
|
) {
|
|
203
228
|
const layouts = [];
|
|
@@ -207,7 +232,7 @@ export function resolveLayouts(
|
|
|
207
232
|
"claude",
|
|
208
233
|
openCodeUsesClaude ? ["claude", "opencode"] : ["claude"],
|
|
209
234
|
defaultClaudeSkillsDir(options, environment, platform),
|
|
210
|
-
|
|
235
|
+
bundledSkillsDir,
|
|
211
236
|
));
|
|
212
237
|
}
|
|
213
238
|
if (clients.includes("codex") || (clients.includes("opencode") && !openCodeUsesClaude)) {
|
|
@@ -219,12 +244,12 @@ export function resolveLayouts(
|
|
|
219
244
|
"agents",
|
|
220
245
|
sharedClients,
|
|
221
246
|
defaultAgentsSkillsDir(options, environment, platform),
|
|
222
|
-
|
|
247
|
+
bundledSkillsDir,
|
|
223
248
|
));
|
|
224
249
|
}
|
|
225
250
|
const unique = new Map();
|
|
226
251
|
for (const layout of layouts) {
|
|
227
|
-
const key = normalizedPath(layout.
|
|
252
|
+
const key = normalizedPath(layout.skillsDir, platform);
|
|
228
253
|
const existing = unique.get(key);
|
|
229
254
|
if (existing) {
|
|
230
255
|
existing.clients = [...new Set([...existing.clients, ...layout.clients])];
|
|
@@ -440,7 +465,7 @@ function requireFile(file, description) {
|
|
|
440
465
|
verifyFile(file, `${description} not found: ${file}. Reinstall the msdevflow npm package.`);
|
|
441
466
|
}
|
|
442
467
|
|
|
443
|
-
function skillManifestIsOwned(file) {
|
|
468
|
+
function skillManifestIsOwned(file, expectedName) {
|
|
444
469
|
try {
|
|
445
470
|
const metadata = lstatSync(file);
|
|
446
471
|
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
@@ -451,7 +476,7 @@ function skillManifestIsOwned(file) {
|
|
|
451
476
|
return false;
|
|
452
477
|
}
|
|
453
478
|
const names = [...frontmatter[1].matchAll(/^name:\s*([^\s#]+)\s*$/gm)];
|
|
454
|
-
return names.length === 1 && names[0][1] ===
|
|
479
|
+
return names.length === 1 && names[0][1] === expectedName;
|
|
455
480
|
} catch (error) {
|
|
456
481
|
if (error?.code === "ENOENT") {
|
|
457
482
|
return false;
|
|
@@ -491,26 +516,25 @@ function skillDigest(root) {
|
|
|
491
516
|
return digest.digest("hex");
|
|
492
517
|
}
|
|
493
518
|
|
|
494
|
-
function
|
|
495
|
-
|
|
496
|
-
requireFile(
|
|
497
|
-
if (!skillManifestIsOwned(
|
|
498
|
-
throw new BootstrapError(
|
|
519
|
+
function installedSkillDetails(source, target, name) {
|
|
520
|
+
const sourceManifest = path.join(source, "SKILL.md");
|
|
521
|
+
requireFile(sourceManifest, `bundled ${name} skill manifest`);
|
|
522
|
+
if (!skillManifestIsOwned(sourceManifest, name)) {
|
|
523
|
+
throw new BootstrapError(`Bundled skill manifest does not identify ${name}.`, 3);
|
|
499
524
|
}
|
|
500
|
-
|
|
501
|
-
const bundledDigest = skillDigest(layout.bundledSkillDir);
|
|
525
|
+
const bundledDigest = skillDigest(source);
|
|
502
526
|
let status = "absent";
|
|
503
527
|
let existingDigest = null;
|
|
504
528
|
try {
|
|
505
|
-
const metadata = lstatSync(
|
|
529
|
+
const metadata = lstatSync(target);
|
|
506
530
|
if (metadata.isSymbolicLink() || !metadata.isDirectory()
|
|
507
|
-
|| !skillManifestIsOwned(
|
|
531
|
+
|| !skillManifestIsOwned(path.join(target, "SKILL.md"), name)) {
|
|
508
532
|
throw new BootstrapError(
|
|
509
|
-
`Refusing to overwrite a skills entry not owned by
|
|
533
|
+
`Refusing to overwrite a skills entry not owned by ${name}: ${target}`,
|
|
510
534
|
3,
|
|
511
535
|
);
|
|
512
536
|
}
|
|
513
|
-
existingDigest = skillDigest(
|
|
537
|
+
existingDigest = skillDigest(target);
|
|
514
538
|
status = existingDigest === bundledDigest ? "current" : "update";
|
|
515
539
|
} catch (error) {
|
|
516
540
|
if (error?.code !== "ENOENT") {
|
|
@@ -518,70 +542,211 @@ function skillInstallDetails(layout) {
|
|
|
518
542
|
}
|
|
519
543
|
}
|
|
520
544
|
return {
|
|
521
|
-
|
|
522
|
-
|
|
545
|
+
name,
|
|
546
|
+
source,
|
|
547
|
+
target,
|
|
523
548
|
status,
|
|
524
549
|
bundledDigest,
|
|
525
550
|
existingDigest,
|
|
526
551
|
};
|
|
527
552
|
}
|
|
528
553
|
|
|
529
|
-
function
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
554
|
+
function legacySkillDetails(layout) {
|
|
555
|
+
try {
|
|
556
|
+
const metadata = lstatSync(layout.legacyDir);
|
|
557
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()
|
|
558
|
+
|| !skillManifestIsOwned(layout.legacyManifest, LEGACY_SKILL_NAME)) {
|
|
559
|
+
throw new BootstrapError(
|
|
560
|
+
`Refusing to migrate a legacy skills entry not owned by ${LEGACY_SKILL_NAME}: ${layout.legacyDir}`,
|
|
561
|
+
3,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
target: layout.legacyDir,
|
|
566
|
+
status: "migrate",
|
|
567
|
+
existingDigest: skillDigest(layout.legacyDir),
|
|
568
|
+
};
|
|
569
|
+
} catch (error) {
|
|
570
|
+
if (error?.code === "ENOENT") {
|
|
571
|
+
return {
|
|
572
|
+
target: layout.legacyDir,
|
|
573
|
+
status: "absent",
|
|
574
|
+
existingDigest: null,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
throw error;
|
|
541
578
|
}
|
|
542
|
-
|
|
543
|
-
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function skillInstallDetails(layout) {
|
|
582
|
+
requireFile(layout.bundledPythonRequirements, "bundled workflow Python requirements file");
|
|
583
|
+
validatePythonRequirements(layout.bundledPythonRequirements);
|
|
584
|
+
const entries = MANAGED_SKILL_NAMES.map((name) => installedSkillDetails(
|
|
585
|
+
path.join(layout.bundledSkillsDir, name),
|
|
586
|
+
path.join(layout.skillsDir, name),
|
|
587
|
+
name,
|
|
588
|
+
));
|
|
589
|
+
return {
|
|
590
|
+
source: layout.bundledSkillsDir,
|
|
591
|
+
target: layout.skillsDir,
|
|
592
|
+
entries,
|
|
593
|
+
legacy: legacySkillDetails(layout),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function assertSkillInstallUnchanged(details) {
|
|
598
|
+
const layout = {
|
|
599
|
+
skillsDir: details.target,
|
|
600
|
+
bundledSkillsDir: details.source,
|
|
601
|
+
bundledPythonRequirements: path.join(
|
|
602
|
+
details.source,
|
|
603
|
+
CORE_SKILL_NAME,
|
|
604
|
+
"scripts",
|
|
605
|
+
"requirements.txt",
|
|
606
|
+
),
|
|
607
|
+
legacyDir: path.join(details.target, LEGACY_SKILL_NAME),
|
|
608
|
+
legacyManifest: path.join(details.target, LEGACY_SKILL_NAME, "SKILL.md"),
|
|
609
|
+
};
|
|
610
|
+
const current = skillInstallDetails(layout);
|
|
611
|
+
for (let index = 0; index < details.entries.length; index += 1) {
|
|
612
|
+
const expected = details.entries[index];
|
|
613
|
+
const actual = current.entries[index];
|
|
614
|
+
if (actual.name !== expected.name || actual.status !== expected.status
|
|
615
|
+
|| actual.existingDigest !== expected.existingDigest
|
|
616
|
+
|| actual.bundledDigest !== expected.bundledDigest) {
|
|
617
|
+
throw new BootstrapError(
|
|
618
|
+
`Skill target changed after confirmation; refusing to overwrite it: ${expected.target}`,
|
|
619
|
+
3,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (current.legacy.status !== details.legacy.status
|
|
624
|
+
|| current.legacy.existingDigest !== details.legacy.existingDigest) {
|
|
625
|
+
throw new BootstrapError(
|
|
626
|
+
`Legacy skill target changed after confirmation; refusing to migrate it: ${details.legacy.target}`,
|
|
627
|
+
3,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function installedSuiteResult(details) {
|
|
633
|
+
const entries = details.entries.map((entry) => ({
|
|
634
|
+
name: entry.name,
|
|
635
|
+
status: entry.status === "current"
|
|
636
|
+
? "current"
|
|
637
|
+
: entry.status === "absent" ? "installed" : "updated",
|
|
638
|
+
target: entry.target,
|
|
639
|
+
}));
|
|
640
|
+
let status = "current";
|
|
641
|
+
if (details.legacy.status === "migrate") {
|
|
642
|
+
status = "migrated";
|
|
643
|
+
} else if (entries.some((entry) => entry.status === "updated")) {
|
|
644
|
+
status = "updated";
|
|
645
|
+
} else if (entries.some((entry) => entry.status === "installed")) {
|
|
646
|
+
status = "installed";
|
|
647
|
+
}
|
|
648
|
+
return { status, entries, legacy: details.legacy.status };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function suiteTransactionPath(details, name, extension) {
|
|
652
|
+
const skillsRootName = path.basename(details.target) || "skills";
|
|
653
|
+
return path.join(
|
|
654
|
+
path.dirname(details.target),
|
|
655
|
+
`.${skillsRootName}-msdevflow-${name}-${randomUUID()}.${extension}`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function installBundledSkillSuites(suites) {
|
|
660
|
+
for (const details of suites) {
|
|
661
|
+
mkdirSync(details.target, { recursive: true });
|
|
662
|
+
assertSkillInstallUnchanged(details);
|
|
544
663
|
}
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
|
|
664
|
+
const staged = [];
|
|
665
|
+
const switched = [];
|
|
666
|
+
const migrated = [];
|
|
548
667
|
try {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
668
|
+
for (const details of suites) {
|
|
669
|
+
for (const entry of details.entries.filter((item) => item.status !== "current")) {
|
|
670
|
+
const staging = suiteTransactionPath(details, entry.name, "tmp");
|
|
671
|
+
cpSync(entry.source, staging, { recursive: true, errorOnExist: true });
|
|
672
|
+
if (skillDigest(staging) !== entry.bundledDigest) {
|
|
673
|
+
throw new BootstrapError(`Bundled ${entry.name} verification failed after staging.`, 3);
|
|
674
|
+
}
|
|
675
|
+
staged.push({ details, entry, staging });
|
|
676
|
+
}
|
|
552
677
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
movedExisting = true;
|
|
678
|
+
for (const details of suites) {
|
|
679
|
+
assertSkillInstallUnchanged(details);
|
|
556
680
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
renameSync(backup, details.target);
|
|
562
|
-
movedExisting = false;
|
|
681
|
+
for (const item of staged) {
|
|
682
|
+
const backup = suiteTransactionPath(item.details, item.entry.name, "backup");
|
|
683
|
+
if (item.entry.status === "update") {
|
|
684
|
+
renameSync(item.entry.target, backup);
|
|
563
685
|
}
|
|
564
|
-
|
|
686
|
+
try {
|
|
687
|
+
renameSync(item.staging, item.entry.target);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
if (item.entry.status === "update") {
|
|
690
|
+
renameSync(backup, item.entry.target);
|
|
691
|
+
}
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
694
|
+
switched.push({ ...item, backup: item.entry.status === "update" ? backup : null });
|
|
695
|
+
}
|
|
696
|
+
for (const details of suites.filter((item) => item.legacy.status === "migrate")) {
|
|
697
|
+
const backup = suiteTransactionPath(details, LEGACY_SKILL_NAME, "backup");
|
|
698
|
+
renameSync(details.legacy.target, backup);
|
|
699
|
+
migrated.push({ details, backup });
|
|
565
700
|
}
|
|
566
|
-
|
|
567
|
-
|
|
701
|
+
} catch (error) {
|
|
702
|
+
const rollbackFailures = [];
|
|
703
|
+
for (const item of migrated.reverse()) {
|
|
568
704
|
try {
|
|
569
|
-
|
|
570
|
-
} catch {
|
|
571
|
-
|
|
705
|
+
renameSync(item.backup, item.details.legacy.target);
|
|
706
|
+
} catch (rollbackError) {
|
|
707
|
+
rollbackFailures.push(rollbackError);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
for (const item of switched.reverse()) {
|
|
711
|
+
try {
|
|
712
|
+
rmSync(item.entry.target, { recursive: true, force: true });
|
|
713
|
+
if (item.backup !== null) {
|
|
714
|
+
renameSync(item.backup, item.entry.target);
|
|
715
|
+
}
|
|
716
|
+
} catch (rollbackError) {
|
|
717
|
+
rollbackFailures.push(rollbackError);
|
|
572
718
|
}
|
|
573
719
|
}
|
|
574
|
-
|
|
720
|
+
if (rollbackFailures.length) {
|
|
721
|
+
throw new BootstrapError(
|
|
722
|
+
`Skill suite installation failed and rollback was incomplete: ${error.message}; rollback: ${rollbackFailures.map((failure) => failure.message).join("; ")}`,
|
|
723
|
+
3,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
throw error;
|
|
575
727
|
} finally {
|
|
576
|
-
|
|
577
|
-
|
|
728
|
+
for (const item of staged) {
|
|
729
|
+
rmSync(item.staging, { recursive: true, force: true });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
for (const item of switched) {
|
|
734
|
+
if (item.backup !== null) {
|
|
578
735
|
try {
|
|
579
|
-
|
|
736
|
+
rmSync(item.backup, { recursive: true, force: true });
|
|
580
737
|
} catch {
|
|
581
|
-
|
|
738
|
+
// The verified replacement is live; stale backup cleanup can be retried manually.
|
|
582
739
|
}
|
|
583
740
|
}
|
|
584
741
|
}
|
|
742
|
+
for (const item of migrated) {
|
|
743
|
+
try {
|
|
744
|
+
rmSync(item.backup, { recursive: true, force: true });
|
|
745
|
+
} catch {
|
|
746
|
+
// The legacy entry is no longer discoverable; stale backup cleanup is non-blocking.
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return suites.map(installedSuiteResult);
|
|
585
750
|
}
|
|
586
751
|
|
|
587
752
|
function validatePythonRequirements(file) {
|
|
@@ -628,7 +793,7 @@ async function selectClientTargets(input, output, promptOutput = output) {
|
|
|
628
793
|
promptOutput.write("No supported agent client was detected. Available targets: claude, codex, opencode, all.\n");
|
|
629
794
|
const terminal = createInterface({ input, output: promptOutput });
|
|
630
795
|
try {
|
|
631
|
-
const answer = await terminal.question("Install
|
|
796
|
+
const answer = await terminal.question("Install the msd skill suite for: ");
|
|
632
797
|
return parseTargets(answer);
|
|
633
798
|
} finally {
|
|
634
799
|
terminal.close();
|
|
@@ -653,7 +818,7 @@ async function resolveClientTargets(options, run, platform, selectClients) {
|
|
|
653
818
|
function clientTargetWarnings(clients, layouts) {
|
|
654
819
|
if (clients.includes("opencode") && layouts.length > 1) {
|
|
655
820
|
return [
|
|
656
|
-
"OpenCode can discover
|
|
821
|
+
"OpenCode can discover the msd skill suite from both installed paths; keep both copies managed by this setup to prevent version drift.",
|
|
657
822
|
];
|
|
658
823
|
}
|
|
659
824
|
return [];
|
|
@@ -784,6 +949,176 @@ function environmentHome(environment, platform) {
|
|
|
784
949
|
return environment.HOME || homedir();
|
|
785
950
|
}
|
|
786
951
|
|
|
952
|
+
function pythonRuntimeAt(directory, python, platform) {
|
|
953
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
954
|
+
return {
|
|
955
|
+
baseExecutable: python.executable,
|
|
956
|
+
baseVersion: python.version,
|
|
957
|
+
directory,
|
|
958
|
+
executable: pathApi.join(
|
|
959
|
+
directory,
|
|
960
|
+
platform === "win32" ? "Scripts" : "bin",
|
|
961
|
+
platform === "win32" ? "python.exe" : "python",
|
|
962
|
+
),
|
|
963
|
+
marker: pathApi.join(directory, PYTHON_RUNTIME_MARKER),
|
|
964
|
+
platform,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function expandHomePath(value, home, platform, variable) {
|
|
969
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
970
|
+
let expanded = value;
|
|
971
|
+
if (value === "~") {
|
|
972
|
+
expanded = home;
|
|
973
|
+
} else {
|
|
974
|
+
const homeRelative = platform === "win32" ? /^~[\\/]/ : /^~\//;
|
|
975
|
+
if (homeRelative.test(value)) {
|
|
976
|
+
expanded = pathApi.join(home, value.slice(2));
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (!pathApi.isAbsolute(expanded) && !path.isAbsolute(expanded)) {
|
|
980
|
+
throw new BootstrapError(`${variable} must be an absolute path or start with ~.`, 2);
|
|
981
|
+
}
|
|
982
|
+
return expanded;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export function pythonRuntimeDetails(
|
|
986
|
+
python,
|
|
987
|
+
environment = process.env,
|
|
988
|
+
platform = process.platform,
|
|
989
|
+
) {
|
|
990
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
991
|
+
const home = environmentHome(environment, platform);
|
|
992
|
+
const configured = environment.MSDEVFLOW_PYTHON_DIR;
|
|
993
|
+
const directory = configured
|
|
994
|
+
? expandHomePath(configured, home, platform, "MSDEVFLOW_PYTHON_DIR")
|
|
995
|
+
: platform === "win32"
|
|
996
|
+
? pathApi.join(environment.LOCALAPPDATA || pathApi.join(home, "AppData", "Local"), "msdevflow", "python")
|
|
997
|
+
: pathApi.join(
|
|
998
|
+
environment.XDG_DATA_HOME
|
|
999
|
+
? expandHomePath(environment.XDG_DATA_HOME, home, platform, "XDG_DATA_HOME")
|
|
1000
|
+
: pathApi.join(home, ".local", "share"),
|
|
1001
|
+
"msdevflow",
|
|
1002
|
+
"python",
|
|
1003
|
+
);
|
|
1004
|
+
return pythonRuntimeAt(directory, python, platform);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function validatePythonRuntime(runtime, run) {
|
|
1008
|
+
let marker;
|
|
1009
|
+
try {
|
|
1010
|
+
const metadata = lstatSync(runtime.directory);
|
|
1011
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
1012
|
+
throw new BootstrapError(
|
|
1013
|
+
`Refusing to use a Python runtime path not owned by msdevflow: ${runtime.directory}`,
|
|
1014
|
+
3,
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
marker = readOptionalFile(
|
|
1018
|
+
runtime.marker,
|
|
1019
|
+
`Refusing to use an invalid msdevflow Python runtime marker: ${runtime.marker}`,
|
|
1020
|
+
);
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
if (error?.code === "ENOENT") {
|
|
1023
|
+
return "absent";
|
|
1024
|
+
}
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
if (marker !== PYTHON_RUNTIME_MARKER_CONTENT) {
|
|
1028
|
+
throw new BootstrapError(
|
|
1029
|
+
`Refusing to use a Python runtime directory not owned by msdevflow: ${runtime.directory}`,
|
|
1030
|
+
3,
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
let descriptor;
|
|
1034
|
+
try {
|
|
1035
|
+
descriptor = openSync(runtime.executable, "r");
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
if (error?.code === "ENOENT") {
|
|
1038
|
+
return "repair";
|
|
1039
|
+
}
|
|
1040
|
+
throw error;
|
|
1041
|
+
} finally {
|
|
1042
|
+
if (descriptor !== undefined) {
|
|
1043
|
+
closeSync(descriptor);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (runOptional(run, {
|
|
1047
|
+
command: runtime.executable,
|
|
1048
|
+
args: ["-m", "pip", "--version"],
|
|
1049
|
+
}).status !== 0) {
|
|
1050
|
+
return "repair";
|
|
1051
|
+
}
|
|
1052
|
+
return "current";
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function writePythonRuntimeMarker(runtime) {
|
|
1056
|
+
const existing = readOptionalFile(
|
|
1057
|
+
runtime.marker,
|
|
1058
|
+
`Refusing to replace an invalid msdevflow Python runtime marker: ${runtime.marker}`,
|
|
1059
|
+
);
|
|
1060
|
+
if (existing === PYTHON_RUNTIME_MARKER_CONTENT) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (existing !== null) {
|
|
1064
|
+
throw new BootstrapError(
|
|
1065
|
+
`Refusing to replace an unrelated Python runtime marker: ${runtime.marker}`,
|
|
1066
|
+
3,
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
writeFileSync(runtime.marker, PYTHON_RUNTIME_MARKER_CONTENT, { encoding: "utf8", flag: "wx" });
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
async function ensurePythonRuntime(runtime, status, runLong, run) {
|
|
1073
|
+
if (validatePythonRuntime(runtime, run) !== status) {
|
|
1074
|
+
throw new BootstrapError(
|
|
1075
|
+
`Managed Python runtime changed after confirmation: ${runtime.directory}`,
|
|
1076
|
+
3,
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
if (status === "absent") {
|
|
1080
|
+
mkdirSync(path.dirname(runtime.directory), { recursive: true });
|
|
1081
|
+
try {
|
|
1082
|
+
mkdirSync(runtime.directory);
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
if (error?.code === "EEXIST") {
|
|
1085
|
+
throw new BootstrapError(
|
|
1086
|
+
`Managed Python runtime changed after confirmation: ${runtime.directory}`,
|
|
1087
|
+
3,
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
throw error;
|
|
1091
|
+
}
|
|
1092
|
+
try {
|
|
1093
|
+
writePythonRuntimeMarker(runtime);
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
try {
|
|
1096
|
+
rmdirSync(runtime.directory);
|
|
1097
|
+
} catch {
|
|
1098
|
+
// Preserve the marker failure as the actionable error.
|
|
1099
|
+
}
|
|
1100
|
+
throw error;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (status !== "current") {
|
|
1104
|
+
try {
|
|
1105
|
+
await runLong({
|
|
1106
|
+
command: runtime.baseExecutable,
|
|
1107
|
+
args: ["-m", "venv", runtime.directory],
|
|
1108
|
+
});
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
throw new BootstrapError(
|
|
1111
|
+
`Python venv is unavailable for ${runtime.baseExecutable}. Install the venv component for this Python (for example python3-venv on Debian/Ubuntu) and rerun setup. ${error.message}`,
|
|
1112
|
+
2,
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
verifyFile(
|
|
1117
|
+
runtime.executable,
|
|
1118
|
+
`Managed Python runtime was not created correctly: ${runtime.executable}`,
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
787
1122
|
export function gitcodeInstallDetails(
|
|
788
1123
|
classification,
|
|
789
1124
|
environment = process.env,
|
|
@@ -831,11 +1166,11 @@ export function gitcodeInstallDetails(
|
|
|
831
1166
|
};
|
|
832
1167
|
}
|
|
833
1168
|
|
|
834
|
-
function readOptionalFile(file) {
|
|
1169
|
+
function readOptionalFile(file, invalidMessage = `Refusing to overwrite unrelated wrapper: ${file}`) {
|
|
835
1170
|
try {
|
|
836
1171
|
const metadata = lstatSync(file);
|
|
837
1172
|
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
838
|
-
throw new BootstrapError(
|
|
1173
|
+
throw new BootstrapError(invalidMessage, 3);
|
|
839
1174
|
}
|
|
840
1175
|
return readFileSync(file, "utf8");
|
|
841
1176
|
} catch (error) {
|
|
@@ -951,11 +1286,16 @@ function safeAuthStatus(output) {
|
|
|
951
1286
|
function printPlan(plan, write) {
|
|
952
1287
|
write("Setup plan (no changes made yet):");
|
|
953
1288
|
write(` Client targets: ${plan.clientTargets.clients.join(", ")} (${plan.clientTargets.source})`);
|
|
954
|
-
write(` Bundled skill: ${plan.skillInstalls[0].details.source}`);
|
|
1289
|
+
write(` Bundled skill suite: ${plan.skillInstalls[0].details.source}`);
|
|
955
1290
|
for (const { layout, details } of plan.skillInstalls) {
|
|
956
|
-
write(` Skill
|
|
1291
|
+
write(` Skill root: ${details.target}`);
|
|
957
1292
|
write(` Clients: ${layout.clients.join(", ")}`);
|
|
958
|
-
|
|
1293
|
+
for (const entry of details.entries) {
|
|
1294
|
+
write(` ${entry.name}: ${entry.status}`);
|
|
1295
|
+
}
|
|
1296
|
+
if (details.legacy.status === "migrate") {
|
|
1297
|
+
write(` ${LEGACY_SKILL_NAME}: migrate after suite verification`);
|
|
1298
|
+
}
|
|
959
1299
|
}
|
|
960
1300
|
for (const warning of plan.clientTargets.warnings) {
|
|
961
1301
|
write(` Warning: ${warning}`);
|
|
@@ -975,7 +1315,11 @@ function printPlan(plan, write) {
|
|
|
975
1315
|
write(` Wrapper target: ${plan.gitcodeInstall.cliTarget}`);
|
|
976
1316
|
}
|
|
977
1317
|
write(` npm command: ${formatCommand(plan.installInvocation)}`);
|
|
978
|
-
write(` Python: ${plan.python.executable} (${plan.python.version.join(".")})`);
|
|
1318
|
+
write(` Base Python: ${plan.python.executable} (${plan.python.version.join(".")})`);
|
|
1319
|
+
write(` Managed Python runtime: ${plan.pythonRuntime.directory} (${plan.pythonRuntime.status})`);
|
|
1320
|
+
if (plan.pythonRuntime.status === "absent") {
|
|
1321
|
+
write(` Python venv command: ${formatCommand(plan.venvInvocation)}`);
|
|
1322
|
+
}
|
|
979
1323
|
write(` Python dependency command: ${formatCommand(plan.pipInvocation)}`);
|
|
980
1324
|
write(" Browser binary download: disabled");
|
|
981
1325
|
write(` GitCode authentication command when needed: ${plan.gitcodeInstall.workflowCommand} auth login --web`);
|
|
@@ -1079,7 +1423,8 @@ function textResult(result, write) {
|
|
|
1079
1423
|
write("Setup completed:");
|
|
1080
1424
|
write(` Client targets: ${result.clients.join(", ")}`);
|
|
1081
1425
|
for (const skill of result.skills) {
|
|
1082
|
-
write(` Skill: ${skill.status} at ${skill.target} (${skill.clients.join(", ")})`);
|
|
1426
|
+
write(` Skill suite: ${skill.status} at ${skill.target} (${skill.clients.join(", ")})`);
|
|
1427
|
+
write(` Entries: ${skill.entries.map((entry) => entry.name).join(", ")}`);
|
|
1083
1428
|
}
|
|
1084
1429
|
for (const warning of result.warnings) {
|
|
1085
1430
|
write(` Warning: ${warning}`);
|
|
@@ -1091,8 +1436,9 @@ function textResult(result, write) {
|
|
|
1091
1436
|
if (result.gitcode.authentication.username) {
|
|
1092
1437
|
write(` Username: ${result.gitcode.authentication.username}`);
|
|
1093
1438
|
}
|
|
1094
|
-
write(` Python: ${result.python.
|
|
1095
|
-
write(
|
|
1439
|
+
write(` Base Python: ${result.python.baseExecutable} ${result.python.baseVersion}`);
|
|
1440
|
+
write(` Managed Python runtime: ${result.python.executable}`);
|
|
1441
|
+
write(" Playwright Python package: installed in managed runtime");
|
|
1096
1442
|
write(" Chromium: not downloaded");
|
|
1097
1443
|
}
|
|
1098
1444
|
|
|
@@ -1124,7 +1470,10 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1124
1470
|
options,
|
|
1125
1471
|
targetSelection.clients,
|
|
1126
1472
|
environment,
|
|
1127
|
-
dependencies.
|
|
1473
|
+
dependencies.bundledSkillsDir
|
|
1474
|
+
|| (dependencies.bundledSkillDir
|
|
1475
|
+
? path.dirname(dependencies.bundledSkillDir)
|
|
1476
|
+
: BUNDLED_SKILLS_DIR),
|
|
1128
1477
|
platform,
|
|
1129
1478
|
);
|
|
1130
1479
|
const skillInstalls = layouts.map((layout) => ({
|
|
@@ -1163,9 +1512,27 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1163
1512
|
);
|
|
1164
1513
|
|
|
1165
1514
|
const python = detectPython(run, platform);
|
|
1166
|
-
|
|
1167
|
-
const pipInvocation = {
|
|
1515
|
+
const venvCapability = runOptional(run, {
|
|
1168
1516
|
command: python.executable,
|
|
1517
|
+
args: ["-m", "venv", "--help"],
|
|
1518
|
+
});
|
|
1519
|
+
if (venvCapability.status !== 0) {
|
|
1520
|
+
throw new BootstrapError(
|
|
1521
|
+
`Python venv is unavailable for ${python.executable}. Install the venv component for this Python (for example python3-venv on Debian/Ubuntu) and rerun setup.`,
|
|
1522
|
+
2,
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
const configuredPythonRuntime = pythonRuntimeDetails(python, environment, platform);
|
|
1526
|
+
const pythonRuntime = dependencies.pythonRuntimeDir
|
|
1527
|
+
? pythonRuntimeAt(dependencies.pythonRuntimeDir, python, platform)
|
|
1528
|
+
: configuredPythonRuntime;
|
|
1529
|
+
pythonRuntime.status = validatePythonRuntime(pythonRuntime, run);
|
|
1530
|
+
const venvInvocation = {
|
|
1531
|
+
command: python.executable,
|
|
1532
|
+
args: ["-m", "venv", pythonRuntime.directory],
|
|
1533
|
+
};
|
|
1534
|
+
const pipInvocation = {
|
|
1535
|
+
command: pythonRuntime.executable,
|
|
1169
1536
|
args: ["-m", "pip", "install", "-r", layouts[0].bundledPythonRequirements],
|
|
1170
1537
|
};
|
|
1171
1538
|
const plan = {
|
|
@@ -1176,6 +1543,8 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1176
1543
|
installInvocation,
|
|
1177
1544
|
gitcodeInstall,
|
|
1178
1545
|
python,
|
|
1546
|
+
pythonRuntime,
|
|
1547
|
+
venvInvocation,
|
|
1179
1548
|
pipInvocation,
|
|
1180
1549
|
};
|
|
1181
1550
|
printPlan(plan, writePlan);
|
|
@@ -1194,6 +1563,20 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1194
1563
|
validateWrapper(gitcodeInstall, platform);
|
|
1195
1564
|
}
|
|
1196
1565
|
|
|
1566
|
+
await progress.run(
|
|
1567
|
+
pythonRuntime.status === "absent"
|
|
1568
|
+
? "Creating the managed Python runtime"
|
|
1569
|
+
: pythonRuntime.status === "repair"
|
|
1570
|
+
? "Repairing the managed Python runtime"
|
|
1571
|
+
: "Validating the managed Python runtime",
|
|
1572
|
+
() => ensurePythonRuntime(pythonRuntime, pythonRuntime.status, runLong, run),
|
|
1573
|
+
);
|
|
1574
|
+
await progress.run("Installing reviewed Python dependencies", () => runLong(pipInvocation));
|
|
1575
|
+
await progress.run(
|
|
1576
|
+
"Verifying the Playwright Python package",
|
|
1577
|
+
() => runLong({ command: pythonRuntime.executable, args: ["-c", "import playwright.sync_api"] }),
|
|
1578
|
+
);
|
|
1579
|
+
|
|
1197
1580
|
const installed = await progress.run(
|
|
1198
1581
|
"Installing the official GitCode CLI",
|
|
1199
1582
|
() => installGitcode(plan, runLong, run, platform),
|
|
@@ -1204,12 +1587,6 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1204
1587
|
);
|
|
1205
1588
|
gitcode.command = installed.command;
|
|
1206
1589
|
gitcode.executable = installed.executable;
|
|
1207
|
-
|
|
1208
|
-
await progress.run("Installing reviewed Python dependencies", () => runLong(pipInvocation));
|
|
1209
|
-
await progress.run(
|
|
1210
|
-
"Verifying the Playwright Python package",
|
|
1211
|
-
() => runLong({ command: python.executable, args: ["-c", "import playwright.sync_api"] }),
|
|
1212
|
-
);
|
|
1213
1590
|
gitcode.authentication = await authenticateGitcode(
|
|
1214
1591
|
installed.executable,
|
|
1215
1592
|
gitcode.authentication,
|
|
@@ -1220,14 +1597,17 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1220
1597
|
progress,
|
|
1221
1598
|
options.json,
|
|
1222
1599
|
);
|
|
1223
|
-
const skills = await progress.run("Installing
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1600
|
+
const skills = await progress.run("Installing msd skill suites", async () => {
|
|
1601
|
+
const installedSuites = installBundledSkillSuites(
|
|
1602
|
+
skillInstalls.map(({ details }) => details),
|
|
1603
|
+
);
|
|
1604
|
+
return installedSuites.map((installedSuite, index) => ({
|
|
1605
|
+
kind: skillInstalls[index].layout.kind,
|
|
1606
|
+
clients: skillInstalls[index].layout.clients,
|
|
1607
|
+
target: skillInstalls[index].layout.skillsDir,
|
|
1608
|
+
...installedSuite,
|
|
1609
|
+
}));
|
|
1610
|
+
});
|
|
1231
1611
|
const result = {
|
|
1232
1612
|
state: "ready",
|
|
1233
1613
|
git: gitVersion,
|
|
@@ -1237,8 +1617,10 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1237
1617
|
skills,
|
|
1238
1618
|
gitcode,
|
|
1239
1619
|
python: {
|
|
1240
|
-
|
|
1241
|
-
|
|
1620
|
+
baseExecutable: python.executable,
|
|
1621
|
+
baseVersion: python.version.join("."),
|
|
1622
|
+
runtimeDirectory: pythonRuntime.directory,
|
|
1623
|
+
executable: pythonRuntime.executable,
|
|
1242
1624
|
playwright: "installed",
|
|
1243
1625
|
chromium: "not-downloaded",
|
|
1244
1626
|
},
|