showdar-skills 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +82 -0
- package/bin/showdar.js +40 -3
- package/package.json +1 -1
- package/router/skill-map.yaml +1 -1
- package/skills/showdar-git/SKILL.md +25 -1
- package/src/adapter-renderers.js +9 -1
- package/src/git-start.js +87 -0
- package/src/project.js +26 -31
- package/src/runtime-route.js +99 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.12.0]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Canonical `showdar route --stdin|--prompt [--json]` runtime bridge projecting lifecycle primary, advisors, the seven public Intent keys, advisory domain matches, and installed/missing skill availability.
|
|
14
|
+
- Specialized domain discovery overlay using deterministic Unicode phrase matching over `router/skill-map.yaml` inline trigger arrays; insurance terminology, underwriting/rating flows, and insurer API/UI review map to `showdar-insurance-domain`, `showdar-insurance-workflows`, and `showdar-insurance-review` without granting authority.
|
|
15
|
+
- `showdar git-start --type --name [--base] [--dry-run] [--json]` for deterministic safe local task-branch preparation without inferred staging, commit, merge, or push.
|
|
16
|
+
- Task branch isolation guidance: branch-before-edit on integration/default branches, repository-policy priority, one branch per coherent task, and conservative dirty-worktree blocking.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Managed harness instructions and commands invoke the canonical runtime router with native/static fallback when the CLI is unavailable.
|
|
21
|
+
- Init/add guidance reflects the complete installed set; add refreshes existing commands and instructions even when the skill is already installed.
|
|
22
|
+
- Showdar Git documents one branch per coherent task, repository-policy priority, and explicit completion/integration authority.
|
|
23
|
+
- Task completion defaults to verify and report; merge and push each require their own explicit authority.
|
|
24
|
+
|
|
25
|
+
### Safety / Compatibility
|
|
26
|
+
|
|
27
|
+
- Domain discovery does not grant authority and never replaces the lifecycle primary or rewrites Intent/mutation.
|
|
28
|
+
- Phase 6G authority, state, evidence, trace, and manifest contracts unchanged; no engine vendoring into projects.
|
|
29
|
+
- No implicit branch commit/merge/push; no automatic stash, reset, restore, or clean.
|
|
30
|
+
- Existing v0.11 projects remain compatible; no migration required.
|
|
31
|
+
|
|
9
32
|
## [0.11.0]
|
|
10
33
|
|
|
11
34
|
### Added
|
package/README.md
CHANGED
|
@@ -40,6 +40,88 @@ Claude Code as supported installation targets. Choose `backend`, `qa`, or
|
|
|
40
40
|
`product` when that gives discovery a more precise context; use `full` when
|
|
41
41
|
you want all capabilities available.
|
|
42
42
|
|
|
43
|
+
## Runtime routing and task branches
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
printf '%s\n' 'Implement the approved underwriting form and add tests' | showdar route --stdin --json
|
|
47
|
+
showdar route --prompt "Explain insurance terminology"
|
|
48
|
+
showdar git-start --dry-run --type feature --name "IDP-123 Add pricing form"
|
|
49
|
+
showdar git-start --type feature --name "IDP-123 Add pricing form"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Prefer `--stdin` for arbitrary or multiline prompts. Supply stdin as literal data;
|
|
53
|
+
never interpolate request text into a shell command. Both commands support `--json`
|
|
54
|
+
with a schemaVersion 1 envelope (`command`, `ok`, `data`, `warnings`, `errors`).
|
|
55
|
+
|
|
56
|
+
`route` is read-only. It consumes the existing Phase 6G authority resolver and thin
|
|
57
|
+
route, projecting exactly the seven public Intent keys plus lifecycle primary and
|
|
58
|
+
advisors. It never exposes internal capabilities, frames or authority diagnostics.
|
|
59
|
+
It neither authorizes work nor creates a branch. It preserves canonical advisor
|
|
60
|
+
semantics: a security-review phrase is not guaranteed to become a security advisor.
|
|
61
|
+
|
|
62
|
+
Domain discovery is a separate advisory overlay using the canonical
|
|
63
|
+
`router/skill-map.yaml` inline trigger arrays and grouped domain catalog metadata.
|
|
64
|
+
It uses deterministic Unicode phrase matching, not lifecycle scoring. Insurance
|
|
65
|
+
terminology can recommend `showdar-insurance-domain`, underwriting/rating business
|
|
66
|
+
flows `showdar-insurance-workflows`, and insurer API/UI terminology review
|
|
67
|
+
`showdar-insurance-review`. These skills do not replace lifecycle primaryCapability,
|
|
68
|
+
rewrite Intent/mutation, or grant authority. The existing ASCII/token ranking
|
|
69
|
+
helper is unsuitable for Vietnamese phrase discovery. No duplicate trigger table
|
|
70
|
+
or second authority engine is introduced.
|
|
71
|
+
|
|
72
|
+
When `.showdar.json` exists, availability checks managed native skill files and
|
|
73
|
+
recorded, still-present globally satisfied skills. Missing lifecycle primary,
|
|
74
|
+
advisors and domain matches are reported separately with `showdar add <name>`
|
|
75
|
+
suggestions; another installed skill never substitutes for the canonical route.
|
|
76
|
+
Without a project manifest, `managed` is false and installed/missing fields are
|
|
77
|
+
null (unknown), not fabricated. Invalid manifests fail explicitly. Explicit named
|
|
78
|
+
installed skills may load directly; this adds no mutation authority.
|
|
79
|
+
|
|
80
|
+
Managed project guidance invokes `showdar route --stdin --json` for automatic
|
|
81
|
+
selection, then loads the installed lifecycle skill, domain overlays and advisors.
|
|
82
|
+
The runtime bridge requires the `showdar` CLI in the harness environment. If it is
|
|
83
|
+
unavailable, native skill descriptions/static discovery are the fallback. No npm
|
|
84
|
+
install, npx fetch, package dependency mutation or remote download happens.
|
|
85
|
+
Projects receive only existing skills/commands/instructions/manifest; no wholesale
|
|
86
|
+
`src/`, `engine/` or `router/` copy. Init and add regenerate complete installed-set
|
|
87
|
+
guidance, including re-adding an existing skill. Global installs keep native discovery
|
|
88
|
+
and do not write project AGENTS.md or CLAUDE.md.
|
|
89
|
+
|
|
90
|
+
The four workflows remain native discoverable/installable skills; `route` does not
|
|
91
|
+
introduce workflow selection. Profiles remain primitive-only: `full` contains all
|
|
92
|
+
18 primitives, and the insurance profile remains unchanged.
|
|
93
|
+
|
|
94
|
+
Before the first local-write task source edit on develop/development/dev/main/master
|
|
95
|
+
or the repository default/integration branch, prepare a task branch. Priority is
|
|
96
|
+
repository instructions/documented convention, explicit current user instruction,
|
|
97
|
+
clearly detected convention, then Showdar defaults. Explicit trunk/direct-work policy
|
|
98
|
+
wins. One coherent task uses one branch across plan/build/test/review. Completion
|
|
99
|
+
means verify and report; commit, merge and push need their own authority.
|
|
100
|
+
|
|
101
|
+
`git-start` prepares local branch state only. Types: feature (new capability), fix
|
|
102
|
+
(defect), refactor (internal structure), test, docs, chore (tooling/config), release,
|
|
103
|
+
hotfix (explicit urgent production repair). Slugs are lowercase, hyphenated, ticket
|
|
104
|
+
preserving and bounded; task text is passed as data and never executed.
|
|
105
|
+
|
|
106
|
+
Base resolution uses explicit `--base`, repository-local `showdar.gitBase`, established
|
|
107
|
+
local develop/development/dev, then local default/main/master. The helper does not
|
|
108
|
+
parse prose instructions or create develop. Read policy first and supply its base
|
|
109
|
+
when convention differs. Local Git config `showdar.gitBranchPrefix` supports custom
|
|
110
|
+
prefixes; `showdar.gitDirectWork=true` records an explicit direct-work convention.
|
|
111
|
+
Hotfix requires a convention via `--base` or `showdar.gitBase`.
|
|
112
|
+
|
|
113
|
+
Dry-run validates Git state/type/slug/base/collision and changes no refs. Execution
|
|
114
|
+
revalidates, then creates/switches only a task branch. Already on the matching branch
|
|
115
|
+
is a no-op; another task branch is blocked. An existing target is reused automatically
|
|
116
|
+
only when its tip equals both current HEAD and base; divergent/ambiguous targets stop
|
|
117
|
+
without overwriting history. Detached HEAD and active Git operations stop.
|
|
118
|
+
|
|
119
|
+
Dirty worktrees are blocked before any switch: the helper cannot prove ownership.
|
|
120
|
+
Confirmed current-task-only changes may use equivalent repository-specific branch
|
|
121
|
+
preparation after explicit ownership inspection. No automatic stash/reset/restore/clean,
|
|
122
|
+
staging, commit, merge or push occurs. Branch preparation itself does not authorize
|
|
123
|
+
source edits.
|
|
124
|
+
|
|
43
125
|
## Why Showdar?
|
|
44
126
|
|
|
45
127
|
- **18 focused primitive skills** plus 4 adaptive workflow skills (22 installable), including an opt-in insurance domain profile.
|
package/bin/showdar.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { startTaskBranch, formatGitStart } from '../src/git-start.js';
|
|
5
|
+
import { routeRequest, formatRoute } from '../src/runtime-route.js';
|
|
3
6
|
import { homedir } from 'node:os';
|
|
4
7
|
import { readFile } from 'node:fs/promises';
|
|
5
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -33,6 +36,14 @@ function scopeAfter(args) {
|
|
|
33
36
|
|
|
34
37
|
function printHelp(version, command = null) {
|
|
35
38
|
const scopeUsage = '[--scope <project|global>]';
|
|
39
|
+
if (command === 'route') {
|
|
40
|
+
console.log('Usage: showdar route (--stdin | --prompt <text>) [--json]');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (command === 'git-start') {
|
|
44
|
+
console.log('Usage: showdar git-start --type <type> --name <task> [--base <branch>] [--dry-run] [--json]');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
36
47
|
if (command === 'init') {
|
|
37
48
|
console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>]\n\nDefaults: scope project, profile full, AI target universal.\nProject scope writes native skills, one native instruction surface, and project .showdar.json. Global scope writes verified user skill directories and ~/.showdar/global.json without instruction files. Codex and universal use .agents/skills in project scope and ~/.agents/skills in global scope; cursor uses .cursor/skills in project scope and ~/.cursor/skills in global scope; --ai all writes each shared destination once, generates OpenCode and Claude commands, and writes only the AGENTS.md block.\n\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
|
|
38
49
|
return;
|
|
@@ -45,7 +56,7 @@ function printHelp(version, command = null) {
|
|
|
45
56
|
console.log(`Showdar Skills ${version}\n\nUsage:\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n\nExamples:\n showdar add debug\n showdar add showdar-security\n showdar add test --ai cursor\n showdar add review --scope global --ai claude\n\nDefault scope: project. Default AI target: universal, or the configured .showdar.json value when present.`);
|
|
46
57
|
return;
|
|
47
58
|
}
|
|
48
|
-
console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>] [--pack <local-path>]\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]\n showdar validate-pack <local-path> [--json]\n showdar inspect-pack <local-path> [--json] [--checkpoint <file>]\n showdar doctor ${scopeUsage} [--extensions] [--json]\n showdar update-pack <local-path> [--dry-run] [--json]\n showdar validate\n showdar list [--extensions] [--json]\n showdar remove ${scopeUsage}\n\nExtension packs accept local directories/workspace paths only; tarball, URL, Git, and registry sources are rejected.\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
|
|
59
|
+
console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>] [--pack <local-path>]\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n showdar route (--stdin | --prompt <text>) [--json]\n showdar git-start --type <type> --name <task> [--base <branch>] [--dry-run] [--json]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]\n showdar validate-pack <local-path> [--json]\n showdar inspect-pack <local-path> [--json] [--checkpoint <file>]\n showdar doctor ${scopeUsage} [--extensions] [--json]\n showdar update-pack <local-path> [--dry-run] [--json]\n showdar validate\n showdar list [--extensions] [--json]\n showdar remove ${scopeUsage}\n\nExtension packs accept local directories/workspace paths only; tarball, URL, Git, and registry sources are rejected.\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
async function main() {
|
|
@@ -59,10 +70,33 @@ async function main() {
|
|
|
59
70
|
console.log(version);
|
|
60
71
|
return;
|
|
61
72
|
}
|
|
62
|
-
if (args.includes('--help') || args.includes('-h')) return printHelp(version, command);
|
|
73
|
+
if (!['route', 'git-start'].includes(command) && (args.includes('--help') || args.includes('-h'))) return printHelp(version, command);
|
|
63
74
|
|
|
64
75
|
const scope = ['init', 'status', 'doctor', 'remove', 'add'].includes(command) ? scopeAfter(args) : null;
|
|
65
76
|
|
|
77
|
+
if (command === 'git-start') {
|
|
78
|
+
const { values } = parseArgs({ args: args.slice(1), options: { type: { type: 'string' }, name: { type: 'string' }, base: { type: 'string' }, 'dry-run': { type: 'boolean' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' } } });
|
|
79
|
+
if (values.help) return printHelp(version, command);
|
|
80
|
+
const data = startTaskBranch({ cwd: projectRoot, type: values.type, name: values.name, base: values.base, dryRun: values['dry-run'] });
|
|
81
|
+
console.log(values.json ? JSON.stringify({ schemaVersion: 1, command, ok: true, data, warnings: [], errors: [] }, null, 2) : formatGitStart(data));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (command === 'route') {
|
|
86
|
+
const { values } = parseArgs({ args: args.slice(1), options: { stdin: { type: 'boolean' }, prompt: { type: 'string' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' } } });
|
|
87
|
+
if (values.help) return printHelp(version, command);
|
|
88
|
+
if (Boolean(values.stdin) === (values.prompt !== undefined)) throw new Error('Select exactly one of --stdin or --prompt.');
|
|
89
|
+
let prompt = values.prompt;
|
|
90
|
+
if (values.stdin) {
|
|
91
|
+
process.stdin.setEncoding('utf8');
|
|
92
|
+
prompt = '';
|
|
93
|
+
for await (const chunk of process.stdin) prompt += chunk;
|
|
94
|
+
}
|
|
95
|
+
const data = await routeRequest({ prompt, cwd: projectRoot, packageRoot });
|
|
96
|
+
console.log(values.json ? JSON.stringify({ schemaVersion: 1, command, ok: true, data, warnings: [], errors: [] }, null, 2) : formatRoute(data));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
66
100
|
if (command === 'list') {
|
|
67
101
|
if (args.includes('--extensions')) {
|
|
68
102
|
const isJson = args.includes('--json');
|
|
@@ -342,6 +376,9 @@ async function main() {
|
|
|
342
376
|
}
|
|
343
377
|
|
|
344
378
|
main().catch((error) => {
|
|
345
|
-
|
|
379
|
+
const command = process.argv[2];
|
|
380
|
+
if (['route', 'git-start'].includes(command) && process.argv.slice(3).includes('--json')) {
|
|
381
|
+
console.log(JSON.stringify({ schemaVersion: 1, command, ok: false, data: null, warnings: [], errors: [{ code: 'INVALID_REQUEST', message: error.message }] }, null, 2));
|
|
382
|
+
} else console.error(`showdar: ${error.message}`);
|
|
346
383
|
process.exitCode = 1;
|
|
347
384
|
});
|
package/package.json
CHANGED
package/router/skill-map.yaml
CHANGED
|
@@ -38,7 +38,7 @@ routes:
|
|
|
38
38
|
triggers: ["resume interrupted implementation", "recover partial work", "session stopped", "continue partial work", "missing context"]
|
|
39
39
|
skill: showdar-recover
|
|
40
40
|
git:
|
|
41
|
-
triggers: ["commit", "stage only task files", "merge branch", "rebase branch", "cherry-pick commit", "git conflict", "push branch", "clean merged branches", "finish git workflow"]
|
|
41
|
+
triggers: ["commit", "stage only task files", "merge branch", "rebase branch", "cherry-pick commit", "git conflict", "push branch", "clean merged branches", "finish git workflow", "start task branch", "create feature branch", "create fix branch", "branch for this task"]
|
|
42
42
|
skill: showdar-git
|
|
43
43
|
requirements:
|
|
44
44
|
triggers: ["requirements", "business rules", "acceptance criteria", "user stories", "use cases", "requirement matrix", "missing requirement", "ambiguity"]
|
|
@@ -46,6 +46,29 @@ This skill owns repository state transitions, not source-code review or GitHub a
|
|
|
46
46
|
- Push, tag, remote deletion, pull with mutation, GitHub operations, and release actions require explicit intent.
|
|
47
47
|
- Report exactly what changed after each Git mutation.
|
|
48
48
|
|
|
49
|
+
## Task branch isolation
|
|
50
|
+
|
|
51
|
+
Before the first task-owned source edit for a local-write engineering task, inspect Git state (`git status --short`, current branch, branch history and repository policy). Direct develop/main edits are not the default: on develop, development, dev, main, master or the default/integration branch, prepare a dedicated task branch before source mutation.
|
|
52
|
+
|
|
53
|
+
Priority: repository instructions/documented branching convention; explicit current user instruction; clearly detected existing convention; Showdar safe default. Explicit trunk-based/direct-work policy wins: do not impose GitFlow. Branch preparation does not authorize source mutation.
|
|
54
|
+
|
|
55
|
+
Use one branch per coherent task across understand, requirements, plan, design, build, test and review. Continue the current task branch; never create a branch per skill or nested branches.
|
|
56
|
+
|
|
57
|
+
Default types: new user-visible capability → `feature`; observed defect → `fix`; internal restructuring → `refactor`; tests only → `test`; documentation only → `docs`; tooling/config/maintenance → `chore`; release preparation → `release`; explicit urgent production repair → `hotfix`. Names use a bounded lowercase hyphen slug preserving ticket keys.
|
|
58
|
+
|
|
59
|
+
Prefer established local develop for normal work; otherwise the repository default/integration branch. Do not create develop automatically. Explicit hotfix follows repository convention. Read prose policy yourself; the helper does not parse repository instructions. Supply `--base` or repository-local Git config `showdar.gitBase`, `showdar.gitBranchPrefix`, `showdar.gitDirectWork` for deterministic custom conventions.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
showdar git-start --dry-run --type feature --name "Task description"
|
|
63
|
+
showdar git-start --type feature --name "Task description"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The helper revalidates before creating/switching and never edits source. A clean worktree can branch. Confirmed current-task changes may be carried only through equivalent repository-specific Git preparation with proven ownership and Git permission; the helper conservatively blocks dirty changes before switching because it cannot prove ownership. Unrelated/ambiguous changes → STOP. No hidden stash, reset, restore or clean.
|
|
67
|
+
|
|
68
|
+
Existing same-name branches must be inspected. The helper reuses only an unused branch with the same tip as both current HEAD and base; an already-active matching branch is a continue/no-op. Any other collision → STOP; never reset or overwrite branch history. Already on another task branch → inspect task identity before switching.
|
|
69
|
+
|
|
70
|
+
Default completion: task branch → verify → report. No inferred commit, merge or push. Merge only when explicitly requested or clearly included in current repository/user task authority; push remains independently explicit.
|
|
71
|
+
|
|
49
72
|
## Workflow
|
|
50
73
|
|
|
51
74
|
### 1. Inspect Git state
|
|
@@ -273,7 +296,8 @@ report -> SHA, staged paths, exclusions, and clean/dirty final state
|
|
|
273
296
|
### Feature completion without push
|
|
274
297
|
|
|
275
298
|
```text
|
|
276
|
-
|
|
299
|
+
task branch -> verify -> report
|
|
300
|
+
selective commit only when requested; merge only with current task authority
|
|
277
301
|
remote -> not touched because no push request was given
|
|
278
302
|
```
|
|
279
303
|
|
package/src/adapter-renderers.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { ALL_SKILLS } from './catalog.js';
|
|
2
2
|
|
|
3
|
+
const RUNTIME_GUIDANCE = `Automatic Showdar selection: route the current request through \`showdar route --stdin --json\` when the Showdar CLI is available. Supply the prompt as literal stdin data, never as interpolated shell syntax. Honor the canonical lifecycle primary; do not substitute another installed skill when it is missing. Report missing skills and suggest \`showdar add <name>\`; never auto-install. Include installed domain matches only as specialized context overlays; load returned advisors only when installed. Domain discovery does not grant authority or replace the lifecycle route.
|
|
4
|
+
Explicit named-skill requests may load that installed skill directly without automatic routing. Workflow skills remain native discoverable choices; the router does not select workflows. If the CLI is unavailable, use native skill discovery/static descriptions below. Do not fetch a CLI through npx or install dependencies automatically.
|
|
5
|
+
Routing does not authorize mutation. Inspect the returned mutation class and current task authority before work. For local-write tasks, before the first task-owned source edit inspect Git state. On develop, development, dev, main, master or the repository default/integration branch, DO NOT begin source edits yet: prepare one branch per coherent task first. Follow repository instructions/documented convention, explicit current user instruction, clearly detected convention, then the Showdar safe default. Explicit trunk/direct-work policy wins.
|
|
6
|
+
Use \`showdar git-start --dry-run --type <type> --name <task>\`, then \`showdar git-start --type <type> --name <task>\`, or equivalent repository-specific preparation. Confirm the task branch before mutating skill execution. Reuse that branch across plan/build/test/review; do not nest branches per skill. Dirty ownership or branch collisions require inspection; never infer stash, reset, restore or clean. Git-start does not authorize source mutation, commit, merge or push. Completion defaults to verify and report on the task branch; merge and push require their own explicit authority.`;
|
|
7
|
+
|
|
3
8
|
const CANONICAL_ROUTE_ORDER = [
|
|
4
9
|
['map repository architecture, dependencies, or impact', 'showdar-understand'],
|
|
5
10
|
['plan implementation of agreed behavior and scope', 'showdar-plan'],
|
|
@@ -37,7 +42,7 @@ export function renderShowdarInstruction(skillIds) {
|
|
|
37
42
|
routes.push(`- ${getSkillDescription(id)} -> \`${id}\``);
|
|
38
43
|
}
|
|
39
44
|
}
|
|
40
|
-
return `${routes.join('\n')}\n`;
|
|
45
|
+
return `${routes.join('\n')}\n\n${RUNTIME_GUIDANCE}\n`;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
export function renderShowdarCommand(skillId) {
|
|
@@ -48,6 +53,7 @@ description: ${description}
|
|
|
48
53
|
|
|
49
54
|
Load and follow \`${skillId}\`.
|
|
50
55
|
Ground in the current repository.
|
|
56
|
+
${RUNTIME_GUIDANCE}
|
|
51
57
|
Request: \$ARGUMENTS
|
|
52
58
|
`;
|
|
53
59
|
}
|
|
@@ -60,6 +66,8 @@ description: Invoke a specific Showdar flagship skill explicitly
|
|
|
60
66
|
---
|
|
61
67
|
Select exactly one requested Showdar skill and follow it: ${ids}. Whole-task intent (complete feature, end-to-end fix, release lifecycle, active incident) selects a workflow; single primitive intent stays primitive. If the requested name is ambiguous, choose the smallest matching skill from this list and say which one was selected.
|
|
62
68
|
|
|
69
|
+
${RUNTIME_GUIDANCE}
|
|
70
|
+
|
|
63
71
|
Request: \$ARGUMENTS
|
|
64
72
|
`;
|
|
65
73
|
}
|
package/src/git-start.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const TASK_BRANCH_TYPES = ['feature', 'fix', 'refactor', 'test', 'docs', 'chore', 'release', 'hotfix'];
|
|
6
|
+
|
|
7
|
+
function git(cwd, args, optional = false) {
|
|
8
|
+
try { return execFileSync('git', ['--no-optional-locks', ...args], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10000 }).trim(); }
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (optional && error.status === 1) return null;
|
|
11
|
+
throw new Error(`Git operation failed (${args[0]}): ${error.stderr?.toString().trim() || error.message}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function taskSlug(name, type) {
|
|
16
|
+
if (typeof name !== 'string' || !name.trim()) throw new Error('--name requires a task name.');
|
|
17
|
+
const words = name.normalize('NFKD').replace(/\p{M}/gu, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').split('-');
|
|
18
|
+
// Drop a redundant task verb, including one following a useful ticket key.
|
|
19
|
+
const offset = /^[a-z]+$/.test(words[0]) && /^\d+$/.test(words[1] ?? '') ? 2 : 0;
|
|
20
|
+
if ([type, 'add'].includes(words[offset])) words.splice(offset, 1);
|
|
21
|
+
const slug = words.join('-').slice(0, 80).replace(/-+$/g, '');
|
|
22
|
+
if (!slug) throw new Error('Task name contains no safe branch characters.');
|
|
23
|
+
return slug;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function inspect({ cwd, type, name, base }) {
|
|
27
|
+
if (!TASK_BRANCH_TYPES.includes(type)) throw new Error(`Invalid task type. Expected ${TASK_BRANCH_TYPES.join(', ')}.`);
|
|
28
|
+
const root = git(cwd, ['rev-parse', '--show-toplevel']);
|
|
29
|
+
const current = git(root, ['symbolic-ref', '--quiet', '--short', 'HEAD'], true);
|
|
30
|
+
if (!current) throw new Error('Detached HEAD: choose a repository branch before git-start.');
|
|
31
|
+
const head = git(root, ['rev-parse', 'HEAD']);
|
|
32
|
+
const dirty = git(root, ['status', '--porcelain=v1', '--untracked-files=all']);
|
|
33
|
+
for (const marker of ['MERGE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD', 'rebase-merge', 'rebase-apply', 'BISECT_LOG', 'sequencer']) {
|
|
34
|
+
const file = git(root, ['rev-parse', '--git-path', marker]);
|
|
35
|
+
if (existsSync(path.resolve(root, file))) throw new Error(`Active Git operation: ${marker}.`);
|
|
36
|
+
}
|
|
37
|
+
const config = key => git(root, ['config', '--local', '--get', key], true);
|
|
38
|
+
const prefix = config('showdar.gitBranchPrefix') ?? type;
|
|
39
|
+
const branch = `${prefix}/${taskSlug(name, type)}`;
|
|
40
|
+
git(root, ['check-ref-format', '--branch', branch]);
|
|
41
|
+
if (branch.startsWith('-')) throw new Error('Invalid task branch.');
|
|
42
|
+
const configuredBase = config('showdar.gitBase');
|
|
43
|
+
const defaultRef = git(root, ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], true)?.replace(/^origin\//, '');
|
|
44
|
+
const localBranch = value => value && !value.startsWith('-') && git(root, ['show-ref', '--verify', '--quiet', `refs/heads/${value}`], true) !== null;
|
|
45
|
+
if (base && !localBranch(base)) throw new Error('Explicit --base must be an existing local branch.');
|
|
46
|
+
if (configuredBase && !localBranch(configuredBase)) throw new Error('Configured showdar.gitBase must be an existing local branch.');
|
|
47
|
+
if (type === 'hotfix' && !base && !configuredBase) throw new Error('Hotfix requires --base or repository showdar.gitBase convention.');
|
|
48
|
+
const resolvedBase = base ?? configuredBase ?? ['develop', 'development', 'dev', defaultRef, 'main', 'master'].find(localBranch);
|
|
49
|
+
if (!resolvedBase) throw new Error('Cannot infer integration base; use --base with the repository convention.');
|
|
50
|
+
const baseHead = git(root, ['rev-parse', `refs/heads/${resolvedBase}`]);
|
|
51
|
+
const integration = new Set(['develop', 'development', 'dev', 'main', 'master', defaultRef, resolvedBase]);
|
|
52
|
+
let action;
|
|
53
|
+
if (current === branch) action = 'continue';
|
|
54
|
+
else {
|
|
55
|
+
if (dirty) throw new Error('Dirty worktree ownership is ambiguous. Preserve changes; git-start never stashes or carries unproven task changes.');
|
|
56
|
+
if (!integration.has(current)) throw new Error('Already on another task branch; continue that coherent task or select its integration branch explicitly.');
|
|
57
|
+
const direct = config('showdar.gitDirectWork');
|
|
58
|
+
if (direct !== null && !['true', 'false'].includes(direct)) throw new Error('showdar.gitDirectWork must be true or false.');
|
|
59
|
+
if (direct === 'true') action = 'direct';
|
|
60
|
+
else if (localBranch(branch)) {
|
|
61
|
+
const targetHead = git(root, ['rev-parse', `refs/heads/${branch}`]);
|
|
62
|
+
// Equal tips are the only automatically provable safe unused branch.
|
|
63
|
+
if (targetHead !== head || targetHead !== baseHead) throw new Error('Existing task branch collision: history/ownership is ambiguous.');
|
|
64
|
+
action = 'switch';
|
|
65
|
+
} else {
|
|
66
|
+
if (baseHead !== head && git(root, ['merge-base', '--is-ancestor', head, baseHead], true) === null) throw new Error('Current branch diverges from base; inspect repository history before branching.');
|
|
67
|
+
action = 'create';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { root, current, head, base: resolvedBase, baseHead, branch, action, dirty: dirty ? 'present' : 'clean' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function startTaskBranch(options) {
|
|
74
|
+
const proposal = inspect(options);
|
|
75
|
+
if (options.dryRun || ['continue', 'direct'].includes(proposal.action)) return { ...proposal, dryRun: Boolean(options.dryRun) };
|
|
76
|
+
const fresh = inspect(options);
|
|
77
|
+
if (JSON.stringify(fresh) !== JSON.stringify(proposal)) throw new Error('Git state changed during preflight; retry after inspection.');
|
|
78
|
+
if (fresh.action === 'create') git(fresh.root, ['switch', '-c', fresh.branch, fresh.baseHead]);
|
|
79
|
+
else git(fresh.root, ['switch', fresh.branch]);
|
|
80
|
+
const current = git(fresh.root, ['branch', '--show-current']);
|
|
81
|
+
if (current !== fresh.branch) throw new Error('Task branch switch was not confirmed.');
|
|
82
|
+
return { ...fresh, dryRun: false, current };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatGitStart(data) {
|
|
86
|
+
return `Task branch: ${data.branch}\nBase: ${data.base}\nAction: ${data.action}${data.dryRun ? ' (dry-run)' : ''}\nWorktree: ${data.dirty}\nBranch preparation does not authorize source edits, commit, merge or push.`;
|
|
87
|
+
}
|
package/src/project.js
CHANGED
|
@@ -588,6 +588,23 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
|
|
|
588
588
|
if (effectiveScope !== 'project' && effectiveScope !== 'global') throw new Error(`Unknown scope "${effectiveScope}".`);
|
|
589
589
|
|
|
590
590
|
if (existingManifest?.skills?.includes(skillId)) {
|
|
591
|
+
const refreshedFiles = [];
|
|
592
|
+
const priorOwned = ownedPathSet(existingManifest);
|
|
593
|
+
for (const target of existingManifest.commandHarness ?? []) {
|
|
594
|
+
const commandRoot = effectiveScope === 'global' ? globalCommandRootForTarget(target, { homeRoot: home }) : commandRootFor(target, cwd);
|
|
595
|
+
if (commandRoot) await generateCommandFiles({ baseRoot, skillIds: existingManifest.skills, target, commandRoot, priorOwned, newFiles: refreshedFiles });
|
|
596
|
+
}
|
|
597
|
+
if (effectiveScope === 'project' && existingManifest.instructions) {
|
|
598
|
+
const instruction = existingManifest.instructions;
|
|
599
|
+
const file = path.join(cwd, instruction.file);
|
|
600
|
+
if (instruction.kind === 'block') await writeManagedBlock(file, existingManifest.skills);
|
|
601
|
+
else if (instruction.kind === 'file') await writeCursorRule(file, existingManifest.skills);
|
|
602
|
+
}
|
|
603
|
+
if (refreshedFiles.length) {
|
|
604
|
+
const files = new Map(existingManifest.files.map(f => [f.path, f]));
|
|
605
|
+
for (const f of refreshedFiles) files.set(f.path, f);
|
|
606
|
+
await writeJsonAtomic(effectiveScope === 'global' ? globalManifestPath(home) : path.join(cwd, PROJECT_MANIFEST), { ...existingManifest, files: [...files.values()] });
|
|
607
|
+
}
|
|
591
608
|
return { skill: skillId, root: '', destination: '', added: false, scope: effectiveScope, ai: effectiveAi, profile: existingManifest.profile };
|
|
592
609
|
}
|
|
593
610
|
|
|
@@ -629,41 +646,19 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
|
|
|
629
646
|
}
|
|
630
647
|
}
|
|
631
648
|
|
|
649
|
+
const allSkillIds = [...new Set([...(existingManifest?.skills ?? []), skillId])];
|
|
632
650
|
const newCommands = [];
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
}
|
|
640
|
-
await mkdir(path.dirname(dest), { recursive: true });
|
|
641
|
-
const content = renderShowdarCommand(skillId);
|
|
642
|
-
await writeTextAtomic(dest, content);
|
|
643
|
-
files.push({ path: rel, hash: await hashTree(dest) });
|
|
644
|
-
newCommands.push({ target, name: shortName, path: rel });
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
if (commandHarnesses.length > 0) {
|
|
648
|
-
for (const { target, root } of commandHarnesses) {
|
|
649
|
-
const aggregatorDest = path.join(root, 'skill.md');
|
|
650
|
-
const aggregatorRel = manifestPathFor(baseRoot, aggregatorDest);
|
|
651
|
-
const addAllSkillIds = [...new Set([...(existingManifest?.skills ?? []), skillId])];
|
|
652
|
-
const aggregatorContent = renderShowdarAggregator(addAllSkillIds);
|
|
653
|
-
if ((await exists(aggregatorDest)) && !priorOwned.has(aggregatorRel)) {
|
|
654
|
-
throw new Error(`Refusing to overwrite existing non-Showdar-managed command: ${aggregatorDest}`);
|
|
655
|
-
}
|
|
656
|
-
await mkdir(path.dirname(aggregatorDest), { recursive: true });
|
|
657
|
-
await writeTextAtomic(aggregatorDest, aggregatorContent);
|
|
658
|
-
files.push({ path: aggregatorRel, hash: await hashTree(aggregatorDest) });
|
|
659
|
-
newCommands.push({ target, name: 'skill', path: aggregatorRel });
|
|
660
|
-
}
|
|
651
|
+
const harnessTargets = [...new Set([...(existingManifest?.commandHarness ?? []), ...commandHarnesses.map(c => c.target)])];
|
|
652
|
+
for (const target of harnessTargets) {
|
|
653
|
+
const root = effectiveScope === 'global' ? globalCommandRootForTarget(target, { homeRoot: home }) : commandRootFor(target, cwd);
|
|
654
|
+
if (!root) continue;
|
|
655
|
+
const generated = await generateCommandFiles({ baseRoot, skillIds: allSkillIds, target, commandRoot: root, priorOwned, newFiles: files, managedRoots });
|
|
656
|
+
newCommands.push(...generated.map(c => ({ target, name: c.shortName, path: manifestPathFor(baseRoot, c.destination) })));
|
|
661
657
|
}
|
|
662
658
|
|
|
663
659
|
const merged = new Map((existingManifest?.files ?? []).map((e) => [e.path, e]));
|
|
664
660
|
for (const f of files) merged.set(f.path, f);
|
|
665
661
|
|
|
666
|
-
const allSkillIds = [...new Set([...(existingManifest?.skills ?? []), skillId])];
|
|
667
662
|
const instructionFile = existingManifest?.instructions ?? (effectiveScope === 'project' ? instructionSurfaceFor(effectiveAi, cwd) : null);
|
|
668
663
|
const commandHarness = [...new Set([...(existingManifest?.commandHarness ?? []), ...commandHarnesses.map((c) => c.target)])];
|
|
669
664
|
|
|
@@ -673,10 +668,10 @@ export async function addSkill({ cwd, skill, ai = null, scope = null, home = hom
|
|
|
673
668
|
packageVersion,
|
|
674
669
|
profile: existingManifest?.profile ?? null,
|
|
675
670
|
ai: effectiveAi,
|
|
676
|
-
targets,
|
|
671
|
+
targets: [...new Set([...(existingManifest?.targets ?? []), ...targets])],
|
|
677
672
|
skills: allSkillIds,
|
|
678
673
|
satisfiedByGlobal: existingManifest?.satisfiedByGlobal ?? [],
|
|
679
|
-
commands: [...(existingManifest?.commands ?? []), ...newCommands],
|
|
674
|
+
commands: [...new Map([...(existingManifest?.commands ?? []), ...newCommands].map(c => [c.path, c])).values()],
|
|
680
675
|
files: [...merged.values()],
|
|
681
676
|
instructions: instructionFile ? { file: instructionFile.file, kind: instructionFile.kind } : null,
|
|
682
677
|
commandHarness,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { ALL_SKILLS, SKILLS } from './catalog.js';
|
|
5
|
+
import { resolveIntentFromPrompt } from './intent-resolver/index.js';
|
|
6
|
+
import { assertSafeManagedPath } from './path-safety.js';
|
|
7
|
+
|
|
8
|
+
async function readJson(file) {
|
|
9
|
+
try {
|
|
10
|
+
const value = JSON.parse(await readFile(file, 'utf8'));
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Manifest must be an object.');
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw new Error(`Cannot read Showdar manifest: ${error.message}`); }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function skillPresent(root, relative) {
|
|
18
|
+
const file = path.resolve(root, relative, 'SKILL.md');
|
|
19
|
+
await assertSafeManagedPath(root, file);
|
|
20
|
+
try { return (await stat(file)).isFile(); }
|
|
21
|
+
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ponytail: inline JSON trigger arrays only; use a YAML parser if the canonical format changes.
|
|
25
|
+
// The canonical map uses JSON-compatible inline trigger arrays. Read only that
|
|
26
|
+
// documented subset; this is discovery data, never an authority input.
|
|
27
|
+
export async function discoverDomain(prompt, packageRoot) {
|
|
28
|
+
const map = await readFile(path.join(packageRoot, 'router/skill-map.yaml'), 'utf8');
|
|
29
|
+
const specialized = new Set(SKILLS.filter(s => s.id !== `showdar-${s.domain}`).map(s => s.id));
|
|
30
|
+
const text = String(prompt).normalize('NFC').toLowerCase();
|
|
31
|
+
const matches = [];
|
|
32
|
+
for (const entry of map.matchAll(/^ [\w-]+:\n triggers: (\[[^\n]*\])\n skill: ([\w-]+)$/gm)) {
|
|
33
|
+
if (!specialized.has(entry[2])) continue;
|
|
34
|
+
const triggers = JSON.parse(entry[1]);
|
|
35
|
+
if (triggers.some(trigger => {
|
|
36
|
+
const phrase = trigger.normalize('NFC').toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
37
|
+
return new RegExp(`(?<![\\p{L}\\p{N}])${phrase}(?![\\p{L}\\p{N}])`, 'u').test(text);
|
|
38
|
+
}) || text.includes(entry[2])) matches.push(entry[2]);
|
|
39
|
+
}
|
|
40
|
+
return [...new Set(matches)].sort();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function projectLifecycle(route) {
|
|
44
|
+
return { primary: route.primary.skill, advisors: [...route.advisors] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function routeRequest({ prompt, cwd, packageRoot, home = homedir() }) {
|
|
48
|
+
if (typeof prompt !== 'string' || !prompt.trim()) throw new Error('A non-empty prompt is required. Use --stdin or --prompt.');
|
|
49
|
+
const canonical = resolveIntentFromPrompt(prompt);
|
|
50
|
+
const lifecycle = projectLifecycle(canonical);
|
|
51
|
+
const domain = { matches: await discoverDomain(prompt, packageRoot) };
|
|
52
|
+
await assertSafeManagedPath(cwd, path.join(cwd, '.showdar.json'));
|
|
53
|
+
const manifest = await readJson(path.join(cwd, '.showdar.json'));
|
|
54
|
+
const installed = new Set();
|
|
55
|
+
if (manifest) {
|
|
56
|
+
if (manifest.version !== 2 || !Array.isArray(manifest.skills) || !Array.isArray(manifest.files)) throw new Error('Invalid managed Showdar manifest v2.');
|
|
57
|
+
for (const id of manifest.skills) {
|
|
58
|
+
if (typeof id !== 'string' || !ALL_SKILLS.some(s => s.id === id)) continue;
|
|
59
|
+
for (const entry of manifest.files) {
|
|
60
|
+
if (typeof entry.path === 'string' && path.basename(entry.path) === id && await skillPresent(cwd, entry.path)) installed.add(id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (manifest.satisfiedByGlobal?.length) {
|
|
64
|
+
const globalPath = path.join(home, '.showdar/global.json');
|
|
65
|
+
await assertSafeManagedPath(home, globalPath);
|
|
66
|
+
const global = await readJson(globalPath);
|
|
67
|
+
const owned = new Set((global?.files ?? []).map(e => e.path));
|
|
68
|
+
for (const entry of manifest.satisfiedByGlobal) {
|
|
69
|
+
if (manifest.skills.includes(entry.skill) && typeof entry.path === 'string' && path.basename(entry.path) === entry.skill && owned.has(entry.path) && await skillPresent(home, entry.path)) installed.add(entry.skill);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
intent: canonical.intent,
|
|
75
|
+
lifecycle,
|
|
76
|
+
domain,
|
|
77
|
+
availability: {
|
|
78
|
+
managed: manifest !== null,
|
|
79
|
+
installed: manifest ? [...installed].sort() : null,
|
|
80
|
+
missingLifecyclePrimary: manifest ? !installed.has(lifecycle.primary) : null,
|
|
81
|
+
missingAdvisors: manifest ? lifecycle.advisors.filter(id => !installed.has(id)) : null,
|
|
82
|
+
missingDomainMatches: manifest ? domain.matches.filter(id => !installed.has(id)) : null,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatRoute(data) {
|
|
88
|
+
const { lifecycle, domain, intent, availability: a } = data;
|
|
89
|
+
const lines = [`Lifecycle: ${lifecycle.primary}`, `Domain: ${domain.matches.join(', ') || 'none'}`, `Advisors: ${lifecycle.advisors.join(', ') || 'none'}`, `Mutation: ${intent.mutation}`];
|
|
90
|
+
lines.push(`Availability: ${!a.managed ? 'not-managed / unknown' : a.missingLifecyclePrimary ? 'lifecycle not installed' : 'lifecycle installed'}`);
|
|
91
|
+
if (a.managed) {
|
|
92
|
+
for (const id of [lifecycle.primary, ...lifecycle.advisors, ...domain.matches]) {
|
|
93
|
+
const present = a.installed.includes(id);
|
|
94
|
+
lines.push(`${id}: ${present ? 'installed' : 'not installed'}`);
|
|
95
|
+
if (!present) lines.push(`Suggested command: showdar add ${id.replace(/^showdar-/, '')}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|