@specship/specship-win32-arm64 0.25.0 → 0.28.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/bin/specship.cmd +31 -1
- package/lib/dist/activation/cheatsheet.d.ts +17 -1
- package/lib/dist/activation/cheatsheet.d.ts.map +1 -1
- package/lib/dist/activation/cheatsheet.js +47 -3
- package/lib/dist/activation/cheatsheet.js.map +1 -1
- package/lib/dist/activation/hook-output.d.ts +80 -0
- package/lib/dist/activation/hook-output.d.ts.map +1 -0
- package/lib/dist/activation/hook-output.js +103 -0
- package/lib/dist/activation/hook-output.js.map +1 -0
- package/lib/dist/activation/search-intercept.d.ts +60 -29
- package/lib/dist/activation/search-intercept.d.ts.map +1 -1
- package/lib/dist/activation/search-intercept.js +159 -55
- package/lib/dist/activation/search-intercept.js.map +1 -1
- package/lib/dist/activation/steering.d.ts +8 -10
- package/lib/dist/activation/steering.d.ts.map +1 -1
- package/lib/dist/activation/steering.js +21 -6
- package/lib/dist/activation/steering.js.map +1 -1
- package/lib/dist/behaviour/behaviour-surface.d.ts +47 -0
- package/lib/dist/behaviour/behaviour-surface.d.ts.map +1 -1
- package/lib/dist/behaviour/behaviour-surface.js +108 -2
- package/lib/dist/behaviour/behaviour-surface.js.map +1 -1
- package/lib/dist/bin/node-version-check.d.ts +22 -7
- package/lib/dist/bin/node-version-check.d.ts.map +1 -1
- package/lib/dist/bin/node-version-check.js +39 -9
- package/lib/dist/bin/node-version-check.js.map +1 -1
- package/lib/dist/bin/specship.js +762 -81
- package/lib/dist/bin/specship.js.map +1 -1
- package/lib/dist/commands/specship/learn.md +22 -0
- package/lib/dist/commands/specship/spec.md +12 -0
- package/lib/dist/db/sqlite-adapter.d.ts +7 -5
- package/lib/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/lib/dist/db/sqlite-adapter.js +11 -9
- package/lib/dist/db/sqlite-adapter.js.map +1 -1
- package/lib/dist/enforce/changed-files.d.ts +11 -0
- package/lib/dist/enforce/changed-files.d.ts.map +1 -1
- package/lib/dist/enforce/changed-files.js +42 -0
- package/lib/dist/enforce/changed-files.js.map +1 -1
- package/lib/dist/enforce/enforce.d.ts +25 -1
- package/lib/dist/enforce/enforce.d.ts.map +1 -1
- package/lib/dist/enforce/enforce.js +58 -3
- package/lib/dist/enforce/enforce.js.map +1 -1
- package/lib/dist/enforce/mutation.d.ts +46 -0
- package/lib/dist/enforce/mutation.d.ts.map +1 -0
- package/lib/dist/enforce/mutation.js +104 -0
- package/lib/dist/enforce/mutation.js.map +1 -0
- package/lib/dist/enforce/suppressions.d.ts +33 -0
- package/lib/dist/enforce/suppressions.d.ts.map +1 -0
- package/lib/dist/enforce/suppressions.js +159 -0
- package/lib/dist/enforce/suppressions.js.map +1 -0
- package/lib/dist/extraction/specs/markdown-spec-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/specs/markdown-spec-extractor.js +16 -2
- package/lib/dist/extraction/specs/markdown-spec-extractor.js.map +1 -1
- package/lib/dist/graph/spec-coverage.d.ts +57 -1
- package/lib/dist/graph/spec-coverage.d.ts.map +1 -1
- package/lib/dist/graph/spec-coverage.js +129 -5
- package/lib/dist/graph/spec-coverage.js.map +1 -1
- package/lib/dist/health/smoke-check.js +2 -2
- package/lib/dist/health/smoke-check.js.map +1 -1
- package/lib/dist/index.d.ts +8 -3
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +65 -9
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/gemini-commands.d.ts +107 -0
- package/lib/dist/installer/gemini-commands.d.ts.map +1 -0
- package/lib/dist/installer/gemini-commands.js +264 -0
- package/lib/dist/installer/gemini-commands.js.map +1 -0
- package/lib/dist/installer/index.d.ts +23 -1
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +22 -3
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/installer/install-defaults.d.ts +19 -0
- package/lib/dist/installer/install-defaults.d.ts.map +1 -1
- package/lib/dist/installer/install-defaults.js +28 -0
- package/lib/dist/installer/install-defaults.js.map +1 -1
- package/lib/dist/installer/instructions-template.d.ts +22 -0
- package/lib/dist/installer/instructions-template.d.ts.map +1 -1
- package/lib/dist/installer/instructions-template.js +37 -1
- package/lib/dist/installer/instructions-template.js.map +1 -1
- package/lib/dist/installer/targets/claude.d.ts +4 -0
- package/lib/dist/installer/targets/claude.d.ts.map +1 -1
- package/lib/dist/installer/targets/claude.js +8 -25
- package/lib/dist/installer/targets/claude.js.map +1 -1
- package/lib/dist/installer/targets/gemini.d.ts +85 -12
- package/lib/dist/installer/targets/gemini.d.ts.map +1 -1
- package/lib/dist/installer/targets/gemini.js +527 -30
- package/lib/dist/installer/targets/gemini.js.map +1 -1
- package/lib/dist/installer/targets/shared.d.ts +13 -0
- package/lib/dist/installer/targets/shared.d.ts.map +1 -1
- package/lib/dist/installer/targets/shared.js +18 -0
- package/lib/dist/installer/targets/shared.js.map +1 -1
- package/lib/dist/installer/targets/types.d.ts +12 -0
- package/lib/dist/installer/targets/types.d.ts.map +1 -1
- package/lib/dist/jira/auto-publish.d.ts +71 -4
- package/lib/dist/jira/auto-publish.d.ts.map +1 -1
- package/lib/dist/jira/auto-publish.js +186 -17
- package/lib/dist/jira/auto-publish.js.map +1 -1
- package/lib/dist/jira/reconcile.d.ts +24 -1
- package/lib/dist/jira/reconcile.d.ts.map +1 -1
- package/lib/dist/jira/reconcile.js +46 -2
- package/lib/dist/jira/reconcile.js.map +1 -1
- package/lib/dist/jira/repo-config.d.ts +9 -0
- package/lib/dist/jira/repo-config.d.ts.map +1 -1
- package/lib/dist/jira/repo-config.js +11 -0
- package/lib/dist/jira/repo-config.js.map +1 -1
- package/lib/dist/jira/types.d.ts +7 -0
- package/lib/dist/jira/types.d.ts.map +1 -1
- package/lib/dist/jira/types.js.map +1 -1
- package/lib/dist/knowledge/enforce.d.ts +75 -0
- package/lib/dist/knowledge/enforce.d.ts.map +1 -0
- package/lib/dist/knowledge/enforce.js +250 -0
- package/lib/dist/knowledge/enforce.js.map +1 -0
- package/lib/dist/knowledge/facts.d.ts +33 -0
- package/lib/dist/knowledge/facts.d.ts.map +1 -0
- package/lib/dist/knowledge/facts.js +117 -0
- package/lib/dist/knowledge/facts.js.map +1 -0
- package/lib/dist/knowledge/proposals.d.ts +88 -0
- package/lib/dist/knowledge/proposals.d.ts.map +1 -0
- package/lib/dist/knowledge/proposals.js +360 -0
- package/lib/dist/knowledge/proposals.js.map +1 -0
- package/lib/dist/knowledge/stale.d.ts +64 -0
- package/lib/dist/knowledge/stale.d.ts.map +1 -0
- package/lib/dist/knowledge/stale.js +174 -0
- package/lib/dist/knowledge/stale.js.map +1 -0
- package/lib/dist/mcp/gemini-session.d.ts +50 -0
- package/lib/dist/mcp/gemini-session.d.ts.map +1 -0
- package/lib/dist/mcp/gemini-session.js +159 -0
- package/lib/dist/mcp/gemini-session.js.map +1 -0
- package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
- package/lib/dist/mcp/jira-tools.js +25 -0
- package/lib/dist/mcp/jira-tools.js.map +1 -1
- package/lib/dist/mcp/model-context.d.ts +22 -9
- package/lib/dist/mcp/model-context.d.ts.map +1 -1
- package/lib/dist/mcp/model-context.js +35 -13
- package/lib/dist/mcp/model-context.js.map +1 -1
- package/lib/dist/mcp/proxy.js +1 -1
- package/lib/dist/mcp/proxy.js.map +1 -1
- package/lib/dist/mcp/server-instructions.d.ts +16 -15
- package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
- package/lib/dist/mcp/server-instructions.js +67 -1
- package/lib/dist/mcp/server-instructions.js.map +1 -1
- package/lib/dist/mcp/session.js +1 -1
- package/lib/dist/mcp/session.js.map +1 -1
- package/lib/dist/mcp/spec-tools.d.ts +9 -1
- package/lib/dist/mcp/spec-tools.d.ts.map +1 -1
- package/lib/dist/mcp/spec-tools.js +69 -21
- package/lib/dist/mcp/spec-tools.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts +37 -1
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +120 -9
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/pulse/pulse.d.ts +93 -0
- package/lib/dist/pulse/pulse.d.ts.map +1 -0
- package/lib/dist/pulse/pulse.js +401 -0
- package/lib/dist/pulse/pulse.js.map +1 -0
- package/lib/dist/reflect/apply.d.ts.map +1 -1
- package/lib/dist/reflect/apply.js +38 -3
- package/lib/dist/reflect/apply.js.map +1 -1
- package/lib/dist/reflect/auto-sweep.d.ts +26 -0
- package/lib/dist/reflect/auto-sweep.d.ts.map +1 -0
- package/lib/dist/reflect/auto-sweep.js +90 -0
- package/lib/dist/reflect/auto-sweep.js.map +1 -0
- package/lib/dist/reflect/cli.d.ts +62 -0
- package/lib/dist/reflect/cli.d.ts.map +1 -0
- package/lib/dist/reflect/cli.js +163 -0
- package/lib/dist/reflect/cli.js.map +1 -0
- package/lib/dist/reflect/miner.d.ts.map +1 -1
- package/lib/dist/reflect/miner.js +119 -0
- package/lib/dist/reflect/miner.js.map +1 -1
- package/lib/dist/reflect/sweep.d.ts +1 -0
- package/lib/dist/reflect/sweep.d.ts.map +1 -1
- package/lib/dist/reflect/sweep.js +62 -13
- package/lib/dist/reflect/sweep.js.map +1 -1
- package/lib/dist/reflect/targets.d.ts +2 -0
- package/lib/dist/reflect/targets.d.ts.map +1 -1
- package/lib/dist/reflect/targets.js +33 -0
- package/lib/dist/reflect/targets.js.map +1 -1
- package/lib/dist/reflect/types.d.ts +19 -2
- package/lib/dist/reflect/types.d.ts.map +1 -1
- package/lib/dist/resolution/test-title-links.d.ts.map +1 -1
- package/lib/dist/resolution/test-title-links.js +3 -14
- package/lib/dist/resolution/test-title-links.js.map +1 -1
- package/lib/dist/server/routes/claude.js +15 -7
- package/lib/dist/server/routes/knowledge.js +118 -0
- package/lib/dist/server/server.js +2 -0
- package/lib/dist/spec-lint/lint.d.ts +1 -1
- package/lib/dist/spec-lint/lint.d.ts.map +1 -1
- package/lib/dist/spec-lint/lint.js +63 -2
- package/lib/dist/spec-lint/lint.js.map +1 -1
- package/lib/dist/spec-quality/criterion-ids.d.ts +13 -0
- package/lib/dist/spec-quality/criterion-ids.d.ts.map +1 -0
- package/lib/dist/spec-quality/criterion-ids.js +38 -0
- package/lib/dist/spec-quality/criterion-ids.js.map +1 -0
- package/lib/dist/spec-quality/evidence-kind.d.ts +54 -0
- package/lib/dist/spec-quality/evidence-kind.d.ts.map +1 -0
- package/lib/dist/spec-quality/evidence-kind.js +183 -0
- package/lib/dist/spec-quality/evidence-kind.js.map +1 -0
- package/lib/dist/spec-quality/link-persist.d.ts +65 -0
- package/lib/dist/spec-quality/link-persist.d.ts.map +1 -0
- package/lib/dist/spec-quality/link-persist.js +249 -0
- package/lib/dist/spec-quality/link-persist.js.map +1 -0
- package/lib/dist/spec-quality/plan-scope.d.ts +38 -0
- package/lib/dist/spec-quality/plan-scope.d.ts.map +1 -0
- package/lib/dist/spec-quality/plan-scope.js +129 -0
- package/lib/dist/spec-quality/plan-scope.js.map +1 -0
- package/lib/dist/spec-quality/postcheck.d.ts +85 -0
- package/lib/dist/spec-quality/postcheck.d.ts.map +1 -0
- package/lib/dist/spec-quality/postcheck.js +390 -0
- package/lib/dist/spec-quality/postcheck.js.map +1 -0
- package/lib/dist/spec-quality/readiness.d.ts +72 -0
- package/lib/dist/spec-quality/readiness.d.ts.map +1 -0
- package/lib/dist/spec-quality/readiness.js +226 -0
- package/lib/dist/spec-quality/readiness.js.map +1 -0
- package/lib/dist/workflows/agents/claude.d.ts +16 -0
- package/lib/dist/workflows/agents/claude.d.ts.map +1 -0
- package/lib/dist/workflows/agents/claude.js +82 -0
- package/lib/dist/workflows/agents/claude.js.map +1 -0
- package/lib/dist/workflows/agents/gemini.d.ts +21 -0
- package/lib/dist/workflows/agents/gemini.d.ts.map +1 -0
- package/lib/dist/workflows/agents/gemini.js +124 -0
- package/lib/dist/workflows/agents/gemini.js.map +1 -0
- package/lib/dist/workflows/agents/index.d.ts +28 -0
- package/lib/dist/workflows/agents/index.d.ts.map +1 -0
- package/lib/dist/workflows/agents/index.js +104 -0
- package/lib/dist/workflows/agents/index.js.map +1 -0
- package/lib/dist/workflows/agents/types.d.ts +55 -0
- package/lib/dist/workflows/agents/types.d.ts.map +1 -0
- package/lib/dist/workflows/agents/types.js +46 -0
- package/lib/dist/workflows/agents/types.js.map +1 -0
- package/lib/dist/workflows/batch.d.ts +63 -0
- package/lib/dist/workflows/batch.d.ts.map +1 -0
- package/lib/dist/workflows/batch.js +151 -0
- package/lib/dist/workflows/batch.js.map +1 -0
- package/lib/dist/workflows/defaults/claude-design-implement.yaml +3 -4
- package/lib/dist/workflows/defaults/spec-author.yaml +11 -9
- package/lib/dist/workflows/defaults/spec-implement-mixed.yaml +21 -436
- package/lib/dist/workflows/defaults/spec-implement.yaml +465 -46
- package/lib/dist/workflows/discovery.d.ts +13 -0
- package/lib/dist/workflows/discovery.d.ts.map +1 -1
- package/lib/dist/workflows/discovery.js +79 -17
- package/lib/dist/workflows/discovery.js.map +1 -1
- package/lib/dist/workflows/executor.d.ts +18 -0
- package/lib/dist/workflows/executor.d.ts.map +1 -1
- package/lib/dist/workflows/executor.js +130 -33
- package/lib/dist/workflows/executor.js.map +1 -1
- package/lib/dist/workflows/gc.d.ts +50 -0
- package/lib/dist/workflows/gc.d.ts.map +1 -0
- package/lib/dist/workflows/gc.js +120 -0
- package/lib/dist/workflows/gc.js.map +1 -0
- package/lib/dist/workflows/merge-queue.d.ts +64 -0
- package/lib/dist/workflows/merge-queue.d.ts.map +1 -0
- package/lib/dist/workflows/merge-queue.js +162 -0
- package/lib/dist/workflows/merge-queue.js.map +1 -0
- package/lib/dist/workflows/runners/approval.d.ts +1 -1
- package/lib/dist/workflows/runners/approval.d.ts.map +1 -1
- package/lib/dist/workflows/runners/approval.js +11 -2
- package/lib/dist/workflows/runners/approval.js.map +1 -1
- package/lib/dist/workflows/runners/bash.d.ts +15 -0
- package/lib/dist/workflows/runners/bash.d.ts.map +1 -1
- package/lib/dist/workflows/runners/bash.js +50 -1
- package/lib/dist/workflows/runners/bash.js.map +1 -1
- package/lib/dist/workflows/runners/prompt.d.ts +16 -41
- package/lib/dist/workflows/runners/prompt.d.ts.map +1 -1
- package/lib/dist/workflows/runners/prompt.js +55 -120
- package/lib/dist/workflows/runners/prompt.js.map +1 -1
- package/lib/dist/workflows/runners/types.d.ts +12 -0
- package/lib/dist/workflows/runners/types.d.ts.map +1 -1
- package/lib/dist/workflows/schemas/workflow.d.ts +17 -0
- package/lib/dist/workflows/schemas/workflow.d.ts.map +1 -1
- package/lib/dist/workflows/schemas/workflow.js +27 -0
- package/lib/dist/workflows/schemas/workflow.js.map +1 -1
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +2 -2
- package/package.json +5 -3
- package/node.exe +0 -4
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Google Gemini CLI target — Phase 1 (GEMINI-TARGET-DOC, REQ-GEMINI-002/003).
|
|
4
4
|
*
|
|
5
|
-
* Implements the full `AgentTarget` contract over
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* commands
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* Implements the full `AgentTarget` contract over Gemini CLI's `mcpServers`
|
|
6
|
+
* settings block, GEMINI.md steering (REQ-GEMINI-004), settings hooks
|
|
7
|
+
* (REQ-GEMINI-010/011), the opt-in `--trust` policy (REQ-GEMINI-009) and the
|
|
8
|
+
* slash commands rendered into Gemini TOML from the same
|
|
9
|
+
* `commands/specship/*.md` sources the Claude target copies (REQ-GEMINI-005,
|
|
10
|
+
* see `../gemini-commands.ts` for the verified format + per-command
|
|
11
|
+
* decisions). What still has no Gemini equivalent (the `specship-explorer`
|
|
12
|
+
* subagent, the status line, the permissions list) is left unwritten and
|
|
13
|
+
* named in `notes` rather than half-installed (REQ-GEMINI-006).
|
|
13
14
|
*
|
|
14
15
|
* The target IS registered in `targets/registry.ts`, but a plain
|
|
15
16
|
* `specship install` stays Claude-only: target selection is opt-in through
|
|
@@ -66,17 +67,77 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
66
67
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
67
68
|
exports.geminiTarget = exports.GeminiCliTarget = void 0;
|
|
68
69
|
exports.settingsPath = settingsPath;
|
|
70
|
+
exports.writeGeminiInstructionsEntry = writeGeminiInstructionsEntry;
|
|
71
|
+
exports.removeGeminiInstructionsEntry = removeGeminiInstructionsEntry;
|
|
72
|
+
exports.writeGeminiSddEntry = writeGeminiSddEntry;
|
|
73
|
+
exports.removeGeminiSddEntry = removeGeminiSddEntry;
|
|
69
74
|
exports.writeGeminiMcpEntry = writeGeminiMcpEntry;
|
|
75
|
+
exports.geminiCommandsDir = geminiCommandsDir;
|
|
76
|
+
exports.writeGeminiCommandsEntries = writeGeminiCommandsEntries;
|
|
77
|
+
exports.removeGeminiCommandsEntries = removeGeminiCommandsEntries;
|
|
78
|
+
exports.writeGeminiHooksEntry = writeGeminiHooksEntry;
|
|
79
|
+
exports.writeGeminiSddHooksEntry = writeGeminiSddHooksEntry;
|
|
80
|
+
exports.removeGeminiHooksEntry = removeGeminiHooksEntry;
|
|
81
|
+
exports.removeGeminiSddHooksEntry = removeGeminiSddHooksEntry;
|
|
82
|
+
exports.renderGeminiPolicyToml = renderGeminiPolicyToml;
|
|
83
|
+
exports.writeGeminiPolicyEntry = writeGeminiPolicyEntry;
|
|
84
|
+
exports.removeGeminiPolicyEntry = removeGeminiPolicyEntry;
|
|
70
85
|
const fs = __importStar(require("fs"));
|
|
71
86
|
const os = __importStar(require("os"));
|
|
72
87
|
const path = __importStar(require("path"));
|
|
73
88
|
const shared_1 = require("./shared");
|
|
89
|
+
const instructions_template_1 = require("../instructions-template");
|
|
90
|
+
const claude_1 = require("./claude");
|
|
91
|
+
const gemini_commands_1 = require("../gemini-commands");
|
|
74
92
|
/** Gemini CLI's settings file for this location. */
|
|
75
93
|
function settingsPath(loc) {
|
|
76
94
|
return loc === 'global'
|
|
77
95
|
? path.join(os.homedir(), '.gemini', 'settings.json')
|
|
78
96
|
: path.join(process.cwd(), '.gemini', 'settings.json');
|
|
79
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Gemini CLI's project / user context file. Global → `~/.gemini/GEMINI.md`
|
|
100
|
+
* (user memory, loaded in every session); local → `<cwd>/GEMINI.md` (project
|
|
101
|
+
* memory, loaded when Gemini CLI starts in this repo). Mirror of the Claude
|
|
102
|
+
* target's `claudeMdPath` for CLAUDE.md.
|
|
103
|
+
*/
|
|
104
|
+
function geminiMdPath(loc) {
|
|
105
|
+
return loc === 'global'
|
|
106
|
+
? path.join(os.homedir(), '.gemini', 'GEMINI.md')
|
|
107
|
+
: path.join(process.cwd(), 'GEMINI.md');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Write the retrieval-preference block into GEMINI.md (REQ-GEMINI-004).
|
|
111
|
+
* Marker-delimited and idempotent — content outside the markers is never
|
|
112
|
+
* touched, and a byte-identical re-run returns `unchanged`.
|
|
113
|
+
*/
|
|
114
|
+
function writeGeminiInstructionsEntry(loc) {
|
|
115
|
+
const file = geminiMdPath(loc);
|
|
116
|
+
const action = (0, shared_1.upsertMarkedSection)(file, instructions_template_1.SPECSHIP_GEMINI_RETRIEVAL_START, instructions_template_1.SPECSHIP_GEMINI_RETRIEVAL_END, (0, instructions_template_1.getGeminiRetrievalBlock)());
|
|
117
|
+
return { path: file, action };
|
|
118
|
+
}
|
|
119
|
+
/** Inverse of `writeGeminiInstructionsEntry`: strip the retrieval block. */
|
|
120
|
+
function removeGeminiInstructionsEntry(loc) {
|
|
121
|
+
const file = geminiMdPath(loc);
|
|
122
|
+
const action = (0, shared_1.removeMarkedSection)(file, instructions_template_1.SPECSHIP_GEMINI_RETRIEVAL_START, instructions_template_1.SPECSHIP_GEMINI_RETRIEVAL_END);
|
|
123
|
+
return { path: file, action };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Write the spec-driven-development steering block into GEMINI.md
|
|
127
|
+
* (REQ-GEMINI-004.A4). Generated from the SAME `getSddRuleBlock` the Claude
|
|
128
|
+
* target writes, so the two surfaces cannot drift byte-wise.
|
|
129
|
+
*/
|
|
130
|
+
function writeGeminiSddEntry(loc) {
|
|
131
|
+
const file = geminiMdPath(loc);
|
|
132
|
+
const action = (0, shared_1.upsertMarkedSection)(file, instructions_template_1.SPECSHIP_SDD_SECTION_START, instructions_template_1.SPECSHIP_SDD_SECTION_END, (0, instructions_template_1.getSddRuleBlock)());
|
|
133
|
+
return { path: file, action };
|
|
134
|
+
}
|
|
135
|
+
/** Inverse of `writeGeminiSddEntry`: strip the SDD rule block. */
|
|
136
|
+
function removeGeminiSddEntry(loc) {
|
|
137
|
+
const file = geminiMdPath(loc);
|
|
138
|
+
const action = (0, shared_1.removeMarkedSection)(file, instructions_template_1.SPECSHIP_SDD_SECTION_START, instructions_template_1.SPECSHIP_SDD_SECTION_END);
|
|
139
|
+
return { path: file, action };
|
|
140
|
+
}
|
|
80
141
|
/**
|
|
81
142
|
* The `mcpServers.specship` entry as Gemini CLI wants it: the Claude entry
|
|
82
143
|
* minus `type`, which Gemini restricts to `'sse' | 'http'` (a stdio server is
|
|
@@ -86,7 +147,15 @@ function settingsPath(loc) {
|
|
|
86
147
|
*/
|
|
87
148
|
function geminiEntry(integrations = []) {
|
|
88
149
|
const { type: _stdioIsImplied, ...server } = (0, shared_1.getMcpServerConfig)(integrations);
|
|
89
|
-
|
|
150
|
+
// Host marker (REQ-LOWEND-001): tier auto-detection's Gemini leg only runs
|
|
151
|
+
// when the server was launched by Gemini CLI. This env is that evidence —
|
|
152
|
+
// present in both the written entry and the printable snippet by
|
|
153
|
+
// construction, absent from every Claude Code launch.
|
|
154
|
+
const env = {
|
|
155
|
+
...server.env,
|
|
156
|
+
SPECSHIP_HOST: 'gemini',
|
|
157
|
+
};
|
|
158
|
+
return { ...server, env };
|
|
90
159
|
}
|
|
91
160
|
/**
|
|
92
161
|
* Write the specship server into Gemini CLI's `mcpServers` block
|
|
@@ -120,13 +189,388 @@ function writeGeminiMcpEntry(loc, integrations = []) {
|
|
|
120
189
|
}
|
|
121
190
|
/**
|
|
122
191
|
* What a Claude install provides that this one does not (REQ-GEMINI-006.A1).
|
|
123
|
-
* One note, naming every unsupported surface
|
|
124
|
-
*
|
|
192
|
+
* One note, naming every still-unsupported surface. As of REQ-GEMINI-010 the
|
|
193
|
+
* prompt / session / tool hooks DO have Gemini equivalents and are installed,
|
|
194
|
+
* so the note no longer lists them; nor slash commands, which REQ-GEMINI-005
|
|
195
|
+
* renders as Gemini TOML. What remains is the specship-explorer subagent, the
|
|
196
|
+
* status line, and the permissions auto-allow list (REQ-GEMINI-009 covers the
|
|
197
|
+
* opt-in policy file). A command the renderer had to exclude gets its own
|
|
198
|
+
* note from `writeGeminiCommandsEntries`.
|
|
125
199
|
*/
|
|
126
|
-
const CAPABILITY_NOTE = 'Gemini CLI install wires the MCP server
|
|
127
|
-
'
|
|
128
|
-
'
|
|
129
|
-
'
|
|
200
|
+
const CAPABILITY_NOTE = 'Gemini CLI install wires the MCP server, writes a GEMINI.md steering ' +
|
|
201
|
+
'block, installs prompt/session/tool hooks, and renders the /specship:* ' +
|
|
202
|
+
'slash commands as Gemini TOML. No specship-explorer subagent and no ' +
|
|
203
|
+
'status line — Gemini CLI has no equivalent surface. The tool playbook ' +
|
|
204
|
+
'itself still lives in the MCP server instructions.';
|
|
205
|
+
/**
|
|
206
|
+
* Gemini CLI's custom-commands dir for this location (verified against
|
|
207
|
+
* gemini-cli 0.28.0 `Storage.getUserCommandsDir` / `getProjectCommandsDir`):
|
|
208
|
+
* global `~/.gemini/commands`, local `<cwd>/.gemini/commands`. Same scope as
|
|
209
|
+
* the MCP entry written by `settingsPath`.
|
|
210
|
+
*/
|
|
211
|
+
function geminiCommandsDir(loc) {
|
|
212
|
+
return path.join(path.dirname(settingsPath(loc)), 'commands');
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Every command the Claude installer ships, both tiers — the Gemini set is
|
|
216
|
+
* derived from the same list so the surfaces can't diverge (REQ-GEMINI-005).
|
|
217
|
+
*/
|
|
218
|
+
const ALL_COMMAND_SOURCES = [
|
|
219
|
+
...claude_1.RETRIEVAL_TIER_COMMANDS,
|
|
220
|
+
...claude_1.GOVERNANCE_TIER_COMMANDS,
|
|
221
|
+
];
|
|
222
|
+
/** True when `file` is a TOML a SpecShip install wrote (carries our header). */
|
|
223
|
+
function isOurCommandFile(file) {
|
|
224
|
+
try {
|
|
225
|
+
return fs.readFileSync(file, 'utf-8').startsWith(gemini_commands_1.GEMINI_COMMAND_HEADER_MARKER);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Render and write the slash commands (REQ-GEMINI-005). Same tiering as the
|
|
233
|
+
* Claude target: the retrieval door always, the governance doors unless
|
|
234
|
+
* `--no-sdd`. Per-file idempotent (byte-equal → `unchanged`); a user-authored
|
|
235
|
+
* file at one of our paths (no header marker) is never overwritten (`kept`).
|
|
236
|
+
* A command the renderer excluded is NOT written, a stale copy an earlier
|
|
237
|
+
* install wrote is removed, and the exclusion is returned as a note.
|
|
238
|
+
*
|
|
239
|
+
* `commandsSourceDir` defaults to the packaged `commands/` dir — the same
|
|
240
|
+
* one the Claude target copies from.
|
|
241
|
+
*/
|
|
242
|
+
function writeGeminiCommandsEntries(loc, includeGovernance = true, commandsSourceDir = (0, shared_1.packageAssetPath)('commands')) {
|
|
243
|
+
const names = includeGovernance ? ALL_COMMAND_SOURCES : [...claude_1.RETRIEVAL_TIER_COMMANDS];
|
|
244
|
+
const dir = geminiCommandsDir(loc);
|
|
245
|
+
const files = [];
|
|
246
|
+
const notes = [];
|
|
247
|
+
for (const cmd of (0, gemini_commands_1.geminiCommandSet)(commandsSourceDir, names)) {
|
|
248
|
+
const dest = path.join(dir, cmd.target);
|
|
249
|
+
if (cmd.toml === undefined) {
|
|
250
|
+
notes.push(`Gemini CLI: skipped /${cmd.target.replace(/\.toml$/, '').replace(/\//g, ':')} — ${cmd.excluded}.`);
|
|
251
|
+
if (isOurCommandFile(dest)) {
|
|
252
|
+
fs.unlinkSync(dest);
|
|
253
|
+
files.push({ path: dest, action: 'removed' });
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const existed = fs.existsSync(dest);
|
|
258
|
+
if (existed) {
|
|
259
|
+
const current = fs.readFileSync(dest, 'utf-8');
|
|
260
|
+
if (current === cmd.toml) {
|
|
261
|
+
files.push({ path: dest, action: 'unchanged' });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (!current.startsWith(gemini_commands_1.GEMINI_COMMAND_HEADER_MARKER)) {
|
|
265
|
+
files.push({ path: dest, action: 'kept' });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
270
|
+
fs.writeFileSync(dest, cmd.toml);
|
|
271
|
+
files.push({ path: dest, action: existed ? 'updated' : 'created' });
|
|
272
|
+
}
|
|
273
|
+
return { files, notes };
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Inverse of `writeGeminiCommandsEntries` (REQ-GEMINI-005.A2). Removes only
|
|
277
|
+
* files at our command paths that carry our header marker — user-authored
|
|
278
|
+
* commands, in the same `specship/` subdir or anywhere else in the commands
|
|
279
|
+
* dir, survive byte-identical. The `specship/` subdir is dropped only when
|
|
280
|
+
* that leaves it empty.
|
|
281
|
+
*/
|
|
282
|
+
function removeGeminiCommandsEntries(loc) {
|
|
283
|
+
const dir = geminiCommandsDir(loc);
|
|
284
|
+
const files = [];
|
|
285
|
+
for (const source of ALL_COMMAND_SOURCES) {
|
|
286
|
+
const dest = path.join(dir, (0, gemini_commands_1.geminiCommandTarget)(source));
|
|
287
|
+
if (!fs.existsSync(dest))
|
|
288
|
+
continue;
|
|
289
|
+
if (!isOurCommandFile(dest)) {
|
|
290
|
+
files.push({ path: dest, action: 'kept' });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
fs.unlinkSync(dest);
|
|
294
|
+
files.push({ path: dest, action: 'removed' });
|
|
295
|
+
}
|
|
296
|
+
const parents = new Set(ALL_COMMAND_SOURCES.map((s) => path.dirname(path.join(dir, s))));
|
|
297
|
+
for (const parent of parents) {
|
|
298
|
+
try {
|
|
299
|
+
if (parent !== dir && fs.existsSync(parent) && fs.readdirSync(parent).length === 0) {
|
|
300
|
+
fs.rmdirSync(parent);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// Leave the directory in place if it can't be removed — never fatal.
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return files;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Opt-in `--trust` only (REQ-GEMINI-009.A5): shown alongside the policy
|
|
311
|
+
* writer so a user who opted in knows the limits of what the file does.
|
|
312
|
+
*/
|
|
313
|
+
const TRUST_POLICY_NOTE = 'Wrote ~/.gemini/policies/specship.toml allowing SpecShip\'s read-only ' +
|
|
314
|
+
'tools. An administrator policy still overrides this file, and ' +
|
|
315
|
+
'run_shell_command confirmations are unaffected.';
|
|
316
|
+
/**
|
|
317
|
+
* Gemini's project-level hooks only run when the user has marked the folder
|
|
318
|
+
* as trusted — a local install points the user at that gate so hooks silently
|
|
319
|
+
* failing to run isn't the first evidence something's off.
|
|
320
|
+
*/
|
|
321
|
+
const LOCAL_TRUST_NOTE = 'Gemini CLI runs project-level hooks only in folders you have marked as ' +
|
|
322
|
+
'trusted. If the SpecShip hooks seem inert in this project, open it once in ' +
|
|
323
|
+
'Gemini CLI and accept the trust prompt.';
|
|
324
|
+
const GEMINI_HOOKS = [
|
|
325
|
+
// Prompt-level retrieval nudge — mirror of Claude's UserPromptSubmit.
|
|
326
|
+
{
|
|
327
|
+
event: 'BeforeAgent',
|
|
328
|
+
matcher: '',
|
|
329
|
+
hook: { type: 'command', command: 'specship steer-nudge' },
|
|
330
|
+
},
|
|
331
|
+
// SessionStart cheat-sheet (human-facing, systemMessage).
|
|
332
|
+
{
|
|
333
|
+
event: 'SessionStart',
|
|
334
|
+
matcher: '',
|
|
335
|
+
hook: { type: 'command', command: 'specship cheatsheet' },
|
|
336
|
+
},
|
|
337
|
+
// SessionStart sync + drift summary — one-shot catch-up at session start.
|
|
338
|
+
{
|
|
339
|
+
event: 'SessionStart',
|
|
340
|
+
matcher: '',
|
|
341
|
+
hook: { type: 'command', command: 'specship sync --quiet --drift-summary' },
|
|
342
|
+
},
|
|
343
|
+
// Post-edit sync — Gemini's `write_file` / `replace` tools are the write
|
|
344
|
+
// surface, so matcher is scoped there (parity with Claude's Edit|Write).
|
|
345
|
+
{
|
|
346
|
+
event: 'AfterTool',
|
|
347
|
+
matcher: 'write_file|replace',
|
|
348
|
+
hook: { type: 'command', command: 'specship sync --quiet' },
|
|
349
|
+
},
|
|
350
|
+
// Search-intercept — REQ-GEMINI-011. Advisory redirect on built-in
|
|
351
|
+
// grep_search / glob, plus run_shell_command (which gets classified
|
|
352
|
+
// search-vs-other at hook time; non-search shell commands pass through
|
|
353
|
+
// silently). In strict mode (`SPECSHIP_GEMINI_STRICT=1`) grep_search and
|
|
354
|
+
// glob are denied; run_shell_command NEVER denies.
|
|
355
|
+
{
|
|
356
|
+
event: 'BeforeTool',
|
|
357
|
+
matcher: 'grep_search',
|
|
358
|
+
hook: { type: 'command', command: 'specship search-intercept' },
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
event: 'BeforeTool',
|
|
362
|
+
matcher: 'glob',
|
|
363
|
+
hook: { type: 'command', command: 'specship search-intercept' },
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
event: 'BeforeTool',
|
|
367
|
+
matcher: 'run_shell_command',
|
|
368
|
+
hook: { type: 'command', command: 'specship search-intercept' },
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
/** SDD governance tier — opt-out via `--no-sdd`, matching the Claude install. */
|
|
372
|
+
const GEMINI_SDD_HOOKS = [
|
|
373
|
+
{
|
|
374
|
+
event: 'BeforeAgent',
|
|
375
|
+
matcher: '',
|
|
376
|
+
hook: { type: 'command', command: 'specship spec-nudge' },
|
|
377
|
+
},
|
|
378
|
+
];
|
|
379
|
+
/**
|
|
380
|
+
* Idempotently merge Gemini hook entries into `settings.hooks`. Mirrors the
|
|
381
|
+
* Claude target's `writeHooksFor`: a matcher group sharing our exact matcher
|
|
382
|
+
* string is reused, sibling matchers / events are untouched, a byte-equal
|
|
383
|
+
* command is skipped. Returns `unchanged` when nothing was added.
|
|
384
|
+
*/
|
|
385
|
+
function writeGeminiHooksFor(loc, hooks) {
|
|
386
|
+
const file = settingsPath(loc);
|
|
387
|
+
const created = !fs.existsSync(file);
|
|
388
|
+
const settings = (0, shared_1.readJsonFile)(file);
|
|
389
|
+
const beforeJson = JSON.stringify(settings);
|
|
390
|
+
if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) {
|
|
391
|
+
settings.hooks = {};
|
|
392
|
+
}
|
|
393
|
+
for (const { event, matcher, hook } of hooks) {
|
|
394
|
+
if (!Array.isArray(settings.hooks[event]))
|
|
395
|
+
settings.hooks[event] = [];
|
|
396
|
+
let group = settings.hooks[event].find((g) => g && g.matcher === matcher);
|
|
397
|
+
if (!group) {
|
|
398
|
+
group = { matcher, hooks: [] };
|
|
399
|
+
settings.hooks[event].push(group);
|
|
400
|
+
}
|
|
401
|
+
if (!Array.isArray(group.hooks))
|
|
402
|
+
group.hooks = [];
|
|
403
|
+
if (!group.hooks.some((h) => h && h.command === hook.command)) {
|
|
404
|
+
group.hooks.push({ ...hook });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const afterJson = JSON.stringify(settings);
|
|
408
|
+
if (beforeJson === afterJson && !created) {
|
|
409
|
+
return { path: file, action: 'unchanged' };
|
|
410
|
+
}
|
|
411
|
+
(0, shared_1.writeJsonFile)(file, settings);
|
|
412
|
+
return { path: file, action: created ? 'created' : 'updated' };
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Surgically remove a predicate-matched set of commands from `settings.hooks`,
|
|
416
|
+
* then prune empty matcher groups / events / the top-level `hooks` map.
|
|
417
|
+
* Mirrors Claude's `stripHooksMatching` so sibling user-written hooks survive
|
|
418
|
+
* byte-identical.
|
|
419
|
+
*/
|
|
420
|
+
function stripGeminiHooksMatching(loc, predicate) {
|
|
421
|
+
const file = settingsPath(loc);
|
|
422
|
+
if (!fs.existsSync(file))
|
|
423
|
+
return { path: file, action: 'not-found' };
|
|
424
|
+
const settings = (0, shared_1.readJsonFile)(file);
|
|
425
|
+
const hooks = settings.hooks;
|
|
426
|
+
if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) {
|
|
427
|
+
return { path: file, action: 'unchanged' };
|
|
428
|
+
}
|
|
429
|
+
let removedAny = false;
|
|
430
|
+
for (const event of Object.keys(hooks)) {
|
|
431
|
+
const groups = hooks[event];
|
|
432
|
+
if (!Array.isArray(groups))
|
|
433
|
+
continue;
|
|
434
|
+
for (const group of groups) {
|
|
435
|
+
if (!group || !Array.isArray(group.hooks))
|
|
436
|
+
continue;
|
|
437
|
+
const before = group.hooks.length;
|
|
438
|
+
group.hooks = group.hooks.filter((h) => !predicate(h?.command));
|
|
439
|
+
if (group.hooks.length !== before)
|
|
440
|
+
removedAny = true;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (!removedAny)
|
|
444
|
+
return { path: file, action: 'unchanged' };
|
|
445
|
+
for (const event of Object.keys(hooks)) {
|
|
446
|
+
const groups = hooks[event];
|
|
447
|
+
if (!Array.isArray(groups))
|
|
448
|
+
continue;
|
|
449
|
+
hooks[event] = groups.filter((g) => !(g && Array.isArray(g.hooks) && g.hooks.length === 0));
|
|
450
|
+
if (hooks[event].length === 0)
|
|
451
|
+
delete hooks[event];
|
|
452
|
+
}
|
|
453
|
+
if (Object.keys(hooks).length === 0)
|
|
454
|
+
delete settings.hooks;
|
|
455
|
+
(0, shared_1.writeJsonFile)(file, settings);
|
|
456
|
+
return { path: file, action: 'removed' };
|
|
457
|
+
}
|
|
458
|
+
/** Write the default (retrieval + sync + cheatsheet) hook set. */
|
|
459
|
+
function writeGeminiHooksEntry(loc) {
|
|
460
|
+
return writeGeminiHooksFor(loc, GEMINI_HOOKS);
|
|
461
|
+
}
|
|
462
|
+
/** Write the governance-tier SDD nudge hook. */
|
|
463
|
+
function writeGeminiSddHooksEntry(loc) {
|
|
464
|
+
return writeGeminiHooksFor(loc, GEMINI_SDD_HOOKS);
|
|
465
|
+
}
|
|
466
|
+
function isGeminiHookCommand(command) {
|
|
467
|
+
if (typeof command !== 'string')
|
|
468
|
+
return false;
|
|
469
|
+
return GEMINI_HOOKS.some(({ hook }) => command === hook.command);
|
|
470
|
+
}
|
|
471
|
+
function isGeminiSddHookCommand(command) {
|
|
472
|
+
if (typeof command !== 'string')
|
|
473
|
+
return false;
|
|
474
|
+
return GEMINI_SDD_HOOKS.some(({ hook }) => command === hook.command);
|
|
475
|
+
}
|
|
476
|
+
/** Inverse of `writeGeminiHooksEntry`. */
|
|
477
|
+
function removeGeminiHooksEntry(loc) {
|
|
478
|
+
return stripGeminiHooksMatching(loc, isGeminiHookCommand);
|
|
479
|
+
}
|
|
480
|
+
/** Inverse of `writeGeminiSddHooksEntry`. */
|
|
481
|
+
function removeGeminiSddHooksEntry(loc) {
|
|
482
|
+
return stripGeminiHooksMatching(loc, isGeminiSddHookCommand);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Policy TOML (REQ-GEMINI-009): auto-allow Gemini's per-tool confirmation
|
|
486
|
+
* for SpecShip's READ-ONLY MCP tools only. User tier by construction — we
|
|
487
|
+
* never write to the admin policies directory, and an admin rule still wins
|
|
488
|
+
* over this one (that is the intended outcome). `run_shell_command` is out
|
|
489
|
+
* of scope: this file removes prompts only for SpecShip's own tools.
|
|
490
|
+
*
|
|
491
|
+
* Verified against Gemini CLI 0.28.0 `loadPoliciesFromToml`: the loader
|
|
492
|
+
* composes `specship__<toolName>` from `mcpName` itself, so every entry here
|
|
493
|
+
* is the BARE server tool name — a prefixed name would match nothing.
|
|
494
|
+
*/
|
|
495
|
+
function geminiPolicyPath() {
|
|
496
|
+
return path.join(os.homedir(), '.gemini', 'policies', 'specship.toml');
|
|
497
|
+
}
|
|
498
|
+
const GEMINI_POLICY_HEADER_MARKER = '# Written by specship install --trust — REQ-GEMINI-009';
|
|
499
|
+
const READONLY_TOOLS = Object.freeze([
|
|
500
|
+
'specship_explore',
|
|
501
|
+
'specship_search',
|
|
502
|
+
'specship_node',
|
|
503
|
+
'specship_callers',
|
|
504
|
+
'specship_callees',
|
|
505
|
+
'specship_impact',
|
|
506
|
+
'specship_files',
|
|
507
|
+
'specship_status',
|
|
508
|
+
'specship_health',
|
|
509
|
+
'specship_spec',
|
|
510
|
+
'specship_drifted',
|
|
511
|
+
]);
|
|
512
|
+
/** Mid of Gemini's 0–999 priority range: well below admin tiers. */
|
|
513
|
+
const POLICY_PRIORITY = 100;
|
|
514
|
+
/**
|
|
515
|
+
* Pure string builder for `specship.toml`. Single source for the writer so
|
|
516
|
+
* the on-disk content and any future `printConfig` surface cannot drift.
|
|
517
|
+
*/
|
|
518
|
+
function renderGeminiPolicyToml() {
|
|
519
|
+
const toolList = READONLY_TOOLS.map((t) => `"${t}"`).join(', ');
|
|
520
|
+
return [
|
|
521
|
+
GEMINI_POLICY_HEADER_MARKER,
|
|
522
|
+
'# User-tier policy auto-allowing SpecShip\'s read-only MCP tools.',
|
|
523
|
+
'# An administrator policy overrides this file. Shell-command',
|
|
524
|
+
'# confirmations (run_shell_command) are not affected.',
|
|
525
|
+
'',
|
|
526
|
+
// Gemini's loader reads `[[rule]]` tables (0.28.0 PolicyFileSchema.rule);
|
|
527
|
+
// any other table name is silently ignored and the policy does nothing.
|
|
528
|
+
'[[rule]]',
|
|
529
|
+
'mcpName = "specship"',
|
|
530
|
+
`toolName = [${toolList}]`,
|
|
531
|
+
'decision = "allow"',
|
|
532
|
+
`priority = ${POLICY_PRIORITY}`,
|
|
533
|
+
'',
|
|
534
|
+
].join('\n');
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Write `~/.gemini/policies/specship.toml`. Idempotent — byte-identical
|
|
538
|
+
* contents report `unchanged`. Never touches the admin policies directory.
|
|
539
|
+
*/
|
|
540
|
+
function writeGeminiPolicyEntry() {
|
|
541
|
+
const file = geminiPolicyPath();
|
|
542
|
+
const content = renderGeminiPolicyToml();
|
|
543
|
+
const existed = fs.existsSync(file);
|
|
544
|
+
if (existed) {
|
|
545
|
+
const current = fs.readFileSync(file, 'utf-8');
|
|
546
|
+
if (current === content)
|
|
547
|
+
return { path: file, action: 'unchanged' };
|
|
548
|
+
// Never overwrite a user-authored file of the same name — only one that
|
|
549
|
+
// an earlier `--trust` install wrote (it carries our header marker).
|
|
550
|
+
if (!current.startsWith(GEMINI_POLICY_HEADER_MARKER))
|
|
551
|
+
return { path: file, action: 'kept' };
|
|
552
|
+
}
|
|
553
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
554
|
+
fs.writeFileSync(file, content);
|
|
555
|
+
return { path: file, action: existed ? 'updated' : 'created' };
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Inverse of `writeGeminiPolicyEntry`. Removes `specship.toml` ONLY if its
|
|
559
|
+
* contents carry our header marker — a user-authored file of the same name
|
|
560
|
+
* is left byte-identical. Sibling files in the policies directory are
|
|
561
|
+
* untouched; the admin policies dir is never written or read.
|
|
562
|
+
*/
|
|
563
|
+
function removeGeminiPolicyEntry() {
|
|
564
|
+
const file = geminiPolicyPath();
|
|
565
|
+
if (!fs.existsSync(file))
|
|
566
|
+
return { path: file, action: 'not-found' };
|
|
567
|
+
const current = fs.readFileSync(file, 'utf-8');
|
|
568
|
+
if (!current.startsWith(GEMINI_POLICY_HEADER_MARKER)) {
|
|
569
|
+
return { path: file, action: 'kept' };
|
|
570
|
+
}
|
|
571
|
+
fs.unlinkSync(file);
|
|
572
|
+
return { path: file, action: 'removed' };
|
|
573
|
+
}
|
|
130
574
|
class GeminiCliTarget {
|
|
131
575
|
id = 'gemini';
|
|
132
576
|
displayName = 'Gemini CLI';
|
|
@@ -149,21 +593,46 @@ class GeminiCliTarget {
|
|
|
149
593
|
};
|
|
150
594
|
}
|
|
151
595
|
/**
|
|
152
|
-
* Writes
|
|
596
|
+
* Writes the MCP entry, GEMINI.md steering, hooks, the rendered slash
|
|
597
|
+
* commands and (with `--trust`) the policy TOML. `autoAllow` and
|
|
153
598
|
* `installStatusLine` are accepted and IGNORED — Gemini CLI has no
|
|
154
|
-
* permissions
|
|
155
|
-
*
|
|
156
|
-
*
|
|
599
|
+
* permissions list or status line to write them to, and faking one would be
|
|
600
|
+
* worse than the note (REQ-GEMINI-006). Nothing under `.claude/` is ever
|
|
601
|
+
* touched (REQ-GEMINI-006.A2).
|
|
157
602
|
*/
|
|
158
603
|
install(loc, opts) {
|
|
159
604
|
const integrations = [
|
|
160
605
|
...(opts.withJira ? ['jira'] : []),
|
|
161
606
|
...(opts.withDesigner ? ['designer'] : []),
|
|
162
607
|
];
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
608
|
+
const files = [writeGeminiMcpEntry(loc, integrations)];
|
|
609
|
+
// GEMINI.md retrieval preference — always written (REQ-GEMINI-004).
|
|
610
|
+
files.push(writeGeminiInstructionsEntry(loc));
|
|
611
|
+
// SDD rule — default-on, matching the Claude target's governance tier.
|
|
612
|
+
// `--no-sdd` (opts.sdd === false) self-heals by stripping an earlier
|
|
613
|
+
// governance install.
|
|
614
|
+
const includeGovernance = opts.sdd !== false;
|
|
615
|
+
files.push(includeGovernance ? writeGeminiSddEntry(loc) : removeGeminiSddEntry(loc));
|
|
616
|
+
// Prompt / session / tool hooks (REQ-GEMINI-010). Default retrieval tier
|
|
617
|
+
// is always written; the governance SDD nudge rides `--sdd` and
|
|
618
|
+
// self-heals (strip on `--no-sdd`).
|
|
619
|
+
files.push(writeGeminiHooksEntry(loc));
|
|
620
|
+
files.push(includeGovernance ? writeGeminiSddHooksEntry(loc) : removeGeminiSddHooksEntry(loc));
|
|
621
|
+
// Slash commands (REQ-GEMINI-005), rendered from the Claude sources.
|
|
622
|
+
// Same tiering as the Claude target: governance doors ride `--sdd`.
|
|
623
|
+
const commands = writeGeminiCommandsEntries(loc, includeGovernance);
|
|
624
|
+
files.push(...commands.files);
|
|
625
|
+
// REQ-GEMINI-009: strictly opt-in. Omitting `--trust` NEVER touches the
|
|
626
|
+
// policy file — a prior `--trust` install is preserved byte-identical
|
|
627
|
+
// (parity with REQ-INTEG-001.A3).
|
|
628
|
+
const notes = [CAPABILITY_NOTE, ...commands.notes];
|
|
629
|
+
if (opts.trust === true) {
|
|
630
|
+
files.push(writeGeminiPolicyEntry());
|
|
631
|
+
notes.push(TRUST_POLICY_NOTE);
|
|
632
|
+
}
|
|
633
|
+
if (loc === 'local')
|
|
634
|
+
notes.push(LOCAL_TRUST_NOTE);
|
|
635
|
+
return { files, notes };
|
|
167
636
|
}
|
|
168
637
|
/**
|
|
169
638
|
* Removes only the `mcpServers.specship` key (REQ-GEMINI-002.A2): sibling
|
|
@@ -172,16 +641,44 @@ class GeminiCliTarget {
|
|
|
172
641
|
* rather than throwing.
|
|
173
642
|
*/
|
|
174
643
|
uninstall(loc) {
|
|
644
|
+
const files = [];
|
|
175
645
|
const file = settingsPath(loc);
|
|
176
646
|
const config = (0, shared_1.readJsonFile)(file);
|
|
177
647
|
if (!config.mcpServers?.specship) {
|
|
178
|
-
|
|
648
|
+
files.push({ path: file, action: 'not-found' });
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
delete config.mcpServers.specship;
|
|
652
|
+
if (Object.keys(config.mcpServers).length === 0)
|
|
653
|
+
delete config.mcpServers;
|
|
654
|
+
(0, shared_1.writeJsonFile)(file, config);
|
|
655
|
+
files.push({ path: file, action: 'removed' });
|
|
179
656
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
657
|
+
// Hooks (REQ-GEMINI-010.A4) — strip both tiers so uninstall reverses
|
|
658
|
+
// either install path. Each helper is surgical: user-written sibling
|
|
659
|
+
// hooks survive byte-identical.
|
|
660
|
+
const hookCleanup = removeGeminiHooksEntry(loc);
|
|
661
|
+
if (hookCleanup.action === 'removed')
|
|
662
|
+
files.push(hookCleanup);
|
|
663
|
+
const sddHookCleanup = removeGeminiSddHooksEntry(loc);
|
|
664
|
+
if (sddHookCleanup.action === 'removed')
|
|
665
|
+
files.push(sddHookCleanup);
|
|
666
|
+
// Strip both GEMINI.md blocks regardless of which tier was selected on
|
|
667
|
+
// install, so uninstall fully reverses either install path
|
|
668
|
+
// (REQ-GEMINI-004.A2). `removeMarkedSection` deletes the file iff
|
|
669
|
+
// removal leaves it empty, so a user-authored GEMINI.md is preserved.
|
|
670
|
+
files.push(removeGeminiInstructionsEntry(loc));
|
|
671
|
+
files.push(removeGeminiSddEntry(loc));
|
|
672
|
+
// Slash commands (REQ-GEMINI-005.A2) — only our marked TOMLs go.
|
|
673
|
+
files.push(...removeGeminiCommandsEntries(loc));
|
|
674
|
+
// REQ-GEMINI-009.A4: strip our policy TOML iff it carries our header
|
|
675
|
+
// marker — a user-authored specship.toml of the same name is left
|
|
676
|
+
// byte-identical. Sibling files in the policies directory survive, and
|
|
677
|
+
// the admin policies dir is never written or read.
|
|
678
|
+
const policyCleanup = removeGeminiPolicyEntry();
|
|
679
|
+
if (policyCleanup.action !== 'not-found')
|
|
680
|
+
files.push(policyCleanup);
|
|
681
|
+
return { files };
|
|
185
682
|
}
|
|
186
683
|
/**
|
|
187
684
|
* The snippet, verbatim-pasteable into `mcpServers`. Pure string building —
|
|
@@ -192,7 +689,7 @@ class GeminiCliTarget {
|
|
|
192
689
|
return `# Add to ${settingsPath(loc)}\n\n${snippet}\n`;
|
|
193
690
|
}
|
|
194
691
|
describePaths(loc) {
|
|
195
|
-
return [settingsPath(loc)];
|
|
692
|
+
return [settingsPath(loc), geminiMdPath(loc), geminiCommandsDir(loc)];
|
|
196
693
|
}
|
|
197
694
|
}
|
|
198
695
|
exports.GeminiCliTarget = GeminiCliTarget;
|