@opengsd/gsd-core 1.6.0-rc.1 → 1.6.0-rc.3
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/.claude-plugin/plugin.json +2 -1
- package/agents/gsd-planner.md +8 -57
- package/agents/gsd-roadmapper.md +6 -0
- package/agents/gsd-security-auditor.md +37 -18
- package/bin/install.js +243 -345
- package/commands/gsd/capture.md +5 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +53 -6
- package/gsd-core/bin/lib/adr-parser.cjs +21 -6
- package/gsd-core/bin/lib/audit-command-router.cjs +52 -14
- package/gsd-core/bin/lib/capability-lifecycle.cjs +30 -7
- package/gsd-core/bin/lib/capability-registry.cjs +96 -83
- package/gsd-core/bin/lib/capability-validator.cjs +22 -0
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +40 -2
- package/gsd-core/bin/lib/command-routing-hub.cjs +10 -3
- package/gsd-core/bin/lib/commands.cjs +247 -0
- package/gsd-core/bin/lib/config-loader.cjs +6 -0
- package/gsd-core/bin/lib/config-schema.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +70 -21
- package/gsd-core/bin/lib/coverage.cjs +464 -0
- package/gsd-core/bin/lib/frontmatter.cjs +7 -3
- package/gsd-core/bin/lib/graphify-command-router.cjs +53 -36
- package/gsd-core/bin/lib/init.cjs +8 -4
- package/gsd-core/bin/lib/install-profiles.cjs +6 -3
- package/gsd-core/bin/lib/intel-command-router.cjs +79 -60
- package/gsd-core/bin/lib/phase-id.cjs +25 -11
- package/gsd-core/bin/lib/phase.cjs +4 -4
- package/gsd-core/bin/lib/planning-workspace.cjs +157 -13
- package/gsd-core/bin/lib/probe-core.cjs +7 -0
- package/gsd-core/bin/lib/profile-output.cjs +18 -6
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
- package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
- package/gsd-core/bin/lib/roadmap-parser.cjs +29 -8
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
- package/gsd-core/bin/lib/roadmap.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +473 -16
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +22 -31
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +15 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +89 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
- package/gsd-core/bin/lib/state.cjs +364 -52
- package/gsd-core/bin/lib/surface.cjs +47 -22
- package/gsd-core/bin/lib/update-context.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +5 -2
- package/gsd-core/bin/lib/verify.cjs +11 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
- package/gsd-core/bin/shared/config-defaults.manifest.json +2 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/context-budget.md +8 -8
- package/gsd-core/references/execute-phase-context-guard.md +16 -0
- package/gsd-core/references/planner-guidance.md +66 -0
- package/gsd-core/references/planning-config.md +3 -2
- package/gsd-core/references/prohibition-probe.md +15 -9
- package/gsd-core/references/security-asvs-levels.md +27 -0
- package/gsd-core/templates/SECURITY.md +6 -4
- package/gsd-core/templates/summary-complex.md +4 -0
- package/gsd-core/templates/summary-minimal.md +3 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +41 -0
- package/gsd-core/workflows/autonomous.md +33 -33
- package/gsd-core/workflows/diagnose-issues.md +6 -1
- package/gsd-core/workflows/execute-phase.md +8 -6
- package/gsd-core/workflows/execute-plan.md +5 -0
- package/gsd-core/workflows/help/modes/full.md +10 -0
- package/gsd-core/workflows/list-seeds.md +63 -0
- package/gsd-core/workflows/manager.md +37 -37
- package/gsd-core/workflows/new-project.md +4 -4
- package/gsd-core/workflows/plan-phase.md +15 -0
- package/gsd-core/workflows/pr-branch.md +156 -0
- package/gsd-core/workflows/quick.md +6 -1
- package/gsd-core/workflows/review.md +10 -2
- package/gsd-core/workflows/secure-phase.md +13 -7
- package/gsd-core/workflows/spec-phase.md +8 -3
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +30 -1
- package/package.json +7 -2
- package/scripts/gen-plugin-skills.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/prompt-injection-scan.sh +5 -0
- package/scripts/release-notes/conventional-title.cjs +88 -0
- package/scripts/release-notes/format-github-release-notes.cjs +4 -3
- package/skills/gsd-add-tests/SKILL.md +38 -0
- package/skills/gsd-ai-integration-phase/SKILL.md +37 -0
- package/skills/gsd-audit-fix/SKILL.md +33 -0
- package/skills/gsd-audit-milestone/SKILL.md +37 -0
- package/skills/gsd-audit-uat/SKILL.md +25 -0
- package/skills/gsd-autonomous/SKILL.md +51 -0
- package/skills/gsd-capture/SKILL.md +67 -0
- package/skills/gsd-cleanup/SKILL.md +24 -0
- package/skills/gsd-code-review/SKILL.md +59 -0
- package/skills/gsd-complete-milestone/SKILL.md +142 -0
- package/skills/gsd-config/SKILL.md +56 -0
- package/skills/gsd-debug/SKILL.md +53 -0
- package/skills/gsd-discuss-phase/SKILL.md +77 -0
- package/skills/gsd-docs-update/SKILL.md +49 -0
- package/skills/gsd-eval-review/SKILL.md +33 -0
- package/skills/gsd-execute-phase/SKILL.md +65 -0
- package/skills/gsd-explore/SKILL.md +28 -0
- package/skills/gsd-extract-learnings/SKILL.md +22 -0
- package/skills/gsd-fast/SKILL.md +31 -0
- package/skills/gsd-forensics/SKILL.md +56 -0
- package/skills/gsd-graphify/SKILL.md +204 -0
- package/skills/gsd-health/SKILL.md +31 -0
- package/skills/gsd-help/SKILL.md +29 -0
- package/skills/gsd-import/SKILL.md +46 -0
- package/skills/gsd-inbox/SKILL.md +39 -0
- package/skills/gsd-ingest-docs/SKILL.md +43 -0
- package/skills/gsd-manager/SKILL.md +45 -0
- package/skills/gsd-map-codebase/SKILL.md +83 -0
- package/skills/gsd-mempalace-capture/SKILL.md +71 -0
- package/skills/gsd-mempalace-recall/SKILL.md +102 -0
- package/skills/gsd-milestone-summary/SKILL.md +51 -0
- package/skills/gsd-mvp-phase/SKILL.md +45 -0
- package/skills/gsd-new-milestone/SKILL.md +45 -0
- package/skills/gsd-new-project/SKILL.md +47 -0
- package/skills/gsd-ns-context/SKILL.md +24 -0
- package/skills/gsd-ns-ideate/SKILL.md +23 -0
- package/skills/gsd-ns-manage/SKILL.md +35 -0
- package/skills/gsd-ns-project/SKILL.md +26 -0
- package/skills/gsd-ns-review/SKILL.md +28 -0
- package/skills/gsd-ns-workflow/SKILL.md +33 -0
- package/skills/gsd-pause-work/SKILL.md +43 -0
- package/skills/gsd-phase/SKILL.md +57 -0
- package/skills/gsd-plan-phase/SKILL.md +63 -0
- package/skills/gsd-plan-review-convergence/SKILL.md +60 -0
- package/skills/gsd-pr-branch/SKILL.md +26 -0
- package/skills/gsd-profile-user/SKILL.md +47 -0
- package/skills/gsd-progress/SKILL.md +49 -0
- package/skills/gsd-quick/SKILL.md +174 -0
- package/skills/gsd-resume-work/SKILL.md +31 -0
- package/skills/gsd-review/SKILL.md +42 -0
- package/skills/gsd-review-backlog/SKILL.md +63 -0
- package/skills/gsd-secure-phase/SKILL.md +36 -0
- package/skills/gsd-settings/SKILL.md +29 -0
- package/skills/gsd-ship/SKILL.md +24 -0
- package/skills/gsd-sketch/SKILL.md +60 -0
- package/skills/gsd-spec-phase/SKILL.md +63 -0
- package/skills/gsd-spike/SKILL.md +57 -0
- package/skills/gsd-stats/SKILL.md +20 -0
- package/skills/gsd-surface/SKILL.md +162 -0
- package/skills/gsd-thread/SKILL.md +24 -0
- package/skills/gsd-ui-phase/SKILL.md +35 -0
- package/skills/gsd-ui-review/SKILL.md +33 -0
- package/skills/gsd-ultraplan-phase/SKILL.md +34 -0
- package/skills/gsd-undo/SKILL.md +35 -0
- package/skills/gsd-update/SKILL.md +50 -0
- package/skills/gsd-validate-phase/SKILL.md +36 -0
- package/skills/gsd-verify-work/SKILL.md +39 -0
- package/skills/gsd-workspace/SKILL.md +53 -0
- package/skills/gsd-workstreams/SKILL.md +70 -0
package/bin/install.js
CHANGED
|
@@ -33,6 +33,11 @@ const {
|
|
|
33
33
|
getGlobalConfigDir,
|
|
34
34
|
getGlobalSkillsBase,
|
|
35
35
|
} = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
|
36
|
+
// getDirName (runtime -> local config dir name) is relocated out of this
|
|
37
|
+
// installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the
|
|
38
|
+
// conversion module's rewrite engine can consume it without importing
|
|
39
|
+
// bin/install.js. Re-exported below for back-compat consumers/tests.
|
|
40
|
+
const { getDirName } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
|
36
41
|
const {
|
|
37
42
|
applyWorktreeBaseRef,
|
|
38
43
|
readBaseRefFromSettings,
|
|
@@ -358,6 +363,10 @@ const {
|
|
|
358
363
|
const {
|
|
359
364
|
resolveRuntimeArtifactLayout,
|
|
360
365
|
} = require(path.join(_gsdLibDir, 'runtime-artifact-layout.cjs'));
|
|
366
|
+
const {
|
|
367
|
+
createRuntimeArtifactInstallPlan,
|
|
368
|
+
createRuntimeArtifactUninstallPlan,
|
|
369
|
+
} = require(path.join(_gsdLibDir, 'runtime-artifact-install-plan.cjs'));
|
|
361
370
|
const {
|
|
362
371
|
planLegacyCleanup,
|
|
363
372
|
applyLegacyCleanup,
|
|
@@ -461,25 +470,8 @@ Then re-run: npx ${pkg.name}@latest
|
|
|
461
470
|
}
|
|
462
471
|
}
|
|
463
472
|
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
if (runtime === 'copilot') return '.github';
|
|
467
|
-
if (runtime === 'opencode') return '.opencode';
|
|
468
|
-
if (runtime === 'gemini') return '.gemini';
|
|
469
|
-
if (runtime === 'kilo') return '.kilo';
|
|
470
|
-
if (runtime === 'codex') return '.codex';
|
|
471
|
-
if (runtime === 'antigravity') return '.agents';
|
|
472
|
-
if (runtime === 'cursor') return '.cursor';
|
|
473
|
-
if (runtime === 'windsurf') return '.devin';
|
|
474
|
-
if (runtime === 'augment') return '.augment';
|
|
475
|
-
if (runtime === 'trae') return '.trae';
|
|
476
|
-
if (runtime === 'qwen') return '.qwen';
|
|
477
|
-
if (runtime === 'hermes') return '.hermes';
|
|
478
|
-
if (runtime === 'kimi') return '.kimi-code';
|
|
479
|
-
if (runtime === 'codebuddy') return '.codebuddy';
|
|
480
|
-
if (runtime === 'cline') return '.cline';
|
|
481
|
-
return '.claude';
|
|
482
|
-
}
|
|
473
|
+
// getDirName (runtime -> local config dir name) now lives in
|
|
474
|
+
// runtime-name-policy.cjs (ADR-1508 / #1510 Phase 1); imported + re-exported.
|
|
483
475
|
|
|
484
476
|
/**
|
|
485
477
|
* Get the config directory path relative to home directory for a runtime
|
|
@@ -605,30 +597,12 @@ if (hasHelp) {
|
|
|
605
597
|
process.exit(0);
|
|
606
598
|
}
|
|
607
599
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
* `@$HOME/...` is treated as a path relative to the config command/ dir, which
|
|
615
|
-
* resolves to `command/$HOME/...` (file not found). For OpenCode we always emit
|
|
616
|
-
* the absolute resolved path. (#2376 Windows, #2831 macOS/Linux.)
|
|
617
|
-
*
|
|
618
|
-
* @param {object} args
|
|
619
|
-
* @param {boolean} args.isGlobal - Global runtime install vs local project
|
|
620
|
-
* @param {boolean} args.isOpencode - Whether the runtime is OpenCode
|
|
621
|
-
* @param {boolean} args.isWindowsHost - process.platform === 'win32'
|
|
622
|
-
* @param {string} args.resolvedTarget - Absolute target dir, forward-slashed
|
|
623
|
-
* @param {string} args.homeDir - User home dir, forward-slashed
|
|
624
|
-
* @returns {string} pathPrefix ending with '/'
|
|
625
|
-
*/
|
|
626
|
-
function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost, resolvedTarget, homeDir }) {
|
|
627
|
-
if (isGlobal && resolvedTarget.startsWith(homeDir) && !isOpencode) {
|
|
628
|
-
return '$HOME' + resolvedTarget.slice(homeDir.length) + '/';
|
|
629
|
-
}
|
|
630
|
-
return `${resolvedTarget}/`;
|
|
631
|
-
}
|
|
600
|
+
// computePathPrefix: implementation moved to runtimeArtifactConversion._computePathPrefix
|
|
601
|
+
// (ADR-1508 / #1511 Phase 2 — single owner). The const binding above (~line 638)
|
|
602
|
+
// re-exports it here for call sites and module.exports.
|
|
603
|
+
// Original doc: Compute the path prefix used for `@file` references in installed
|
|
604
|
+
// command/skill markdown. For global installs under $HOME uses $HOME/... form;
|
|
605
|
+
// OpenCode always uses the absolute path (#2376 Windows, #2831 macOS/Linux).
|
|
632
606
|
|
|
633
607
|
// normalizeNodePath, resolveNodeRunner, resolveBashRunner, referencesHook are
|
|
634
608
|
// now owned by the runtime-hooks-surface module. Import them here so
|
|
@@ -643,6 +617,19 @@ const referencesHook = hooksSurface.referencesHook;
|
|
|
643
617
|
// applySettingsJsonHooks: mutates settings.hooks.* in place with all GSD-managed
|
|
644
618
|
// hook registrations for settings.json-surface runtimes (ADR-857 phase 5f-1b).
|
|
645
619
|
const applySettingsJsonHooks = hooksSurface.applySettingsJsonHooks;
|
|
620
|
+
// processAttribution: pure Co-Authored-By content transform, relocated to the
|
|
621
|
+
// conversion module (ADR-1508 / #1510 Phase 1). Bound here so install.js
|
|
622
|
+
// callers continue to work and there is a single implementation. (All call
|
|
623
|
+
// sites are below this line, so the const binding has no TDZ hazard.)
|
|
624
|
+
const processAttribution = runtimeArtifactConversion.processAttribution;
|
|
625
|
+
// computePathPrefix / applyRuntimeContentRewritesInPlace / applyRuntimeContentRewritesForCommandsInPlace:
|
|
626
|
+
// Single implementations now live in runtimeArtifactConversion (ADR-1508 / #1511 Phase 2).
|
|
627
|
+
// Re-bound here so install.js call sites and exports continue to work unchanged.
|
|
628
|
+
// Local bodies replaced by breadcrumb comments at their original locations.
|
|
629
|
+
// All call sites are below this line → no TDZ hazard.
|
|
630
|
+
const computePathPrefix = runtimeArtifactConversion._computePathPrefix;
|
|
631
|
+
const applyRuntimeContentRewritesInPlace = runtimeArtifactConversion.applyRuntimeContentRewritesInPlace;
|
|
632
|
+
const applyRuntimeContentRewritesForCommandsInPlace = runtimeArtifactConversion.applyRuntimeContentRewritesForCommandsInPlace;
|
|
646
633
|
|
|
647
634
|
function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
|
|
648
635
|
return hooksSurface.rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts);
|
|
@@ -1417,24 +1404,9 @@ function getCommitAttribution(runtime) {
|
|
|
1417
1404
|
return result;
|
|
1418
1405
|
}
|
|
1419
1406
|
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
* @param {null|undefined|string} attribution - null=remove, undefined=keep, string=replace
|
|
1424
|
-
* @returns {string} Processed content
|
|
1425
|
-
*/
|
|
1426
|
-
function processAttribution(content, attribution) {
|
|
1427
|
-
if (attribution === null) {
|
|
1428
|
-
// Remove Co-Authored-By lines and the preceding blank line
|
|
1429
|
-
return content.replace(/(\r?\n){2}Co-Authored-By:.*$/gim, '');
|
|
1430
|
-
}
|
|
1431
|
-
if (attribution === undefined) {
|
|
1432
|
-
return content;
|
|
1433
|
-
}
|
|
1434
|
-
// Replace with custom attribution (escape $ to prevent backreference injection)
|
|
1435
|
-
const safeAttribution = attribution.replace(/\$/g, '$$$$');
|
|
1436
|
-
return content.replace(/Co-Authored-By:.*$/gim, `Co-Authored-By: ${safeAttribution}`);
|
|
1437
|
-
}
|
|
1407
|
+
// processAttribution (pure Co-Authored-By content transform) relocated to
|
|
1408
|
+
// runtime-artifact-conversion.cjs (ADR-1508 / #1510 Phase 1); bound above.
|
|
1409
|
+
// getCommitAttribution stays here — it is impure install-time config I/O.
|
|
1438
1410
|
|
|
1439
1411
|
/**
|
|
1440
1412
|
* Convert Claude Code frontmatter to opencode format
|
|
@@ -1545,11 +1517,17 @@ function convertGeminiToolName(claudeTool) {
|
|
|
1545
1517
|
// Task/Agent: exclude — agents are auto-registered as callable tools.
|
|
1546
1518
|
// AskUserQuestion: exclude — Gemini CLI does not expose an ask_user tool;
|
|
1547
1519
|
// emitting it causes frontmatter validation errors (#3362).
|
|
1520
|
+
// Skill/SlashCommand: exclude — Gemini CLI has no 'skill' built-in tool;
|
|
1521
|
+
// the lowercase fallback would emit an invalid 'skill'/'slashcommand' name
|
|
1522
|
+
// that fails frontmatter validation (tools.N: Invalid tool name) and aborts
|
|
1523
|
+
// the entire agent load (#1394).
|
|
1548
1524
|
if (
|
|
1549
1525
|
claudeTool === 'Task' ||
|
|
1550
1526
|
claudeTool === 'Agent' ||
|
|
1551
1527
|
claudeTool === 'AskUserQuestion' ||
|
|
1552
|
-
claudeTool === 'ask_user'
|
|
1528
|
+
claudeTool === 'ask_user' ||
|
|
1529
|
+
claudeTool === 'Skill' ||
|
|
1530
|
+
claudeTool === 'SlashCommand'
|
|
1553
1531
|
) {
|
|
1554
1532
|
return null;
|
|
1555
1533
|
}
|
|
@@ -2479,20 +2457,18 @@ function convertClaudeToWindsurfMarkdown(content) {
|
|
|
2479
2457
|
// Replace subagent_type from Claude to Windsurf format
|
|
2480
2458
|
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
|
2481
2459
|
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
|
2482
|
-
// Replace project-level Claude conventions with Windsurf
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
converted = converted.replace(
|
|
2486
|
-
converted = converted.replace(
|
|
2487
|
-
converted = converted.replace(
|
|
2488
|
-
converted = converted.replace(
|
|
2489
|
-
converted = converted.replace(/\.claude
|
|
2490
|
-
converted = converted.replace(/\.\/\.claude\//g, './.devin/');
|
|
2491
|
-
converted = converted.replace(/\.claude\//g, '.devin/');
|
|
2460
|
+
// Replace project-level Claude conventions with Windsurf equivalents.
|
|
2461
|
+
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
|
|
2462
|
+
converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
|
|
2463
|
+
converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
|
|
2464
|
+
converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
|
|
2465
|
+
converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
|
|
2466
|
+
converted = converted.replace(/\.\/\.claude\//g, './.windsurf/');
|
|
2467
|
+
converted = converted.replace(/\.claude\//g, '.windsurf/');
|
|
2492
2468
|
// Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
|
|
2493
2469
|
// Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
|
|
2494
|
-
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.
|
|
2495
|
-
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.
|
|
2470
|
+
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf');
|
|
2471
|
+
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf');
|
|
2496
2472
|
// Environment variable name rewrite
|
|
2497
2473
|
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
|
|
2498
2474
|
// Remove Claude Code-specific bug workarounds before brand replacement
|
|
@@ -2546,6 +2522,33 @@ function convertClaudeCommandToWindsurfSkill(content, skillName) {
|
|
|
2546
2522
|
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
2547
2523
|
}
|
|
2548
2524
|
|
|
2525
|
+
function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
|
|
2526
|
+
// #1615 security: commandName flows unsanitized into a markdown body that
|
|
2527
|
+
// Windsurf loads as an LLM-readable workflow. Validate at entry to prevent
|
|
2528
|
+
// (a) prompt injection via newlines / markdown structure in the filename,
|
|
2529
|
+
// (b) path-component injection via .., /, \ in stem → @-reference target.
|
|
2530
|
+
// Pattern: optional gsd- prefix + lowercase alphanumeric + dashes; rejects
|
|
2531
|
+
// everything else. See DEFECT.PROMPT-INJECTION-SCAN-COLLISION and the
|
|
2532
|
+
// PR #1622 security review.
|
|
2533
|
+
if (typeof commandName !== 'string' || !/^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(commandName)) {
|
|
2534
|
+
const preview = typeof commandName === 'string' ? JSON.stringify(commandName.slice(0, 60)) : String(commandName);
|
|
2535
|
+
throw new Error(
|
|
2536
|
+
`convertClaudeCommandToWindsurfWorkflow: rejected commandName ${preview}; ` +
|
|
2537
|
+
'must match /^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ (no slashes, backslashes, spaces, dots, trailing dash, or control chars — prevents prompt injection and path-component injection into the workflow body)'
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
const converted = convertClaudeToWindsurfMarkdown(content);
|
|
2541
|
+
const { frontmatter } = extractFrontmatterAndBody(converted);
|
|
2542
|
+
const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
|
|
2543
|
+
const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName;
|
|
2544
|
+
const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
|
|
2545
|
+
const byteLength = Buffer.byteLength(workflow, 'utf8');
|
|
2546
|
+
if (byteLength > 12000) {
|
|
2547
|
+
throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`);
|
|
2548
|
+
}
|
|
2549
|
+
return workflow;
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2549
2552
|
/**
|
|
2550
2553
|
* Convert Claude Code agent markdown to Windsurf agent format.
|
|
2551
2554
|
* Strips frontmatter fields Windsurf doesn't support (color, skills),
|
|
@@ -3251,6 +3254,66 @@ function cleanupCodexSkillMetadataSidecars(skillsDir) {
|
|
|
3251
3254
|
}
|
|
3252
3255
|
}
|
|
3253
3256
|
|
|
3257
|
+
/**
|
|
3258
|
+
* Remove legacy Windsurf skill artifacts from .devin/skills/gsd- directories.
|
|
3259
|
+
*
|
|
3260
|
+
* Pre-#1615 Windsurf installs wrote skills under .devin/ (Devin Desktop
|
|
3261
|
+
* preferred dir, #1085). #1615 moved Windsurf to .windsurf/workflows/.
|
|
3262
|
+
* Old .devin/skills/gsd- dirs linger on disk indefinitely and confuse
|
|
3263
|
+
* users who see two GSD trees.
|
|
3264
|
+
*
|
|
3265
|
+
* Preserves user-owned content:
|
|
3266
|
+
* - non-gsd-* dirs under .devin/skills/ (user-authored skills)
|
|
3267
|
+
* - gsd-dev-preferences/ (user-owned per #2973)
|
|
3268
|
+
* - any files (not dirs) under .devin/skills/
|
|
3269
|
+
*
|
|
3270
|
+
* @param {string} workspaceDir - workspace root (process.cwd() for local installs)
|
|
3271
|
+
* @returns {number} count of removed legacy gsd-* skill directories
|
|
3272
|
+
*/
|
|
3273
|
+
function cleanupWindsurfLegacyDevinSkills(workspaceDir) {
|
|
3274
|
+
const legacySkillsDir = path.join(workspaceDir, '.devin', 'skills');
|
|
3275
|
+
if (!fs.existsSync(legacySkillsDir)) return 0;
|
|
3276
|
+
|
|
3277
|
+
// Mirror the user-owned list from cleanupCodexSkillMetadataSidecars (#2973).
|
|
3278
|
+
const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']);
|
|
3279
|
+
let removed = 0;
|
|
3280
|
+
|
|
3281
|
+
for (const entry of fs.readdirSync(legacySkillsDir, { withFileTypes: true })) {
|
|
3282
|
+
if (!entry.isDirectory() || !entry.name.startsWith('gsd-')) continue;
|
|
3283
|
+
if (_userOwnedSkillDirs.has(entry.name)) continue;
|
|
3284
|
+
|
|
3285
|
+
const dirToRemove = path.join(legacySkillsDir, entry.name);
|
|
3286
|
+
try {
|
|
3287
|
+
// Symlink guard: if the gsd-* dir is itself a symlink pointing outside
|
|
3288
|
+
// the .devin tree, deleting through it could escape the tree. Skip.
|
|
3289
|
+
const stat = fs.lstatSync(dirToRemove);
|
|
3290
|
+
if (stat.isSymbolicLink()) continue;
|
|
3291
|
+
|
|
3292
|
+
fs.rmSync(dirToRemove, { recursive: true, force: true });
|
|
3293
|
+
removed++;
|
|
3294
|
+
} catch (_err) {
|
|
3295
|
+
// Fail open — a single bad dir must not block the install.
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
// If .devin/skills/ is now empty, prune it. If .devin/ itself is then empty,
|
|
3300
|
+
// prune that too — leaves the workspace clean for the new .windsurf/ layout.
|
|
3301
|
+
// Never remove non-empty containers (user may have other Devin content).
|
|
3302
|
+
try {
|
|
3303
|
+
if (fs.existsSync(legacySkillsDir) && fs.readdirSync(legacySkillsDir).length === 0) {
|
|
3304
|
+
fs.rmdirSync(legacySkillsDir);
|
|
3305
|
+
const devinDir = path.join(workspaceDir, '.devin');
|
|
3306
|
+
if (fs.existsSync(devinDir) && fs.readdirSync(devinDir).length === 0) {
|
|
3307
|
+
fs.rmdirSync(devinDir);
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
} catch (_err) {
|
|
3311
|
+
// best-effort container cleanup
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
return removed;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3254
3317
|
/**
|
|
3255
3318
|
* Generate the GSD config block for Codex config.toml.
|
|
3256
3319
|
* @param {Array<{name: string, description: string}>} agents
|
|
@@ -6689,24 +6752,10 @@ function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = '
|
|
|
6689
6752
|
* @param {string} pathPrefix e.g. "~/.codex/" — trailing-slash string
|
|
6690
6753
|
* @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
|
|
6691
6754
|
*/
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
const walkAndRewrite = (dir) => {
|
|
6697
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
6698
|
-
const fullPath = path.join(dir, entry.name);
|
|
6699
|
-
if (entry.isDirectory()) {
|
|
6700
|
-
walkAndRewrite(fullPath);
|
|
6701
|
-
} else if (entry.name.endsWith('.md')) {
|
|
6702
|
-
let content = fs.readFileSync(fullPath, 'utf8');
|
|
6703
|
-
content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal);
|
|
6704
|
-
fs.writeFileSync(fullPath, content);
|
|
6705
|
-
}
|
|
6706
|
-
}
|
|
6707
|
-
};
|
|
6708
|
-
walkAndRewrite(stagedDir);
|
|
6709
|
-
}
|
|
6755
|
+
// applyRuntimeContentRewritesInPlace: walk loop is now owned by
|
|
6756
|
+
// runtimeArtifactConversion.applyRuntimeContentRewritesInPlace (ADR-1508 / #1511 Phase 2).
|
|
6757
|
+
// The const binding above (~line 629) delegates here. Call sites in installRuntimeArtifacts
|
|
6758
|
+
// pass attribution as the 5th arg (getCommitAttribution(runtime)) per the new contract.
|
|
6710
6759
|
|
|
6711
6760
|
/**
|
|
6712
6761
|
* Apply per-runtime content rewrites to flat .md files in a staged commands dir.
|
|
@@ -6724,30 +6773,10 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGl
|
|
|
6724
6773
|
* @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
|
|
6725
6774
|
* @returns {string} path to a temp dir with rewritten files (caller is responsible for cleanup)
|
|
6726
6775
|
*/
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
// package source. A temp copy is unconditional to keep the code simple and safe.
|
|
6732
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cmd-rewrites-'));
|
|
6733
|
-
try {
|
|
6734
|
-
for (const entry of fs.readdirSync(stagedDir, { withFileTypes: true })) {
|
|
6735
|
-
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
6736
|
-
let content = fs.readFileSync(path.join(stagedDir, entry.name), 'utf8');
|
|
6737
|
-
content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal);
|
|
6738
|
-
// For augment commands, apply the markdown conversion so tool references
|
|
6739
|
-
// and skill paths use Augment equivalents.
|
|
6740
|
-
if (runtime === 'augment') {
|
|
6741
|
-
content = convertClaudeToAugmentMarkdown(content);
|
|
6742
|
-
}
|
|
6743
|
-
fs.writeFileSync(path.join(tempDir, entry.name), content);
|
|
6744
|
-
}
|
|
6745
|
-
} catch (err) {
|
|
6746
|
-
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
6747
|
-
throw err;
|
|
6748
|
-
}
|
|
6749
|
-
return tempDir;
|
|
6750
|
-
}
|
|
6776
|
+
// applyRuntimeContentRewritesForCommandsInPlace: copy+rewrite loop is now owned by
|
|
6777
|
+
// runtimeArtifactConversion.applyRuntimeContentRewritesForCommandsInPlace (ADR-1508 / #1511 Phase 2).
|
|
6778
|
+
// The const binding above (~line 630) delegates here. Call sites in installRuntimeArtifacts
|
|
6779
|
+
// pass attribution as the 5th arg (getCommitAttribution(runtime)) per the new contract.
|
|
6751
6780
|
|
|
6752
6781
|
/**
|
|
6753
6782
|
* Apply the per-runtime rewrite table to a single content string.
|
|
@@ -6759,196 +6788,12 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
|
|
|
6759
6788
|
* @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
|
|
6760
6789
|
* @returns {string}
|
|
6761
6790
|
*/
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6769
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6770
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6771
|
-
content = content.replace(/~\/\.codex\//g, pathPrefix);
|
|
6772
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6773
|
-
break;
|
|
6774
|
-
|
|
6775
|
-
case 'cline':
|
|
6776
|
-
// Slash forms: both the original ~/.claude/ (safety net) and the stage-time
|
|
6777
|
-
// converted ~/.cline/ (from convertClaudeToCliineMarkdown) → pathPrefix
|
|
6778
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6779
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6780
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6781
|
-
content = content.replace(/~\/\.cline\//g, pathPrefix);
|
|
6782
|
-
content = content.replace(/\$HOME\/\.cline\//g, pathPrefix);
|
|
6783
|
-
// Bare forms (no trailing slash)
|
|
6784
|
-
content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
|
|
6785
|
-
content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
|
|
6786
|
-
content = content.replace(/~\/\.cline\b/g, normalizedPathPrefix);
|
|
6787
|
-
content = content.replace(/\$HOME\/\.cline\b/g, normalizedPathPrefix);
|
|
6788
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6789
|
-
break;
|
|
6790
|
-
|
|
6791
|
-
case 'cursor':
|
|
6792
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6793
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6794
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6795
|
-
// Bare forms (no trailing slash) — use (?![\w-]) instead of \b so that
|
|
6796
|
-
// .claude-plugin / .claudeignore are NOT corrupted (the \b word-boundary
|
|
6797
|
-
// fires between 'e' and '-', which rewrites .claude-plugin → .cursor-plugin).
|
|
6798
|
-
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6799
|
-
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6800
|
-
content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
|
|
6801
|
-
content = content.replace(/~\/\.cursor\//g, pathPrefix);
|
|
6802
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6803
|
-
break;
|
|
6804
|
-
|
|
6805
|
-
case 'windsurf': {
|
|
6806
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6807
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6808
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6809
|
-
// Bare forms (no trailing slash) — use (?![\w-]) instead of \b so that
|
|
6810
|
-
// .claude-plugin / .claudeignore are NOT corrupted (the \b word-boundary
|
|
6811
|
-
// fires between 'e' and '-', which rewrites .claude-plugin → .devin-plugin).
|
|
6812
|
-
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6813
|
-
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6814
|
-
content = content.replace(/~\/\.codeium\/windsurf\//g, pathPrefix);
|
|
6815
|
-
// Stage-1 converter rewrites .claude/skills/ → .devin/skills/ (workspace-relative
|
|
6816
|
-
// form). For global installs the real path is pathPrefix + skills/, so fix that up
|
|
6817
|
-
// here using the real isGlobal flag (threaded from installRuntimeArtifacts scope,
|
|
6818
|
-
// not derived from pathPrefix substring which misclassifies custom config dirs).
|
|
6819
|
-
// For local installs, the relative .devin/ form is correct — leave it. (#1085)
|
|
6820
|
-
if (isGlobal) {
|
|
6821
|
-
content = content.replace(/\.devin\/skills\//g, `${pathPrefix}skills/`);
|
|
6822
|
-
content = content.replace(/\.\/\.devin\//g, pathPrefix);
|
|
6823
|
-
content = content.replace(/~\/\.devin(?![\w-])/g, normalizedPathPrefix);
|
|
6824
|
-
content = content.replace(/\$HOME\/\.devin(?![\w-])/g, normalizedPathPrefix);
|
|
6825
|
-
}
|
|
6826
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6827
|
-
break;
|
|
6828
|
-
}
|
|
6829
|
-
|
|
6830
|
-
case 'augment':
|
|
6831
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6832
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6833
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6834
|
-
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6835
|
-
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6836
|
-
content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
|
|
6837
|
-
content = content.replace(/~\/\.augment\//g, pathPrefix);
|
|
6838
|
-
content = content.replace(/\$HOME\/\.augment\//g, pathPrefix);
|
|
6839
|
-
content = content.replace(/~\/\.augment(?![\w-])/g, normalizedPathPrefix);
|
|
6840
|
-
content = content.replace(/\$HOME\/\.augment(?![\w-])/g, normalizedPathPrefix);
|
|
6841
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6842
|
-
break;
|
|
6843
|
-
|
|
6844
|
-
case 'trae':
|
|
6845
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6846
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6847
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6848
|
-
content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
|
|
6849
|
-
content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
|
|
6850
|
-
content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
|
|
6851
|
-
content = content.replace(/~\/\.trae\//g, pathPrefix);
|
|
6852
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6853
|
-
break;
|
|
6854
|
-
|
|
6855
|
-
case 'codebuddy':
|
|
6856
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6857
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6858
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6859
|
-
content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
|
|
6860
|
-
content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
|
|
6861
|
-
content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
|
|
6862
|
-
// The codebuddy converter rewrites `.claude/` → `.codebuddy/` at stage
|
|
6863
|
-
// time, so `$HOME/.claude/...` arrives here as `$HOME/.codebuddy/...`.
|
|
6864
|
-
// Normalize BOTH the `~/` and `$HOME/` forms (slash + bare) to the install
|
|
6865
|
-
// target so `--config-dir`/local installs don't leak the default home.
|
|
6866
|
-
content = content.replace(/~\/\.codebuddy\//g, pathPrefix);
|
|
6867
|
-
content = content.replace(/\$HOME\/\.codebuddy\//g, pathPrefix);
|
|
6868
|
-
content = content.replace(/~\/\.codebuddy\b/g, normalizedPathPrefix);
|
|
6869
|
-
content = content.replace(/\$HOME\/\.codebuddy\b/g, normalizedPathPrefix);
|
|
6870
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6871
|
-
break;
|
|
6872
|
-
|
|
6873
|
-
case 'copilot':
|
|
6874
|
-
// Copilot converter handles path rewrites; only attribution here
|
|
6875
|
-
content = processAttribution(content, getCommitAttribution('copilot'));
|
|
6876
|
-
break;
|
|
6877
|
-
|
|
6878
|
-
case 'antigravity':
|
|
6879
|
-
// Antigravity converter handles path rewrites; only attribution here
|
|
6880
|
-
content = processAttribution(content, getCommitAttribution('antigravity'));
|
|
6881
|
-
break;
|
|
6882
|
-
|
|
6883
|
-
case 'claude':
|
|
6884
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6885
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6886
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6887
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6888
|
-
break;
|
|
6889
|
-
|
|
6890
|
-
case 'qwen':
|
|
6891
|
-
// Branding rewrites run before path rewrites to avoid consuming
|
|
6892
|
-
// patterns that the path step would also match.
|
|
6893
|
-
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
|
|
6894
|
-
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
|
|
6895
|
-
// Base path rewrites (use ~/ and $HOME/ slash forms first — most specific)
|
|
6896
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6897
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6898
|
-
content = content.replace(/~\/\.qwen\//g, pathPrefix);
|
|
6899
|
-
content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
|
|
6900
|
-
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6901
|
-
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6902
|
-
content = content.replace(/~\/\.qwen(?![\w-])/g, normalizedPathPrefix);
|
|
6903
|
-
content = content.replace(/\$HOME\/\.qwen(?![\w-])/g, normalizedPathPrefix);
|
|
6904
|
-
// Bare relative .claude/ → .qwen/ (residual refs not matched above)
|
|
6905
|
-
content = content.replace(/\.claude\//g, '.qwen/');
|
|
6906
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6907
|
-
content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
|
|
6908
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6909
|
-
break;
|
|
6910
|
-
|
|
6911
|
-
case 'hermes':
|
|
6912
|
-
// Branding rewrites run before path rewrites (same rationale as qwen)
|
|
6913
|
-
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
|
|
6914
|
-
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
|
6915
|
-
// Base path rewrites
|
|
6916
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6917
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6918
|
-
content = content.replace(/~\/\.hermes\//g, pathPrefix);
|
|
6919
|
-
content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
|
|
6920
|
-
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6921
|
-
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6922
|
-
content = content.replace(/~\/\.hermes(?![\w-])/g, normalizedPathPrefix);
|
|
6923
|
-
content = content.replace(/\$HOME\/\.hermes(?![\w-])/g, normalizedPathPrefix);
|
|
6924
|
-
// Bare relative .claude/ → .hermes/ (residual refs)
|
|
6925
|
-
content = content.replace(/\.claude\//g, '.hermes/');
|
|
6926
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6927
|
-
content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
|
|
6928
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6929
|
-
break;
|
|
6930
|
-
|
|
6931
|
-
case 'kimi':
|
|
6932
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6933
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6934
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6935
|
-
content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
|
|
6936
|
-
content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
|
|
6937
|
-
content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
|
|
6938
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6939
|
-
break;
|
|
6940
|
-
|
|
6941
|
-
default:
|
|
6942
|
-
// Unknown runtime — no rewrites.
|
|
6943
|
-
// OpenCode/Kilo are intentionally absent: their skills are written by
|
|
6944
|
-
// installOpencodeFamilySkills, which applies pathPrefix BEFORE the
|
|
6945
|
-
// command→skill conversion (mirroring copyFlattenedCommands) rather than
|
|
6946
|
-
// rewriting already-converted SKILL.md bodies. See #784.
|
|
6947
|
-
break;
|
|
6948
|
-
}
|
|
6949
|
-
|
|
6950
|
-
return content;
|
|
6951
|
-
}
|
|
6791
|
+
// _applyRuntimeRewrites: single implementation lives in runtimeArtifactConversion
|
|
6792
|
+
// (ADR-1508 / #1511 Phase 2). Bound here so install.js call sites and exports are
|
|
6793
|
+
// reference-identical to the conversion module (consistent with the walkers above).
|
|
6794
|
+
// All call sites are below this line → no TDZ hazard.
|
|
6795
|
+
const _applyRuntimeRewrites = runtimeArtifactConversion._applyRuntimeRewrites;
|
|
6796
|
+
const _stampNonClaudeRuntimeDefaults = runtimeArtifactConversion._stampNonClaudeRuntimeDefaults;
|
|
6952
6797
|
|
|
6953
6798
|
/**
|
|
6954
6799
|
* Copy a staged directory's contents into destDir.
|
|
@@ -7258,36 +7103,25 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
|
|
|
7258
7103
|
_runLegacyInstallMigrations(runtime, configDir, scope);
|
|
7259
7104
|
|
|
7260
7105
|
const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope);
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
isGlobal: scope === 'global',
|
|
7268
|
-
isOpencode: runtime === 'opencode',
|
|
7269
|
-
isWindowsHost: process.platform === 'win32',
|
|
7270
|
-
resolvedTarget: _resolvedTarget,
|
|
7271
|
-
homeDir: _homeDir,
|
|
7106
|
+
const planResult = createRuntimeArtifactInstallPlan({
|
|
7107
|
+
layout,
|
|
7108
|
+
resolvedProfile,
|
|
7109
|
+
homedir: () => os.homedir(),
|
|
7110
|
+
platform: process.platform,
|
|
7111
|
+
resolveAttribution: getCommitAttribution,
|
|
7272
7112
|
});
|
|
7273
7113
|
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
}
|
|
7286
|
-
// applyRuntimeContentRewritesForCommandsInPlace() returns a fresh mkdtemp dir under
|
|
7287
|
-
// os.tmpdir() (gsd-cmd-rewrites-*); remove it once copied so it does not accumulate (#856).
|
|
7288
|
-
const tempToClean = stagedForCopy !== staged ? stagedForCopy : null;
|
|
7289
|
-
try {
|
|
7290
|
-
const dest = path.join(layout.configDir, kind.destSubpath);
|
|
7114
|
+
const cleanupDirs = planResult.ok ? planResult.plan.cleanupDirs : planResult.cleanupDirs;
|
|
7115
|
+
try {
|
|
7116
|
+
if (!planResult.ok) {
|
|
7117
|
+
throw new Error(planResult.message);
|
|
7118
|
+
}
|
|
7119
|
+
|
|
7120
|
+
const kindsByName = new Map(layout.kinds.map((kind) => [kind.kind, kind]));
|
|
7121
|
+
for (const item of planResult.plan.items) {
|
|
7122
|
+
const kind = kindsByName.get(item.kind);
|
|
7123
|
+
if (!kind) throw new Error(`Install plan returned unknown artifact kind: ${item.kind}`);
|
|
7124
|
+
const dest = item.destDir;
|
|
7291
7125
|
fs.mkdirSync(dest, { recursive: true });
|
|
7292
7126
|
if (kind.kind === 'skills' && fs.existsSync(dest)) {
|
|
7293
7127
|
// Pre-prune: snapshot user-owned content before _removeGsdEntries wipes it,
|
|
@@ -7314,7 +7148,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
|
|
|
7314
7148
|
}
|
|
7315
7149
|
|
|
7316
7150
|
_removeGsdEntries(dest, kind);
|
|
7317
|
-
_copyStaged(
|
|
7151
|
+
_copyStaged(item.sourceDir, dest, kind);
|
|
7318
7152
|
|
|
7319
7153
|
// Restore user-owned dirs after the prune+copy
|
|
7320
7154
|
for (const [dirName, snap] of toPreserve) {
|
|
@@ -7324,13 +7158,13 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
|
|
|
7324
7158
|
// For non-skills kinds (commands, agents): no user content to preserve;
|
|
7325
7159
|
// just prune stale gsd-* entries and copy new ones.
|
|
7326
7160
|
_removeGsdEntries(dest, kind);
|
|
7327
|
-
_copyStaged(
|
|
7328
|
-
}
|
|
7329
|
-
} finally {
|
|
7330
|
-
if (tempToClean) {
|
|
7331
|
-
try { fs.rmSync(tempToClean, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
7161
|
+
_copyStaged(item.sourceDir, dest, kind);
|
|
7332
7162
|
}
|
|
7333
7163
|
}
|
|
7164
|
+
} finally {
|
|
7165
|
+
for (const dir of cleanupDirs) {
|
|
7166
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
7167
|
+
}
|
|
7334
7168
|
}
|
|
7335
7169
|
|
|
7336
7170
|
// Hermes: after the install loop has written all gsd-<stem>/ dirs to
|
|
@@ -7447,9 +7281,14 @@ function uninstallRuntimeArtifacts(runtime, configDir, scope) {
|
|
|
7447
7281
|
const savedLegacyArtifacts = _runLegacyUninstallCleanup(runtime, configDir, scope);
|
|
7448
7282
|
|
|
7449
7283
|
const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope);
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7284
|
+
const plan = createRuntimeArtifactUninstallPlan(layout);
|
|
7285
|
+
const kindsByName = new Map(layout.kinds.map((kind) => [kind.kind, kind]));
|
|
7286
|
+
for (const item of plan.items) {
|
|
7287
|
+
const kind = kindsByName.get(item.kind);
|
|
7288
|
+
if (!kind) {
|
|
7289
|
+
throw new Error(`Runtime artifact uninstall plan referenced unknown kind: ${item.kind}`);
|
|
7290
|
+
}
|
|
7291
|
+
_removeGsdEntries(item.destDir, kind);
|
|
7453
7292
|
}
|
|
7454
7293
|
|
|
7455
7294
|
// Hermes: after removing gsd-* skill dirs from skills/gsd/, also remove
|
|
@@ -7540,6 +7379,15 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7540
7379
|
}
|
|
7541
7380
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
7542
7381
|
|
|
7382
|
+
// #1521: stamp the workflow runtime-resolution block so every non-Claude
|
|
7383
|
+
// install resolves its own runtime identity and defaults use_worktrees=false.
|
|
7384
|
+
// copyWithPathReplacement is the emit path for gsd-core/workflows/*.md;
|
|
7385
|
+
// _applyRuntimeRewrites is NOT invoked here, so this is what makes the fix
|
|
7386
|
+
// live in real installs (it is a no-op for files without those lines).
|
|
7387
|
+
if (runtime !== 'claude') {
|
|
7388
|
+
content = _stampNonClaudeRuntimeDefaults(content, runtime);
|
|
7389
|
+
}
|
|
7390
|
+
|
|
7543
7391
|
// #3683 — normalize /gsd:<cmd> → /gsd-<cmd> in any body passing through
|
|
7544
7392
|
// copyWithPathReplacement for runtimes that register commands under the
|
|
7545
7393
|
// hyphen form; normalizeAgentBodyForRuntime self-gates on
|
|
@@ -9835,6 +9683,17 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9835
9683
|
cleanupCodexSkillMetadataSidecars(path.join(targetDir, 'skills'));
|
|
9836
9684
|
}
|
|
9837
9685
|
|
|
9686
|
+
// #1629 Finding B: Windsurf local only — remove legacy .devin/skills/gsd-*
|
|
9687
|
+
// dirs from pre-#1615 installs. #1615 moved Windsurf to .windsurf/workflows/
|
|
9688
|
+
// but never cleaned up the old .devin/skills/ layout (#1085). User-owned
|
|
9689
|
+
// content is preserved (non-gsd- dirs, gsd-dev-preferences, symlinks).
|
|
9690
|
+
if (isWindsurf && !isGlobal) {
|
|
9691
|
+
const removedCount = cleanupWindsurfLegacyDevinSkills(process.cwd());
|
|
9692
|
+
if (removedCount > 0) {
|
|
9693
|
+
console.log(` ${green}✓${reset} Removed ${removedCount} legacy .devin/skills/gsd-* dir(s) (pre-#1615 Windsurf layout)`);
|
|
9694
|
+
}
|
|
9695
|
+
}
|
|
9696
|
+
|
|
9838
9697
|
// Hermes only: write DESCRIPTION.md for the gsd/ category after layout install
|
|
9839
9698
|
if (isHermes) {
|
|
9840
9699
|
writeHermesCategoryDescription(path.join(targetDir, 'skills', 'gsd'));
|
|
@@ -9875,6 +9734,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9875
9734
|
} else {
|
|
9876
9735
|
failures.push('agents/gsd.yaml');
|
|
9877
9736
|
}
|
|
9737
|
+
} else if (isWindsurf) {
|
|
9738
|
+
if (isGlobal) {
|
|
9739
|
+
console.log(` ${green}✓${reset} Windsurf global install skipped workflow artifacts (workspace-only)`);
|
|
9740
|
+
} else {
|
|
9741
|
+
const workflowsDir = path.join(targetDir, 'workflows');
|
|
9742
|
+
if (fs.existsSync(workflowsDir)) {
|
|
9743
|
+
const workflowCount = fs.readdirSync(workflowsDir)
|
|
9744
|
+
.filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length;
|
|
9745
|
+
if (workflowCount > 0) {
|
|
9746
|
+
console.log(` ${green}✓${reset} Installed ${workflowCount} workflows to workflows/`);
|
|
9747
|
+
} else {
|
|
9748
|
+
failures.push('workflows/gsd-*');
|
|
9749
|
+
}
|
|
9750
|
+
} else {
|
|
9751
|
+
failures.push('workflows/gsd-*');
|
|
9752
|
+
}
|
|
9753
|
+
}
|
|
9878
9754
|
} else {
|
|
9879
9755
|
const skillsDir = path.join(targetDir, 'skills');
|
|
9880
9756
|
if (fs.existsSync(skillsDir)) {
|
|
@@ -10044,7 +9920,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10044
9920
|
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
10045
9921
|
const stem = entry.name.slice(0, -3);
|
|
10046
9922
|
let content = fs.readFileSync(path.join(gsdSrc, entry.name), 'utf8');
|
|
10047
|
-
content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal);
|
|
9923
|
+
content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, getCommitAttribution(runtime));
|
|
10048
9924
|
content = normalizeAgentBodyForRuntime(content, runtime, cmdNames);
|
|
10049
9925
|
fs.writeFileSync(path.join(commandsDir, `gsd-${stem}.md`), content);
|
|
10050
9926
|
cmdCount++;
|
|
@@ -10100,6 +9976,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10100
9976
|
failures.push('gsd-core');
|
|
10101
9977
|
}
|
|
10102
9978
|
|
|
9979
|
+
// #1629 critical fix: Windsurf workflow wrappers (convertClaudeCommandToWindsurfWorkflow)
|
|
9980
|
+
// delegate to command bodies at <targetDir>/gsd-core/commands/gsd/${stem}.md via a
|
|
9981
|
+
// hardcoded @~/.claude/gsd-core/commands/gsd/ path that _applyRuntimeRewrites rewrites
|
|
9982
|
+
// to the install target. The source gsd-core/ dir does NOT ship with commands/ —
|
|
9983
|
+
// the canonical command source lives at the package root (commands/gsd/). Without
|
|
9984
|
+
// this copy, every /gsd-* workflow in Cascade references a missing file and the LLM
|
|
9985
|
+
// cannot execute the command body. Surfaced by the #1629 regression test after the
|
|
9986
|
+
// original adversarial review of #1622 missed it.
|
|
9987
|
+
if (isWindsurf && !isGlobal) {
|
|
9988
|
+
const commandsSrc = path.join(src, 'commands', 'gsd');
|
|
9989
|
+
const commandsDest = path.join(skillDest, 'commands', 'gsd');
|
|
9990
|
+
if (fs.existsSync(commandsSrc)) {
|
|
9991
|
+
copyWithPathReplacement(commandsSrc, commandsDest, pathPrefix, runtime, true, isGlobal);
|
|
9992
|
+
console.log(` ${green}✓${reset} Installed command bodies to gsd-core/commands/gsd/ (workflow delegation targets)`);
|
|
9993
|
+
}
|
|
9994
|
+
}
|
|
9995
|
+
|
|
10103
9996
|
// Copy shared manifests into the gsd-core payload
|
|
10104
9997
|
// at the co-located path that CJS modules resolve first:
|
|
10105
9998
|
// gsd-core/bin/shared/*.json
|
|
@@ -12156,6 +12049,7 @@ module.exports = {
|
|
|
12156
12049
|
convertClaudeAgentToCodexAgent,
|
|
12157
12050
|
generateCodexAgentToml,
|
|
12158
12051
|
cleanupCodexSkillMetadataSidecars,
|
|
12052
|
+
cleanupWindsurfLegacyDevinSkills,
|
|
12159
12053
|
generateCodexConfigBlock,
|
|
12160
12054
|
stripGsdFromCodexConfig,
|
|
12161
12055
|
migrateCodexHooksMapFormat,
|
|
@@ -12217,6 +12111,7 @@ module.exports = {
|
|
|
12217
12111
|
skillFrontmatterName,
|
|
12218
12112
|
convertClaudeToWindsurfMarkdown,
|
|
12219
12113
|
convertClaudeCommandToWindsurfSkill,
|
|
12114
|
+
convertClaudeCommandToWindsurfWorkflow,
|
|
12220
12115
|
convertClaudeAgentToWindsurfAgent,
|
|
12221
12116
|
convertClaudeToAugmentMarkdown,
|
|
12222
12117
|
convertClaudeCommandToAugmentSkill,
|
|
@@ -12291,7 +12186,10 @@ module.exports = {
|
|
|
12291
12186
|
// #1191 — exported so tests exercise the REAL readSettings, not a replica
|
|
12292
12187
|
readSettings,
|
|
12293
12188
|
stripJsonComments,
|
|
12294
|
-
|
|
12189
|
+
// Compatibility relays retained after auditing the former broad
|
|
12190
|
+
// runtimeArtifactConversion spread (#1559).
|
|
12191
|
+
processAttribution,
|
|
12192
|
+
applyRuntimeContentRewritesForCommandsInPlace,
|
|
12295
12193
|
};
|
|
12296
12194
|
|
|
12297
12195
|
// Main logic — only run when not loaded as a module for testing
|