gsd-cc 1.5.7 → 1.6.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/README.md +77 -4
- package/bin/install/args.js +161 -0
- package/bin/install/assets.js +230 -0
- package/bin/install/cli.js +167 -0
- package/bin/install/constants.js +77 -0
- package/bin/install/dashboard.js +93 -0
- package/bin/install/dependencies.js +150 -0
- package/bin/install/fs-utils.js +133 -0
- package/bin/install/hooks.js +152 -0
- package/bin/install/language-config.js +171 -0
- package/bin/install/manifest.js +306 -0
- package/bin/install/operations.js +422 -0
- package/bin/install/paths.js +65 -0
- package/bin/install.js +1 -416
- package/checklists/planning-ready.md +7 -1
- package/checklists/unify-complete.md +49 -3
- package/dashboard/app.js +2929 -0
- package/dashboard/index.html +33 -0
- package/dashboard/styles.css +2348 -0
- package/hooks/gsd-boundary-guard.sh +67 -12
- package/hooks/gsd-context-monitor.sh +19 -2
- package/hooks/gsd-prompt-guard.sh +25 -0
- package/hooks/gsd-statusline.sh +46 -6
- package/hooks/gsd-workflow-guard.sh +30 -19
- package/package.json +21 -1
- package/scripts/dashboard/read-model.js +2457 -0
- package/scripts/dashboard/task-plan-parser.js +284 -0
- package/scripts/dashboard/watch.js +391 -0
- package/scripts/dashboard-server.js +734 -0
- package/scripts/task-plan-xml.js +138 -0
- package/scripts/validate-plan.js +580 -0
- package/skills/apply/SKILL.md +172 -22
- package/skills/auto/SKILL.md +80 -5
- package/skills/auto/apply-instructions.txt +25 -3
- package/skills/auto/auto-loop.sh +331 -147
- package/skills/auto/lib/allowlist.sh +171 -0
- package/skills/auto/lib/approval.sh +333 -0
- package/skills/auto/lib/dispatch.sh +30 -0
- package/skills/auto/lib/events.sh +239 -0
- package/skills/auto/lib/git.sh +441 -0
- package/skills/auto/lib/recovery.sh +294 -0
- package/skills/auto/lib/runtime.sh +216 -0
- package/skills/auto/lib/state.sh +350 -0
- package/skills/auto/lib/task-plan.sh +575 -0
- package/skills/auto/plan-instructions.txt +38 -5
- package/skills/auto/reassess-instructions.txt +4 -0
- package/skills/auto/unify-instructions.txt +148 -8
- package/skills/config/SKILL.md +52 -11
- package/skills/dashboard/SKILL.md +63 -0
- package/skills/discuss/SKILL.md +14 -2
- package/skills/gsd-cc/SKILL.md +152 -12
- package/skills/help/SKILL.md +10 -1
- package/skills/ideate/SKILL.md +24 -21
- package/skills/ingest/SKILL.md +56 -20
- package/skills/plan/SKILL.md +91 -29
- package/skills/seed/SKILL.md +51 -16
- package/skills/stack/SKILL.md +32 -5
- package/skills/status/SKILL.md +147 -28
- package/skills/status/token-usage.py +240 -0
- package/skills/tutorial/SKILL.md +18 -5
- package/skills/unify/SKILL.md +270 -40
- package/skills/update/SKILL.md +30 -9
- package/templates/PLAN.xml +7 -2
- package/templates/STATE.md +5 -1
- package/templates/STATE_MACHINE.json +269 -0
- package/templates/UNIFY.md +93 -10
package/README.md
CHANGED
|
@@ -6,13 +6,14 @@ A project management system for AI-powered software development. Structure your
|
|
|
6
6
|
|
|
7
7
|
Claude Code is the best coding agent available. But without structure, large projects degrade into chaos: context rot, lost decisions, no quality control.
|
|
8
8
|
|
|
9
|
-
GSD-CC orchestrates Claude Code with native Skills (Markdown) — no API costs,
|
|
9
|
+
GSD-CC orchestrates Claude Code with native Skills (Markdown) — no API costs,
|
|
10
|
+
no build step, no custom agent.
|
|
10
11
|
|
|
11
12
|
| Feature | GSD-CC |
|
|
12
13
|
|---------|--------|
|
|
13
14
|
| Runtime | Claude Code (native) |
|
|
14
15
|
| Cost model | Max Plan (flat rate) |
|
|
15
|
-
| Dependencies |
|
|
16
|
+
| Dependencies | No build step (Markdown + Bash + CLI tools) |
|
|
16
17
|
| Quality control | Mandatory UNIFY after every slice |
|
|
17
18
|
| Boundary enforcement | Explicit DO NOT CHANGE rules per task |
|
|
18
19
|
| Custom project types | Drop 3 files, done |
|
|
@@ -22,9 +23,31 @@ GSD-CC orchestrates Claude Code with native Skills (Markdown) — no API costs,
|
|
|
22
23
|
```bash
|
|
23
24
|
npx gsd-cc # Install globally (default)
|
|
24
25
|
npx gsd-cc --local # Install to current project only
|
|
25
|
-
npx gsd-cc --
|
|
26
|
+
npx gsd-cc --global --yes # Install/update without prompts
|
|
27
|
+
npx gsd-cc --local --language Deutsch
|
|
28
|
+
npx gsd-cc --uninstall # Remove detected installs safely
|
|
29
|
+
npx gsd-cc --uninstall --global # Remove only the global install
|
|
30
|
+
npx gsd-cc --uninstall --local # Remove only the local install
|
|
31
|
+
npx gsd-cc dashboard --no-open # Start the local dashboard
|
|
26
32
|
```
|
|
27
33
|
|
|
34
|
+
GSD-CC tracks installed assets in `~/.claude/gsd-cc/install-manifest.json`
|
|
35
|
+
(global) or `./.claude/gsd-cc/install-manifest.json` (local), removes only
|
|
36
|
+
files it owns during uninstall, and aborts if an existing target file cannot
|
|
37
|
+
be proven safe to overwrite.
|
|
38
|
+
|
|
39
|
+
Installed layout:
|
|
40
|
+
- Hooks: `~/.claude/hooks/gsd-cc/` or `./.claude/hooks/gsd-cc/`
|
|
41
|
+
- Custom types: `~/.claude/skills/seed/types/<your-type>/` or `./.claude/skills/seed/types/<your-type>/`
|
|
42
|
+
- Scope-specific uninstall: `--global` or `--local`
|
|
43
|
+
- Prompt-free installs: `--yes`
|
|
44
|
+
- Explicit UI language: `--language <name>`
|
|
45
|
+
|
|
46
|
+
Reinstall and update runs preserve existing `GSD-CC language` and
|
|
47
|
+
`GSD-CC commit language` settings by default. In non-interactive mode, missing
|
|
48
|
+
UI language defaults to English and missing scope defaults to a global install.
|
|
49
|
+
Commit language defaults to English and can be changed with `/gsd-cc-config`.
|
|
50
|
+
|
|
28
51
|
## Usage
|
|
29
52
|
|
|
30
53
|
```bash
|
|
@@ -38,12 +61,62 @@ GSD-CC reads your project state and suggests the next action. The full cycle:
|
|
|
38
61
|
|
|
39
62
|
Auto-mode runs tasks autonomously via `claude -p` on your Max Plan.
|
|
40
63
|
|
|
64
|
+
Artifact convention:
|
|
65
|
+
- Slice overview: `.gsd/S{nn}-PLAN.md`
|
|
66
|
+
- Per-task plans: `.gsd/S{nn}-T{nn}-PLAN.xml`
|
|
67
|
+
|
|
68
|
+
## Dashboard
|
|
69
|
+
|
|
70
|
+
The dashboard gives you a browser view of the current repository's GSD state:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx gsd-cc dashboard
|
|
74
|
+
npx gsd-cc dashboard --no-open
|
|
75
|
+
npx gsd-cc dashboard --host 127.0.0.1 --port 4766
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Inside Claude Code, run `/gsd-cc-dashboard` for the same launcher guidance.
|
|
79
|
+
|
|
80
|
+
The server is local-only by default: it binds to `127.0.0.1`, reads `.gsd/`
|
|
81
|
+
from the current project, and serves dashboard assets from the installed
|
|
82
|
+
package. V1 is read-only. It shows project progress, auto-mode events, token
|
|
83
|
+
costs, and safe `.gsd/` artifact previews, but it does not write files or run
|
|
84
|
+
workflow actions.
|
|
85
|
+
|
|
41
86
|
## Requirements
|
|
42
87
|
|
|
43
88
|
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed
|
|
44
89
|
- Claude Code **Max Plan** (recommended for auto-mode)
|
|
45
90
|
- **Git** initialized in your project
|
|
46
|
-
- **jq** installed (`brew install jq`) — required for
|
|
91
|
+
- **jq** installed (`brew install jq`) — required for hooks-ready and
|
|
92
|
+
auto-ready installs
|
|
93
|
+
|
|
94
|
+
Install still succeeds without `jq`, but jq-dependent hooks stay disabled and
|
|
95
|
+
auto-mode remains unavailable until `jq` is installed. Rerun the installer
|
|
96
|
+
after adding `jq` to activate hooks.
|
|
97
|
+
|
|
98
|
+
## Testing
|
|
99
|
+
|
|
100
|
+
From this package directory:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm test
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For dashboard integration sweeps, also launch the read-only Web App from a
|
|
107
|
+
project that has `.gsd/` state:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx gsd-cc dashboard --no-open
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The dashboard is optional for auto-mode. Auto-mode writes `.gsd/events.jsonl`
|
|
114
|
+
and other artifacts directly, so it continues to work when the dashboard
|
|
115
|
+
server is not running.
|
|
116
|
+
|
|
117
|
+
The suite uses temporary homes, projects, fake `claude`/`jq` binaries, and
|
|
118
|
+
temporary Git repositories. It must not touch the developer's real
|
|
119
|
+
`~/.claude`, call the real `claude` CLI, or require network access.
|
|
47
120
|
|
|
48
121
|
## Documentation
|
|
49
122
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
function defaultParsed() {
|
|
2
|
+
return {
|
|
3
|
+
command: 'install',
|
|
4
|
+
global: false,
|
|
5
|
+
local: false,
|
|
6
|
+
uninstall: false,
|
|
7
|
+
help: false,
|
|
8
|
+
yes: false,
|
|
9
|
+
language: null,
|
|
10
|
+
dashboard: null,
|
|
11
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseRequiredValue(rawArgs, index, optionName) {
|
|
16
|
+
const value = rawArgs[index + 1];
|
|
17
|
+
if (value === undefined || value.startsWith('-') || !value.trim()) {
|
|
18
|
+
throw new Error(`${optionName} requires a non-empty value.`);
|
|
19
|
+
}
|
|
20
|
+
return value.trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseDashboardPort(value) {
|
|
24
|
+
const trimmed = String(value).trim();
|
|
25
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
26
|
+
throw new Error('--port requires a number between 1 and 65535.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const port = Number(trimmed);
|
|
30
|
+
if (port < 1 || port > 65535) {
|
|
31
|
+
throw new Error('--port requires a number between 1 and 65535.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return port;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureDashboardOptions(parsed) {
|
|
38
|
+
if (!parsed.dashboard) {
|
|
39
|
+
parsed.dashboard = {
|
|
40
|
+
host: null,
|
|
41
|
+
port: null,
|
|
42
|
+
open: true
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseArgs(rawArgs) {
|
|
48
|
+
const parsed = defaultParsed();
|
|
49
|
+
|
|
50
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
51
|
+
const arg = rawArgs[index];
|
|
52
|
+
|
|
53
|
+
if (index === 0 && arg === 'dashboard') {
|
|
54
|
+
parsed.command = 'dashboard';
|
|
55
|
+
ensureDashboardOptions(parsed);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (parsed.command === 'dashboard') {
|
|
60
|
+
if (arg === '--no-open') {
|
|
61
|
+
parsed.dashboard.open = false;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (arg === '--host') {
|
|
66
|
+
parsed.dashboard.host = parseRequiredValue(rawArgs, index, '--host');
|
|
67
|
+
index += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (arg.startsWith('--host=')) {
|
|
72
|
+
const value = arg.slice('--host='.length).trim();
|
|
73
|
+
if (!value) {
|
|
74
|
+
throw new Error('--host requires a non-empty value.');
|
|
75
|
+
}
|
|
76
|
+
parsed.dashboard.host = value;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (arg === '--port') {
|
|
81
|
+
parsed.dashboard.port = parseDashboardPort(
|
|
82
|
+
parseRequiredValue(rawArgs, index, '--port')
|
|
83
|
+
);
|
|
84
|
+
index += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (arg.startsWith('--port=')) {
|
|
89
|
+
parsed.dashboard.port = parseDashboardPort(arg.slice('--port='.length));
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (arg === '--help' || arg === '-h') {
|
|
94
|
+
parsed.help = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (arg.startsWith('-')) {
|
|
99
|
+
throw new Error(`Unknown dashboard option: ${arg}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new Error(`Unexpected dashboard argument: ${arg}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (arg === '--global' || arg === '-g') {
|
|
106
|
+
parsed.global = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (arg === '--local' || arg === '-l') {
|
|
111
|
+
parsed.local = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (arg === '--uninstall') {
|
|
116
|
+
parsed.uninstall = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (arg === '--help' || arg === '-h') {
|
|
121
|
+
parsed.help = true;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (arg === '--yes' || arg === '-y') {
|
|
126
|
+
parsed.yes = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (arg === '--language') {
|
|
131
|
+
parsed.language = parseRequiredValue(rawArgs, index, '--language');
|
|
132
|
+
index += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (arg.startsWith('--language=')) {
|
|
137
|
+
const value = arg.slice('--language='.length).trim();
|
|
138
|
+
if (!value) {
|
|
139
|
+
throw new Error('--language requires a non-empty value.');
|
|
140
|
+
}
|
|
141
|
+
parsed.language = value;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (arg.startsWith('-')) {
|
|
146
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (parsed.global && parsed.local) {
|
|
153
|
+
throw new Error('Cannot specify both --global and --local.');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
parseArgs
|
|
161
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const {
|
|
5
|
+
INSTALL_LAYOUT,
|
|
6
|
+
CURRENT_HOOK_DIR,
|
|
7
|
+
LEGACY_HOOK_DIR
|
|
8
|
+
} = require('./constants');
|
|
9
|
+
const {
|
|
10
|
+
compareFileContents,
|
|
11
|
+
ensureDirectory,
|
|
12
|
+
sortPathsDeepFirst
|
|
13
|
+
} = require('./fs-utils');
|
|
14
|
+
const { formatPath } = require('./paths');
|
|
15
|
+
const { buildHookSpecs } = require('./hooks');
|
|
16
|
+
|
|
17
|
+
function collectRelativeFiles(rootDir, currentDir, files) {
|
|
18
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
19
|
+
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
22
|
+
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
collectRelativeFiles(rootDir, absolutePath, files);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
files.push(path.relative(rootDir, absolutePath));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function collectAssets(srcBase, claudeBase) {
|
|
33
|
+
const assets = [];
|
|
34
|
+
|
|
35
|
+
for (const layout of INSTALL_LAYOUT) {
|
|
36
|
+
const sourceRoot = path.join(srcBase, layout.sourceDir);
|
|
37
|
+
if (!fs.existsSync(sourceRoot)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const relativeFiles = [];
|
|
42
|
+
collectRelativeFiles(sourceRoot, sourceRoot, relativeFiles);
|
|
43
|
+
|
|
44
|
+
for (const relativePath of relativeFiles) {
|
|
45
|
+
const targetRelativePath = path.join(layout.targetDir, relativePath);
|
|
46
|
+
assets.push({
|
|
47
|
+
sourcePath: path.join(sourceRoot, relativePath),
|
|
48
|
+
targetPath: path.join(claudeBase, targetRelativePath),
|
|
49
|
+
targetRelativePath,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return assets.sort((left, right) => {
|
|
55
|
+
return left.targetRelativePath.localeCompare(right.targetRelativePath);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fileContains(filePath, snippet) {
|
|
60
|
+
if (!fs.existsSync(filePath)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return fs.readFileSync(filePath, 'utf8').includes(snippet);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function detectLegacyInstallation(claudeBase) {
|
|
68
|
+
const reasons = [];
|
|
69
|
+
const skillsRoot = path.join(claudeBase, 'skills');
|
|
70
|
+
const routerSkill = path.join(skillsRoot, 'gsd-cc', 'SKILL.md');
|
|
71
|
+
|
|
72
|
+
if (fileContains(routerSkill, 'name: gsd-cc')) {
|
|
73
|
+
reasons.push('skills/gsd-cc/SKILL.md');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (fs.existsSync(skillsRoot)) {
|
|
77
|
+
const entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (!entry.isDirectory()) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (entry.name === 'gsd' || entry.name.startsWith('gsd-cc-')) {
|
|
83
|
+
reasons.push(path.join('skills', entry.name));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const spec of buildHookSpecs(claudeBase, LEGACY_HOOK_DIR)) {
|
|
89
|
+
for (const hook of spec.hooks) {
|
|
90
|
+
if (fs.existsSync(hook.command)) {
|
|
91
|
+
reasons.push(path.relative(claudeBase, hook.command));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
detected: reasons.length > 0,
|
|
98
|
+
reasons: [...new Set(reasons)].sort()
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function collectLegacyPaths(claudeBase, assets) {
|
|
103
|
+
const detection = detectLegacyInstallation(claudeBase);
|
|
104
|
+
const legacyFiles = new Set();
|
|
105
|
+
const legacyDirectories = new Set();
|
|
106
|
+
const warnings = [];
|
|
107
|
+
|
|
108
|
+
if (!detection.detected) {
|
|
109
|
+
return {
|
|
110
|
+
detected: false,
|
|
111
|
+
reasons: [],
|
|
112
|
+
files: [],
|
|
113
|
+
directories: [],
|
|
114
|
+
warnings
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const asset of assets) {
|
|
119
|
+
if (fs.existsSync(asset.targetPath)) {
|
|
120
|
+
legacyFiles.add(asset.targetRelativePath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const spec of buildHookSpecs(claudeBase, LEGACY_HOOK_DIR)) {
|
|
125
|
+
for (const command of spec.commands) {
|
|
126
|
+
const absolutePath = path.join(claudeBase, command);
|
|
127
|
+
if (fs.existsSync(absolutePath)) {
|
|
128
|
+
legacyFiles.add(command);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const skillsRoot = path.join(claudeBase, 'skills');
|
|
134
|
+
if (fs.existsSync(skillsRoot)) {
|
|
135
|
+
const entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
if (!entry.isDirectory()) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (entry.name === 'gsd' || entry.name.startsWith('gsd-cc-')) {
|
|
141
|
+
legacyDirectories.add(path.join('skills', entry.name));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const promptsDir = path.join(claudeBase, 'prompts');
|
|
147
|
+
if (fs.existsSync(promptsDir)) {
|
|
148
|
+
warnings.push(
|
|
149
|
+
`${formatPath(promptsDir)} was left in place because GSD-CC can no ` +
|
|
150
|
+
'longer prove it owns that directory.'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
detected: true,
|
|
156
|
+
reasons: detection.reasons,
|
|
157
|
+
files: [...legacyFiles].sort(),
|
|
158
|
+
directories: sortPathsDeepFirst([...legacyDirectories]),
|
|
159
|
+
warnings
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function ensureNoConflicts(assets, ownedRelativePaths) {
|
|
164
|
+
const conflicts = [];
|
|
165
|
+
|
|
166
|
+
for (const asset of assets) {
|
|
167
|
+
if (!fs.existsSync(asset.targetPath)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (ownedRelativePaths.has(asset.targetRelativePath)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const targetStat = fs.lstatSync(asset.targetPath);
|
|
176
|
+
if (!targetStat.isFile()) {
|
|
177
|
+
conflicts.push(asset.targetRelativePath);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (compareFileContents(asset.sourcePath, asset.targetPath)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
conflicts.push(asset.targetRelativePath);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (conflicts.length === 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const rendered = conflicts.map((relativePath) => {
|
|
193
|
+
const asset = assets.find((candidate) => {
|
|
194
|
+
return candidate.targetRelativePath === relativePath;
|
|
195
|
+
});
|
|
196
|
+
return ` - ${formatPath(asset ? asset.targetPath : relativePath)}`;
|
|
197
|
+
}).join('\n');
|
|
198
|
+
|
|
199
|
+
throw new Error(
|
|
200
|
+
'Refusing to overwrite files that are not proven to be owned by GSD-CC:\n' +
|
|
201
|
+
`${rendered}\n` +
|
|
202
|
+
'Remove the conflicting files manually or uninstall the existing tool first.'
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getInstallMode(asset) {
|
|
207
|
+
const hookDirPrefix = `${CURRENT_HOOK_DIR}${path.sep}`;
|
|
208
|
+
if (
|
|
209
|
+
asset.targetRelativePath.startsWith(hookDirPrefix) &&
|
|
210
|
+
asset.targetRelativePath.endsWith('.sh')
|
|
211
|
+
) {
|
|
212
|
+
return 0o755;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return fs.statSync(asset.sourcePath).mode & 0o777;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function copyAsset(asset) {
|
|
219
|
+
ensureDirectory(path.dirname(asset.targetPath));
|
|
220
|
+
fs.copyFileSync(asset.sourcePath, asset.targetPath);
|
|
221
|
+
fs.chmodSync(asset.targetPath, getInstallMode(asset));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = {
|
|
225
|
+
collectAssets,
|
|
226
|
+
collectLegacyPaths,
|
|
227
|
+
detectLegacyInstallation,
|
|
228
|
+
ensureNoConflicts,
|
|
229
|
+
copyAsset
|
|
230
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const readline = require('readline');
|
|
3
|
+
const pkg = require('../../package.json');
|
|
4
|
+
const { parseArgs } = require('./args');
|
|
5
|
+
const { COLORS } = require('./constants');
|
|
6
|
+
const { launchDashboard } = require('./dashboard');
|
|
7
|
+
const { getClaudeBase } = require('./paths');
|
|
8
|
+
const { writeLanguageConfig } = require('./language-config');
|
|
9
|
+
const {
|
|
10
|
+
installAndConfigure,
|
|
11
|
+
printDefaultGlobalChoice,
|
|
12
|
+
printLanguageSet,
|
|
13
|
+
uninstall
|
|
14
|
+
} = require('./operations');
|
|
15
|
+
|
|
16
|
+
const { cyan, yellow, red, dim, reset } = COLORS;
|
|
17
|
+
|
|
18
|
+
const banner = `
|
|
19
|
+
${cyan} ██████╗ ███████╗██████╗ ██████╗ ██████╗
|
|
20
|
+
██╔════╝ ██╔════╝██╔══██╗ ██╔════╝██╔════╝
|
|
21
|
+
██║ ███╗███████╗██║ ██║█████╗██║ ██║
|
|
22
|
+
██║ ██║╚════██║██║ ██║╚════╝██║ ██║
|
|
23
|
+
╚██████╔╝███████║██████╔╝ ╚██████╗╚██████╗
|
|
24
|
+
╚═════╝ ╚══════╝╚═════╝ ╚═════╝ ╚═════╝${reset}
|
|
25
|
+
|
|
26
|
+
Get Shit Done on Claude Code ${dim}v${pkg.version}${reset}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
function printHelp() {
|
|
30
|
+
console.log(` ${yellow}Usage:${reset} npx gsd-cc [options]
|
|
31
|
+
npx gsd-cc dashboard [options]
|
|
32
|
+
|
|
33
|
+
${yellow}Commands:${reset}
|
|
34
|
+
${cyan}dashboard${reset} Start the local dashboard server
|
|
35
|
+
|
|
36
|
+
${yellow}Options:${reset}
|
|
37
|
+
${cyan}-g, --global${reset} Install globally to ~/.claude/skills/ ${dim}(default)${reset}
|
|
38
|
+
${cyan}-l, --local${reset} Install locally to ./.claude/skills/
|
|
39
|
+
${cyan}--uninstall${reset} Remove GSD-CC safely from detected installs
|
|
40
|
+
${cyan}-y, --yes${reset} Run without prompts
|
|
41
|
+
${cyan}--language <name>${reset} Set GSD-CC language non-interactively
|
|
42
|
+
${cyan}-h, --help${reset} Show this help message
|
|
43
|
+
|
|
44
|
+
${yellow}Dashboard Options:${reset}
|
|
45
|
+
${cyan}--host <host>${reset} Host to bind ${dim}(default: 127.0.0.1)${reset}
|
|
46
|
+
${cyan}--port <number>${reset} Port to bind ${dim}(default: 4766)${reset}
|
|
47
|
+
${cyan}--no-open${reset} Do not open a browser automatically
|
|
48
|
+
|
|
49
|
+
${yellow}Examples:${reset}
|
|
50
|
+
${dim}# Install globally (default)${reset}
|
|
51
|
+
npx gsd-cc
|
|
52
|
+
|
|
53
|
+
${dim}# Install to current project only${reset}
|
|
54
|
+
npx gsd-cc --local
|
|
55
|
+
|
|
56
|
+
${dim}# Update or automate without prompts${reset}
|
|
57
|
+
npx gsd-cc --global --yes
|
|
58
|
+
|
|
59
|
+
${dim}# Remove GSD-CC${reset}
|
|
60
|
+
npx gsd-cc --uninstall
|
|
61
|
+
|
|
62
|
+
${dim}# Start the dashboard launcher${reset}
|
|
63
|
+
npx gsd-cc dashboard --no-open
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fail(error) {
|
|
68
|
+
const message = error && error.message ? error.message : String(error);
|
|
69
|
+
console.error(` ${red}Error:${reset} ${message}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function promptLanguage(isGlobal, onDone) {
|
|
74
|
+
const rl = readline.createInterface({
|
|
75
|
+
input: process.stdin,
|
|
76
|
+
output: process.stdout,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
console.log(`
|
|
80
|
+
${yellow}Which language should GSD-CC use?${reset}
|
|
81
|
+
${dim}(e.g. English, Deutsch, Français, Español, ...)${reset}
|
|
82
|
+
${dim}You can change this anytime with /gsd-cc-config in Claude Code${reset}
|
|
83
|
+
`);
|
|
84
|
+
|
|
85
|
+
rl.question(` Language ${dim}[English]${reset}: `, (answer) => {
|
|
86
|
+
rl.close();
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const language = answer.trim() || 'English';
|
|
90
|
+
writeLanguageConfig(isGlobal, language);
|
|
91
|
+
printLanguageSet(language);
|
|
92
|
+
onDone();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
fail(error);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function promptLocation(installOptions) {
|
|
100
|
+
const rl = readline.createInterface({
|
|
101
|
+
input: process.stdin,
|
|
102
|
+
output: process.stdout,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const globalPath = getClaudeBase(true).replace(os.homedir(), '~');
|
|
106
|
+
|
|
107
|
+
console.log(` ${yellow}Where would you like to install?${reset}
|
|
108
|
+
|
|
109
|
+
${cyan}1${reset}) Global ${dim}(${globalPath})${reset} — available in all projects
|
|
110
|
+
${cyan}2${reset}) Local ${dim}(./.claude/)${reset} — this project only
|
|
111
|
+
`);
|
|
112
|
+
|
|
113
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
114
|
+
rl.close();
|
|
115
|
+
console.log();
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const isGlobal = (answer.trim() || '1') !== '2';
|
|
119
|
+
installAndConfigure(isGlobal, installOptions, promptLanguage);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
fail(error);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function main(rawArgs) {
|
|
127
|
+
let options;
|
|
128
|
+
try {
|
|
129
|
+
options = parseArgs(rawArgs);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.log(banner);
|
|
132
|
+
fail(error);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const hasGlobal = options.global;
|
|
136
|
+
const hasLocal = options.local;
|
|
137
|
+
const hasUninstall = options.uninstall;
|
|
138
|
+
const hasHelp = options.help;
|
|
139
|
+
|
|
140
|
+
console.log(banner);
|
|
141
|
+
|
|
142
|
+
if (hasHelp) {
|
|
143
|
+
printHelp();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
if (options.command === 'dashboard') {
|
|
149
|
+
launchDashboard(options).catch(fail);
|
|
150
|
+
} else if (hasUninstall) {
|
|
151
|
+
uninstall(options);
|
|
152
|
+
} else if (hasGlobal) {
|
|
153
|
+
installAndConfigure(true, options, promptLanguage);
|
|
154
|
+
} else if (hasLocal) {
|
|
155
|
+
installAndConfigure(false, options, promptLanguage);
|
|
156
|
+
} else if (options.yes || !options.interactive) {
|
|
157
|
+
printDefaultGlobalChoice();
|
|
158
|
+
installAndConfigure(true, options, promptLanguage);
|
|
159
|
+
} else {
|
|
160
|
+
promptLocation(options);
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
fail(error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { main };
|