@kunlv/atdc 1.0.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/AGENTS.md +58 -0
- package/GO.md +245 -0
- package/bin/add-command.py +144 -0
- package/bin/atdc.js +460 -0
- package/bin/cls.js +98 -0
- package/central/link.sh +60 -0
- package/central/ticket_center_server/.handoffs/TASK-238389_QYinVipLimit.md +128 -0
- package/central/ticket_center_server/.test_plans/TASK-227254-SupportMembership-self-test-plan.md +566 -0
- package/central/ticket_center_server/.test_plans/task-238389-qyin-vip-limit-self-test-plan.md +210 -0
- package/commands/claude/atd.md +3 -0
- package/commands/claude/atdc/1-spec.md +5 -0
- package/commands/claude/atdc/1-update.md +5 -0
- package/commands/claude/atdc/2-qa.md +3 -0
- package/commands/claude/atdc/3-impl-skip-spec.md +5 -0
- package/commands/claude/atdc/3-impl.md +3 -0
- package/commands/claude/atdc/4-verify.md +3 -0
- package/commands/claude/atdc/5-done.md +3 -0
- package/commands/claude/atdc/add-command.md +4 -0
- package/commands/claude/atdc/add-to-hc.md +3 -0
- package/commands/claude/atdc/analysis-cls-log.md +4 -0
- package/commands/claude/atdc/article.md +6 -0
- package/commands/claude/atdc/bugfix.md +4 -0
- package/commands/claude/atdc/code-simplifier.md +8 -0
- package/commands/claude/atdc/commit.md +4 -0
- package/commands/claude/atdc/compound.md +5 -0
- package/commands/claude/atdc/context.md +3 -0
- package/commands/claude/atdc/diagnose.md +8 -0
- package/commands/claude/atdc/git-guardrails.md +18 -0
- package/commands/claude/atdc/grill-with-docs.md +8 -0
- package/commands/claude/atdc/handoff.md +8 -0
- package/commands/claude/atdc/health.md +3 -0
- package/commands/claude/atdc/learn.md +4 -0
- package/commands/claude/atdc/review.md +10 -0
- package/commands/claude/atdc/search.md +4 -0
- package/commands/claude/atdc/strategy.md +3 -0
- package/commands/claude/atdc/table2xlsx.md +4 -0
- package/commands/claude/atdc/test-plan.md +5 -0
- package/commands/claude/atdc/travel-planner.md +4 -0
- package/commands/claude/atdc/xlsx2sql-insert.md +4 -0
- package/commands/claude/atdc/xlsx2sql-update.md +4 -0
- package/commands/claude/atdc/zoom-out.md +8 -0
- package/commands/codex/atdc/SKILL.md +8 -0
- package/commands/codex/atdc-1-spec/SKILL.md +8 -0
- package/commands/codex/atdc-1-update/SKILL.md +8 -0
- package/commands/codex/atdc-2-qa/SKILL.md +8 -0
- package/commands/codex/atdc-3-impl/SKILL.md +8 -0
- package/commands/codex/atdc-3-impl-skip-spec/SKILL.md +8 -0
- package/commands/codex/atdc-4-verify/SKILL.md +8 -0
- package/commands/codex/atdc-5-done/SKILL.md +8 -0
- package/commands/codex/atdc-add-command/SKILL.md +7 -0
- package/commands/codex/atdc-add-to-hc/SKILL.md +8 -0
- package/commands/codex/atdc-analysis-cls-log/SKILL.md +5 -0
- package/commands/codex/atdc-article/SKILL.md +8 -0
- package/commands/codex/atdc-bugfix/SKILL.md +5 -0
- package/commands/codex/atdc-code-simplifier/SKILL.md +7 -0
- package/commands/codex/atdc-commit/SKILL.md +7 -0
- package/commands/codex/atdc-compound/SKILL.md +8 -0
- package/commands/codex/atdc-context/SKILL.md +8 -0
- package/commands/codex/atdc-diagnose/SKILL.md +9 -0
- package/commands/codex/atdc-git-guardrails/SKILL.md +19 -0
- package/commands/codex/atdc-grill-with-docs/SKILL.md +9 -0
- package/commands/codex/atdc-handoff/SKILL.md +9 -0
- package/commands/codex/atdc-health/SKILL.md +8 -0
- package/commands/codex/atdc-learn/SKILL.md +7 -0
- package/commands/codex/atdc-review/SKILL.md +49 -0
- package/commands/codex/atdc-search/SKILL.md +5 -0
- package/commands/codex/atdc-strategy/SKILL.md +8 -0
- package/commands/codex/atdc-table2xlsx/SKILL.md +8 -0
- package/commands/codex/atdc-test-plan/SKILL.md +13 -0
- package/commands/codex/atdc-travel-planner/SKILL.md +7 -0
- package/commands/codex/atdc-xlsx2sql-insert/SKILL.md +8 -0
- package/commands/codex/atdc-xlsx2sql-update/SKILL.md +8 -0
- package/commands/codex/atdc-zoom-out/SKILL.md +9 -0
- package/commands/cursor/atdc/SKILL.md +8 -0
- package/commands/cursor/atdc-1-spec/SKILL.md +8 -0
- package/commands/cursor/atdc-1-update/SKILL.md +8 -0
- package/commands/cursor/atdc-2-qa/SKILL.md +8 -0
- package/commands/cursor/atdc-3-impl/SKILL.md +8 -0
- package/commands/cursor/atdc-3-impl-skip-spec/SKILL.md +8 -0
- package/commands/cursor/atdc-4-verify/SKILL.md +8 -0
- package/commands/cursor/atdc-5-done/SKILL.md +8 -0
- package/commands/cursor/atdc-add-command/SKILL.md +7 -0
- package/commands/cursor/atdc-add-to-hc/SKILL.md +8 -0
- package/commands/cursor/atdc-analysis-cls-log/SKILL.md +5 -0
- package/commands/cursor/atdc-article/SKILL.md +8 -0
- package/commands/cursor/atdc-bugfix/SKILL.md +5 -0
- package/commands/cursor/atdc-code-simplifier/SKILL.md +7 -0
- package/commands/cursor/atdc-commit/SKILL.md +7 -0
- package/commands/cursor/atdc-compound/SKILL.md +8 -0
- package/commands/cursor/atdc-context/SKILL.md +8 -0
- package/commands/cursor/atdc-diagnose/SKILL.md +8 -0
- package/commands/cursor/atdc-git-guardrails/SKILL.md +18 -0
- package/commands/cursor/atdc-grill-with-docs/SKILL.md +8 -0
- package/commands/cursor/atdc-handoff/SKILL.md +8 -0
- package/commands/cursor/atdc-health/SKILL.md +8 -0
- package/commands/cursor/atdc-learn/SKILL.md +7 -0
- package/commands/cursor/atdc-review/SKILL.md +49 -0
- package/commands/cursor/atdc-search/SKILL.md +5 -0
- package/commands/cursor/atdc-strategy/SKILL.md +8 -0
- package/commands/cursor/atdc-table2xlsx/SKILL.md +8 -0
- package/commands/cursor/atdc-test-plan/SKILL.md +7 -0
- package/commands/cursor/atdc-travel-planner/SKILL.md +7 -0
- package/commands/cursor/atdc-xlsx2sql-insert/SKILL.md +8 -0
- package/commands/cursor/atdc-xlsx2sql-update/SKILL.md +8 -0
- package/commands/cursor/atdc-zoom-out/SKILL.md +8 -0
- package/commands/opencode/atd.md +6 -0
- package/commands/opencode/atdc/1-spec.md +8 -0
- package/commands/opencode/atdc/1-update.md +8 -0
- package/commands/opencode/atdc/2-qa.md +6 -0
- package/commands/opencode/atdc/3-impl-skip-spec.md +8 -0
- package/commands/opencode/atdc/3-impl.md +6 -0
- package/commands/opencode/atdc/4-verify.md +6 -0
- package/commands/opencode/atdc/5-done.md +6 -0
- package/commands/opencode/atdc/add-command.md +6 -0
- package/commands/opencode/atdc/add-to-hc.md +6 -0
- package/commands/opencode/atdc/analysis-cls-log.md +6 -0
- package/commands/opencode/atdc/article.md +6 -0
- package/commands/opencode/atdc/bugfix.md +6 -0
- package/commands/opencode/atdc/code-simplifier.md +8 -0
- package/commands/opencode/atdc/commit.md +6 -0
- package/commands/opencode/atdc/compound.md +8 -0
- package/commands/opencode/atdc/context.md +6 -0
- package/commands/opencode/atdc/diagnose.md +8 -0
- package/commands/opencode/atdc/git-guardrails.md +18 -0
- package/commands/opencode/atdc/grill-with-docs.md +8 -0
- package/commands/opencode/atdc/handoff.md +8 -0
- package/commands/opencode/atdc/health.md +6 -0
- package/commands/opencode/atdc/learn.md +6 -0
- package/commands/opencode/atdc/review.md +7 -0
- package/commands/opencode/atdc/search.md +6 -0
- package/commands/opencode/atdc/strategy.md +6 -0
- package/commands/opencode/atdc/table2xlsx.md +6 -0
- package/commands/opencode/atdc/test-plan.md +6 -0
- package/commands/opencode/atdc/travel-planner.md +6 -0
- package/commands/opencode/atdc/xlsx2sql-insert.md +6 -0
- package/commands/opencode/atdc/xlsx2sql-update.md +6 -0
- package/commands/opencode/atdc/zoom-out.md +8 -0
- package/config/cls-defaults.json +19 -0
- package/hooks/atd-archive.sh +86 -0
- package/hooks/atd-state.sh +165 -0
- package/hooks/evidence-hook.sh +88 -0
- package/hooks/evidence-log.sh +54 -0
- package/hooks/guardrails.sh +65 -0
- package/hooks/handoff-compress.sh +104 -0
- package/hooks/phase-guard.sh +134 -0
- package/hooks/phase-write-guard.sh +96 -0
- package/hooks/ponytail-fix.sh +179 -0
- package/hooks/ponytail-guard.sh +66 -0
- package/hooks/ponytail-scan.sh +62 -0
- package/install.sh +276 -0
- package/package.json +20 -0
- package/packages/cls/bin/cls.js +146 -0
- package/packages/cls/config/defaults.json +17 -0
- package/packages/cls/mcp/server.js +142 -0
- package/packages/cls/package.json +29 -0
- package/packages/cls/scripts/postinstall.js +19 -0
- package/packages/cls/src/client.js +262 -0
- package/prompts/1-spec.md +271 -0
- package/prompts/1-update.md +198 -0
- package/prompts/2-qa.md +164 -0
- package/prompts/3-impl-bugfix.md +271 -0
- package/prompts/3-impl.md +419 -0
- package/prompts/4-verify.md +160 -0
- package/prompts/5-done.md +227 -0
- package/prompts/6-article.md +571 -0
- package/prompts/X-add-command.md +60 -0
- package/prompts/X-add-to-hc.md +178 -0
- package/prompts/X-analysis-cls-log.md +68 -0
- package/prompts/X-bugfix.md +10 -0
- package/prompts/X-code-simplifier.md +38 -0
- package/prompts/X-commit.md +116 -0
- package/prompts/X-compound.md +188 -0
- package/prompts/X-context-scan.md +101 -0
- package/prompts/X-diagnose.md +116 -0
- package/prompts/X-grill-with-docs.md +120 -0
- package/prompts/X-handoff.md +80 -0
- package/prompts/X-health.md +78 -0
- package/prompts/X-learn.md +151 -0
- package/prompts/X-review.md +417 -0
- package/prompts/X-search.md +74 -0
- package/prompts/X-strategy.md +95 -0
- package/prompts/X-table2xlsx.md +46 -0
- package/prompts/X-test-plan.md +436 -0
- package/prompts/X-travel-planner.md +22 -0
- package/prompts/X-xlsx2sql-insert.md +34 -0
- package/prompts/X-xlsx2sql-update.md +40 -0
- package/prompts/X-zoom-out.md +119 -0
- package/reference/diagnose-loop.md +66 -0
- package/reference/gates.md +90 -0
- package/reference/git-guardrails.md +71 -0
- package/reference/guards.md +275 -0
- package/reference/learn.md +100 -0
- package/reference/memory.md +190 -0
- package/reference/risk.md +74 -0
- package/reference/tdd.md +40 -0
- package/scripts/svg2png.js +46 -0
- package/src/cls/client.js +244 -0
- package/src/constants.js +49 -0
- package/src/evidence/ai-draft-metrics.js +157 -0
- package/src/evidence/canvas.js +184 -0
- package/src/evidence/index.js +98 -0
- package/src/evidence/sink-events.js +165 -0
- package/src/evidence/stage-timing.js +112 -0
- package/src/installers/claude.js +79 -0
- package/src/installers/codex.js +62 -0
- package/src/installers/opencode.js +72 -0
- package/src/manifest/ponytail-patterns.json +185 -0
- package/src/mcp/cls-mcp-server.js +142 -0
- package/src/utils.js +79 -0
- package/src/validators/state-validate.js +123 -0
- package/templates/.phase-state.json +13 -0
- package/templates/adr.md +62 -0
- package/templates/changelog.md +21 -0
- package/templates/concepts.md +35 -0
- package/templates/design.md +47 -0
- package/templates/handoff.md +64 -0
- package/templates/learn-skill.md +43 -0
- package/templates/progress.json +17 -0
- package/templates/proposal.md +29 -0
- package/templates/solution.md +36 -0
- package/templates/strategy.md +39 -0
- package/templates/tasks.md +41 -0
package/bin/atdc.js
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { HOME, MEMORY_DIR, PACKAGE_DIR, TERMINAL_CONFIGS } = require('../src/constants');
|
|
7
|
+
const { exists, mkdirp, resolvePackageDir, replaceAllInDir } = require('../src/utils');
|
|
8
|
+
const evidence = require('../src/evidence');
|
|
9
|
+
|
|
10
|
+
const installers = {
|
|
11
|
+
claude: require('../src/installers/claude'),
|
|
12
|
+
opencode: require('../src/installers/opencode'),
|
|
13
|
+
codex: require('../src/installers/codex'),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ALL_TARGETS = ['claude', 'opencode', 'codex'];
|
|
17
|
+
|
|
18
|
+
function usage() {
|
|
19
|
+
console.log(`atdc — AI coding workflow orchestration kit
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
atdc install [--claude|--opencode|--codex|--all] Register to AI terminals
|
|
23
|
+
atdc uninstall [--all] Remove registrations
|
|
24
|
+
atdc status Show installation status
|
|
25
|
+
atdc guardrails Install git pre-push hook
|
|
26
|
+
atdc guard <phase> Validate phase transition preconditions
|
|
27
|
+
atdc validate Validate .phase-state.json schema
|
|
28
|
+
atdc handoff <change> Generate compressed context package
|
|
29
|
+
atdc run [--hotfix|--tweak|--bugfix] Quick-run with preset workflow
|
|
30
|
+
atdc archive <change> [--dry-run] Archive a completed change
|
|
31
|
+
atdc hook install Install phase write guard to OpenCode
|
|
32
|
+
atdc ponytail scan [--diff] Scan project for over-engineering patterns
|
|
33
|
+
atdc ponytail guard [--strict|--warn] Enforce ponytail rules before build
|
|
34
|
+
atdc ponytail fix [--dry-run] --pattern=<id> Apply generic refactor (crud|rpc|di)
|
|
35
|
+
atdc evidence events <change> [--type=<t>] [--limit=<n>] List evidence events
|
|
36
|
+
atdc evidence stats <change> Event statistics
|
|
37
|
+
atdc evidence timing <change> Stage timing report
|
|
38
|
+
atdc evidence metrics <change> [--snapshot|--commit] AI draft metrics
|
|
39
|
+
atdc evidence canvas <change> [--out=<path>] Generate evidence dashboard
|
|
40
|
+
atdc evidence record <change> <type> Record an evidence event
|
|
41
|
+
atdc evidence all <change> Full evidence summary
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--claude Claude Code only
|
|
45
|
+
--opencode OpenCode only
|
|
46
|
+
--codex Codex only
|
|
47
|
+
--all All detected terminals (default for install)
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseTargets(args) {
|
|
52
|
+
const targets = new Set();
|
|
53
|
+
for (const a of args) {
|
|
54
|
+
if (a === '--all') { ALL_TARGETS.forEach(t => targets.add(t)); }
|
|
55
|
+
else if (a === '--claude') targets.add('claude');
|
|
56
|
+
else if (a === '--opencode') targets.add('opencode');
|
|
57
|
+
else if (a === '--codex') targets.add('codex');
|
|
58
|
+
}
|
|
59
|
+
if (targets.size === 0) targets.add('all');
|
|
60
|
+
return targets;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function runForTargets(targets, fnName) {
|
|
64
|
+
const hasAll = targets.has('all');
|
|
65
|
+
const results = [];
|
|
66
|
+
|
|
67
|
+
for (const t of ALL_TARGETS) {
|
|
68
|
+
if (!hasAll && !targets.has(t)) continue;
|
|
69
|
+
const cfg = TERMINAL_CONFIGS[t];
|
|
70
|
+
console.log(`\n${cfg.name}:`);
|
|
71
|
+
const r = installers[t][fnName]();
|
|
72
|
+
if (!r.ok) {
|
|
73
|
+
console.log(` ⚠ ${r.error}`);
|
|
74
|
+
results.push({ target: t, ok: false, error: r.error });
|
|
75
|
+
} else if (r.results) {
|
|
76
|
+
r.results.forEach(line => console.log(line));
|
|
77
|
+
results.push({ target: t, ok: true });
|
|
78
|
+
} else if (r.details) {
|
|
79
|
+
for (const [k, v] of Object.entries(r.details)) {
|
|
80
|
+
const icon = v === 'ok' ? '✓' : v === 'conflict' ? '⚠' : '✗';
|
|
81
|
+
console.log(` ${icon} ${k}: ${v}`);
|
|
82
|
+
}
|
|
83
|
+
results.push({ target: t, ok: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cmdInstall(args) {
|
|
91
|
+
const targets = parseTargets(args);
|
|
92
|
+
const pkgDir = resolvePackageDir();
|
|
93
|
+
|
|
94
|
+
replaceAllInDir(path.join(pkgDir, 'commands'), '{{ATDC_DIR}}', pkgDir);
|
|
95
|
+
|
|
96
|
+
mkdirp(MEMORY_DIR);
|
|
97
|
+
|
|
98
|
+
console.log(`atdc dir : ${pkgDir}`);
|
|
99
|
+
console.log(`memory dir : ${MEMORY_DIR}`);
|
|
100
|
+
|
|
101
|
+
runForTargets(targets, 'install');
|
|
102
|
+
|
|
103
|
+
console.log(`\nDone. Triggers:`);
|
|
104
|
+
console.log(` Claude Code /atdc:1-spec <需求>`);
|
|
105
|
+
console.log(` OpenCode /atdc:1-spec <需求>`);
|
|
106
|
+
console.log(` Codex atd:1-spec <需求>`);
|
|
107
|
+
console.log('');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cmdUninstall(args) {
|
|
111
|
+
const targets = parseTargets(args);
|
|
112
|
+
const pkgDir = resolvePackageDir();
|
|
113
|
+
|
|
114
|
+
runForTargets(targets, 'uninstall');
|
|
115
|
+
|
|
116
|
+
replaceAllInDir(path.join(pkgDir, 'commands'), pkgDir, '{{ATDC_DIR}}');
|
|
117
|
+
|
|
118
|
+
console.log('\nUninstall complete. Run `npm uninstall -g atdc` to remove package files.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function cmdStatus() {
|
|
122
|
+
console.log(`atdc dir : ${resolvePackageDir()}`);
|
|
123
|
+
|
|
124
|
+
for (const t of ALL_TARGETS) {
|
|
125
|
+
const cfg = TERMINAL_CONFIGS[t];
|
|
126
|
+
console.log(`\n${cfg.name}:`);
|
|
127
|
+
const r = installers[t].status();
|
|
128
|
+
if (!r.installed) {
|
|
129
|
+
console.log(` not detected`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
for (const [k, v] of Object.entries(r.details)) {
|
|
133
|
+
const icon = v === 'ok' ? '✓' : v === 'conflict' ? '⚠' : '✗';
|
|
134
|
+
console.log(` ${icon} ${k}: ${v}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
console.log('');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function cmdGuardrails() {
|
|
141
|
+
const pkgDir = resolvePackageDir();
|
|
142
|
+
const hookSrc = path.join(pkgDir, 'hooks', 'guardrails.sh');
|
|
143
|
+
|
|
144
|
+
let gitDir;
|
|
145
|
+
try {
|
|
146
|
+
const { execSync } = require('child_process');
|
|
147
|
+
gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
|
|
148
|
+
} catch {
|
|
149
|
+
console.log('Not a git repository. Run inside a git project.');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const hookPath = path.resolve(gitDir, 'hooks', 'pre-push');
|
|
154
|
+
|
|
155
|
+
if (exists(hookPath)) {
|
|
156
|
+
const { isSymlink, getSymlinkTarget } = require('../src/utils');
|
|
157
|
+
if (isSymlink(hookPath) && getSymlinkTarget(hookPath) === hookSrc) {
|
|
158
|
+
console.log('✓ git-guardrails already installed');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
console.log(`⚠ Existing pre-push hook found. Manual merge needed.`);
|
|
162
|
+
console.log(` Reference: ${hookSrc}`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fs.symlinkSync(hookSrc, hookPath);
|
|
167
|
+
console.log(`✓ git-guardrails installed → .git/hooks/pre-push`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function runScript(script, ...args) {
|
|
171
|
+
const { execSync } = require('child_process');
|
|
172
|
+
const pkgDir = resolvePackageDir();
|
|
173
|
+
const scriptPath = path.join(pkgDir, 'hooks', script);
|
|
174
|
+
if (!exists(scriptPath)) {
|
|
175
|
+
console.log(`Script not found: ${scriptPath}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const result = execSync(`sh "${scriptPath}" ${args.join(' ')}`, { encoding: 'utf8' });
|
|
180
|
+
console.log(result.trim());
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.error(e.stderr || e.stdout || e.message);
|
|
183
|
+
process.exit(e.status || 1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function findActiveChange() {
|
|
188
|
+
const changesDir = 'openspec/changes';
|
|
189
|
+
if (!exists(changesDir)) return null;
|
|
190
|
+
const entries = fs.readdirSync(changesDir).filter(e => {
|
|
191
|
+
if (e === 'archive') return false;
|
|
192
|
+
const stat = fs.statSync(path.join(changesDir, e));
|
|
193
|
+
return stat.isDirectory();
|
|
194
|
+
});
|
|
195
|
+
// prefer changes with .phase-state.json
|
|
196
|
+
const phaseState = entries.filter(e => exists(path.join(changesDir, e, '.phase-state.json')));
|
|
197
|
+
if (phaseState.length === 1) return phaseState[0];
|
|
198
|
+
if (phaseState.length > 1) return phaseState[0];
|
|
199
|
+
return entries.length === 1 ? entries[0] : (entries.length > 0 ? entries[0] : null);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function cmdGuard(args) {
|
|
203
|
+
const phase = args[0];
|
|
204
|
+
if (!phase) { console.log('Usage: atdc guard <phase>'); process.exit(1); }
|
|
205
|
+
const change = findActiveChange();
|
|
206
|
+
if (!change) { console.log('No active change found. Run atdc run first.'); process.exit(1); }
|
|
207
|
+
runScript('phase-guard.sh', change, phase);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function cmdValidate(args) {
|
|
211
|
+
const { execSync } = require('child_process');
|
|
212
|
+
const pkgDir = resolvePackageDir();
|
|
213
|
+
const validator = path.join(pkgDir, 'src', 'validators', 'state-validate.js');
|
|
214
|
+
if (!exists(validator)) { console.log('Validator not found. Install atdc first.'); process.exit(1); }
|
|
215
|
+
try {
|
|
216
|
+
const result = execSync(`node "${validator}"`, { encoding: 'utf8' });
|
|
217
|
+
console.log(result.trim());
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.error(e.stderr || e.stdout || e.message);
|
|
220
|
+
process.exit(e.status || 1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function cmdHandoff(args) {
|
|
225
|
+
const change = args[0] || findActiveChange();
|
|
226
|
+
if (!change) { console.log('Usage: atdc handoff <change-name>'); process.exit(1); }
|
|
227
|
+
runScript('handoff-compress.sh', change);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function cmdRun(args) {
|
|
231
|
+
let mode = 'full';
|
|
232
|
+
if (args.includes('--bugfix')) mode = 'bugfix';
|
|
233
|
+
else if (args.includes('--hotfix')) mode = 'hotfix';
|
|
234
|
+
else if (args.includes('--tweak')) mode = 'tweak';
|
|
235
|
+
|
|
236
|
+
const { execSync } = require('child_process');
|
|
237
|
+
const ts = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
238
|
+
const changeName = `${mode}-${ts}`;
|
|
239
|
+
const changeDir = `openspec/changes/${changeName}`;
|
|
240
|
+
|
|
241
|
+
mkdirp(changeDir);
|
|
242
|
+
mkdirp(`${changeDir}/specs`);
|
|
243
|
+
|
|
244
|
+
if (mode === 'bugfix') {
|
|
245
|
+
console.log('🐛 Bugfix mode: feedback loop → root cause → fix → post-mortem');
|
|
246
|
+
runScript('atd-state.sh', 'init', changeName, 'bugfix');
|
|
247
|
+
runScript('atd-state.sh', 'set', changeName, 'phase', '"build"');
|
|
248
|
+
} else if (mode === 'hotfix') {
|
|
249
|
+
console.log('⚡ Hotfix mode: skipping brainstorm + design');
|
|
250
|
+
runScript('atd-state.sh', 'init', changeName, 'hotfix');
|
|
251
|
+
runScript('atd-state.sh', 'set', changeName, 'phase', '"build"');
|
|
252
|
+
} else if (mode === 'tweak') {
|
|
253
|
+
console.log('🔧 Tweak mode: skipping brainstorm + full plan');
|
|
254
|
+
runScript('atd-state.sh', 'init', changeName, 'tweak');
|
|
255
|
+
runScript('atd-state.sh', 'set', changeName, 'phase', '"build"');
|
|
256
|
+
} else {
|
|
257
|
+
runScript('atd-state.sh', 'init', changeName, 'full');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log(`Change created: ${changeName} (mode: ${mode})`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function cmdArchive(args) {
|
|
264
|
+
const dryRun = args.includes('--dry-run');
|
|
265
|
+
const change = args.filter(a => a !== '--dry-run')[0] || findActiveChange();
|
|
266
|
+
if (!change) { console.log('Usage: atdc archive <change-name> [--dry-run]'); process.exit(1); }
|
|
267
|
+
const extra = dryRun ? '--dry-run' : '';
|
|
268
|
+
runScript('atd-archive.sh', extra, change);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function cmdHook(args) {
|
|
272
|
+
const sub = args[0];
|
|
273
|
+
if (sub !== 'install') { console.log('Usage: atdc hook install'); process.exit(1); }
|
|
274
|
+
const { execSync } = require('child_process');
|
|
275
|
+
const pkgDir = resolvePackageDir();
|
|
276
|
+
const guardPath = path.join(pkgDir, 'hooks', 'phase-write-guard.sh');
|
|
277
|
+
const evidencePath = path.join(pkgDir, 'hooks', 'evidence-hook.sh');
|
|
278
|
+
|
|
279
|
+
// Check OpenCode config paths
|
|
280
|
+
const configPaths = [
|
|
281
|
+
path.join(process.cwd(), '.opencode', 'config.json'),
|
|
282
|
+
path.join(process.cwd(), '.opencode', 'opencode.json'),
|
|
283
|
+
path.join(HOME, '.config', 'opencode', 'opencode.json'),
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
const configFile = configPaths.find(p => exists(p));
|
|
287
|
+
if (!configFile) {
|
|
288
|
+
console.log('No OpenCode config found. Skipping hook install.');
|
|
289
|
+
console.log('Supported paths: .opencode/config.json, .opencode/opencode.json, ~/.config/opencode/opencode.json');
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let config;
|
|
294
|
+
try {
|
|
295
|
+
config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
296
|
+
} catch {
|
|
297
|
+
console.log('Failed to parse OpenCode config file.');
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!config.hooks) config.hooks = {};
|
|
302
|
+
if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
|
|
303
|
+
|
|
304
|
+
// phase-write-guard (PreToolUse)
|
|
305
|
+
if (!config.hooks.PreToolUse.some(h => typeof h === 'string' && h.includes('phase-write-guard'))) {
|
|
306
|
+
config.hooks.PreToolUse.push(`sh ${guardPath}`);
|
|
307
|
+
console.log('✓ phase-write-guard hook installed');
|
|
308
|
+
} else {
|
|
309
|
+
console.log('✓ phase-write-guard hook already installed');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// evidence-hook (PostToolUse — fires AFTER writes, detects state changes)
|
|
313
|
+
if (!config.hooks.PostToolUse) config.hooks.PostToolUse = [];
|
|
314
|
+
if (!config.hooks.PostToolUse.some(h => typeof h === 'string' && h.includes('evidence-hook'))) {
|
|
315
|
+
config.hooks.PostToolUse.push(`sh ${evidencePath}`);
|
|
316
|
+
console.log('✓ evidence-hook installed (auto-records state changes)');
|
|
317
|
+
} else {
|
|
318
|
+
console.log('✓ evidence-hook already installed');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2) + '\n');
|
|
322
|
+
console.log(`✓ hooks updated → ${configFile}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function cmdEvidence(args) {
|
|
326
|
+
const sub = args[0];
|
|
327
|
+
const rest = args.slice(1);
|
|
328
|
+
|
|
329
|
+
if (!sub) {
|
|
330
|
+
console.log('Usage: atdc evidence {events|stats|timing|metrics|canvas|record|all} ...');
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
switch (sub) {
|
|
335
|
+
case 'events': {
|
|
336
|
+
const change = rest.filter(a => !a.startsWith('--'))[0] || findActiveChange();
|
|
337
|
+
if (!change) { console.log('No active change found.'); process.exit(1); }
|
|
338
|
+
const type = rest.find(a => a.startsWith('--type='))?.split('=')[1];
|
|
339
|
+
const limit = parseInt(rest.find(a => a.startsWith('--limit='))?.split('=')[1] || '50');
|
|
340
|
+
const dir = evidence.resolveChangeDir(change);
|
|
341
|
+
const events = evidence.listEvents(dir, { type, limit });
|
|
342
|
+
if (events.length === 0) {
|
|
343
|
+
console.log('No events found.');
|
|
344
|
+
} else {
|
|
345
|
+
events.forEach(({ event }) => {
|
|
346
|
+
console.log(`[${event.timestamp.slice(0, 19)}] ${event.event_type} · phase=${event.phase} · ${event.event_id.slice(0, 8)}`);
|
|
347
|
+
});
|
|
348
|
+
console.log(`\n${events.length} events total.`);
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case 'stats': {
|
|
353
|
+
const change = rest[0] || findActiveChange();
|
|
354
|
+
if (!change) { console.log('No active change found.'); process.exit(1); }
|
|
355
|
+
const dir = evidence.resolveChangeDir(change);
|
|
356
|
+
const stats = evidence.eventStats(dir);
|
|
357
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case 'timing': {
|
|
361
|
+
const change = rest[0] || findActiveChange();
|
|
362
|
+
if (!change) { console.log('No active change found.'); process.exit(1); }
|
|
363
|
+
const dir = evidence.resolveChangeDir(change);
|
|
364
|
+
const report = evidence.stageReport(dir);
|
|
365
|
+
console.log(JSON.stringify(report, null, 2));
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
case 'metrics': {
|
|
369
|
+
const change = rest.filter(a => !a.startsWith('--'))[0] || findActiveChange();
|
|
370
|
+
if (!change) { console.log('No active change found.'); process.exit(1); }
|
|
371
|
+
const dir = evidence.resolveChangeDir(change);
|
|
372
|
+
if (rest.includes('--snapshot')) {
|
|
373
|
+
const result = evidence.captureDraftSnapshot(dir);
|
|
374
|
+
console.log(JSON.stringify(result, null, 2));
|
|
375
|
+
} else if (rest.includes('--commit')) {
|
|
376
|
+
const result = evidence.captureCommitDiff(dir);
|
|
377
|
+
console.log(JSON.stringify(result, null, 2));
|
|
378
|
+
} else {
|
|
379
|
+
const report = evidence.metricsReport(dir);
|
|
380
|
+
console.log(JSON.stringify(report, null, 2));
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
case 'canvas': {
|
|
385
|
+
const change = rest.filter(a => !a.startsWith('--'))[0] || findActiveChange();
|
|
386
|
+
if (!change) { console.log('No active change found.'); process.exit(1); }
|
|
387
|
+
const outArg = rest.find(a => a.startsWith('--out='));
|
|
388
|
+
const outPath = outArg ? outArg.split('=')[1] : null;
|
|
389
|
+
const dir = evidence.resolveChangeDir(change);
|
|
390
|
+
const dest = evidence.writeCanvas(dir, outPath);
|
|
391
|
+
console.log(`Canvas written to: ${dest}`);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case 'record': {
|
|
395
|
+
const change = rest[0];
|
|
396
|
+
const type = rest[1];
|
|
397
|
+
if (!change || !type) {
|
|
398
|
+
console.log('Usage: atdc evidence record <change> <type> [--phase=<p>] [--key=value...]');
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
const dir = evidence.resolveChangeDir(change);
|
|
402
|
+
const phase = rest.find(a => a.startsWith('--phase='))?.split('=')[1] || 'unknown';
|
|
403
|
+
const metadata = {};
|
|
404
|
+
rest.forEach(a => {
|
|
405
|
+
if (a.startsWith('--') && a.includes('=') && !a.startsWith('--phase=')) {
|
|
406
|
+
const [k, v] = a.slice(2).split('=');
|
|
407
|
+
metadata[k] = v;
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
const result = evidence.writeEvent(dir, type, metadata, { change, phase });
|
|
411
|
+
console.log(`Event recorded: ${result.filepath}`);
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
case 'all': {
|
|
415
|
+
const change = rest[0] || findActiveChange();
|
|
416
|
+
if (!change) { console.log('No active change found.'); process.exit(1); }
|
|
417
|
+
const dir = evidence.resolveChangeDir(change);
|
|
418
|
+
const result = evidence.allEvidence(change);
|
|
419
|
+
console.log(JSON.stringify(result, null, 2));
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case 'link':
|
|
423
|
+
cmdEvidenceLink();
|
|
424
|
+
break;
|
|
425
|
+
default:
|
|
426
|
+
console.log('Usage: atdc evidence {events|stats|timing|metrics|canvas|record|all|link} ...');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function cmdEvidenceLink() {
|
|
431
|
+
const linkScript = path.join(HOME, 'atdc', 'central', 'link.sh');
|
|
432
|
+
if (!exists(linkScript)) { console.log('~/atdc/central/link.sh not found'); process.exit(1); }
|
|
433
|
+
const { execSync } = require('child_process');
|
|
434
|
+
try {
|
|
435
|
+
const result = execSync(`sh "${linkScript}" --status`, { encoding: 'utf8' });
|
|
436
|
+
console.log(result.trim());
|
|
437
|
+
execSync(`sh "${linkScript}"`, { encoding: 'utf8', stdio: 'inherit' });
|
|
438
|
+
} catch (e) {
|
|
439
|
+
console.error(e.stderr || e.message);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const [,, cmd, ...args] = process.argv;
|
|
445
|
+
|
|
446
|
+
switch (cmd) {
|
|
447
|
+
case 'install': cmdInstall(args); break;
|
|
448
|
+
case 'uninstall': cmdUninstall(args); break;
|
|
449
|
+
case 'status': cmdStatus(); break;
|
|
450
|
+
case 'guardrails': cmdGuardrails(); break;
|
|
451
|
+
case 'guard': cmdGuard(args); break;
|
|
452
|
+
case 'validate': cmdValidate(args); break;
|
|
453
|
+
case 'handoff': cmdHandoff(args); break;
|
|
454
|
+
case 'run': cmdRun(args); break;
|
|
455
|
+
case 'archive': cmdArchive(args); break;
|
|
456
|
+
case 'hook': cmdHook(args); break;
|
|
457
|
+
case 'ponytail': cmdPonytail(args); break;
|
|
458
|
+
case 'evidence': cmdEvidence(args); break;
|
|
459
|
+
default: usage(); break;
|
|
460
|
+
}
|
package/bin/cls.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const client = require('../src/cls/client');
|
|
5
|
+
|
|
6
|
+
const HELP = `cls — CLS 日志查询 CLI
|
|
7
|
+
|
|
8
|
+
用法:
|
|
9
|
+
cls --init 初始化配置文件
|
|
10
|
+
cls --list-accounts 列出可用账号
|
|
11
|
+
cls <account> <query> [options] 查询 CLS 日志
|
|
12
|
+
|
|
13
|
+
选项:
|
|
14
|
+
--limit N 返回条数(默认 200,最大 1000)
|
|
15
|
+
--out DIR 输出目录(默认当前目录)
|
|
16
|
+
--from TIME 开始时间(默认 15m 前,支持 "2026-07-17 10:00")
|
|
17
|
+
--to TIME 结束时间(默认当前)
|
|
18
|
+
--type TYPE topic 类型(默认 error)
|
|
19
|
+
--regions R 地域(默认 gz,bj)
|
|
20
|
+
--format FORMAT 输出格式 json|jsonl(默认 jsonl)
|
|
21
|
+
--fetch 全量落地(调 cls_fetch_bulk.py)
|
|
22
|
+
--max N 全量最大条数(默认 5000)
|
|
23
|
+
--quiet 静默模式(不输出进度)
|
|
24
|
+
--help 显示此帮助
|
|
25
|
+
|
|
26
|
+
示例:
|
|
27
|
+
cls tcs "callTencentMusicAPI AND done"
|
|
28
|
+
cls tcs "order AND pay*" --limit 500 --out /tmp/logs/
|
|
29
|
+
cls tcs "order AND pay*" --fetch --max 10000`;
|
|
30
|
+
|
|
31
|
+
function usage() {
|
|
32
|
+
console.log(HELP);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fail(msg) {
|
|
37
|
+
console.error(`cls: ${msg}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const args = process.argv.slice(2);
|
|
42
|
+
|
|
43
|
+
if (args.length === 0 || args.includes('--help')) {
|
|
44
|
+
if (args.length === 0) {
|
|
45
|
+
try { client.loadConfig(); } catch { client.initConfig(); console.log('配置已生成: ~/.cls/config.json'); process.exit(0); }
|
|
46
|
+
}
|
|
47
|
+
usage();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (args[0] === '--init') {
|
|
51
|
+
const r = client.initConfig();
|
|
52
|
+
console.log(r.msg);
|
|
53
|
+
process.exit(r.ok ? 0 : 1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (args[0] === '--list-accounts') {
|
|
57
|
+
try {
|
|
58
|
+
const accounts = client.listAccounts();
|
|
59
|
+
console.log('可用账号:');
|
|
60
|
+
Object.entries(accounts).forEach(([alias, name]) => console.log(` ${alias} → ${name}`));
|
|
61
|
+
} catch (e) { fail(e.message); }
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const [account, query] = args;
|
|
66
|
+
if (!account || !query) fail('用法: cls <account> <query> [options]');
|
|
67
|
+
|
|
68
|
+
const opts = {};
|
|
69
|
+
for (let i = 2; i < args.length; i++) {
|
|
70
|
+
const a = args[i];
|
|
71
|
+
if (a === '--limit') opts.limit = parseInt(args[++i]);
|
|
72
|
+
else if (a === '--out') opts.out_dir = args[++i];
|
|
73
|
+
else if (a === '--from') opts.from = args[++i];
|
|
74
|
+
else if (a === '--to') opts.to = args[++i];
|
|
75
|
+
else if (a === '--type') opts.type = args[++i];
|
|
76
|
+
else if (a === '--regions') opts.regions = args[++i];
|
|
77
|
+
else if (a === '--format') opts.format = args[++i];
|
|
78
|
+
else if (a === '--max') opts.max = parseInt(args[++i]);
|
|
79
|
+
else if (a === '--fetch') opts.fetch = true;
|
|
80
|
+
else if (a === '--quiet') opts.quiet = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
if (opts.fetch) {
|
|
85
|
+
const result = client.fetchSync(account, query, opts);
|
|
86
|
+
console.log(`→ ${result.output_file} (${result.total} 条)`);
|
|
87
|
+
} else {
|
|
88
|
+
const result = client.searchSync(account, query, opts);
|
|
89
|
+
console.log(`→ ${result.output_file} (${result.count} 条)`);
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
if (e.message && e.message.includes('配置文件') && e.message.includes('不存在')) {
|
|
93
|
+
client.initConfig();
|
|
94
|
+
console.log('配置已生成: ~/.cls/config.json,请重试命令');
|
|
95
|
+
} else {
|
|
96
|
+
fail(e.stderr || e.message);
|
|
97
|
+
}
|
|
98
|
+
}
|
package/central/link.sh
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ~/atdc/central/link.sh — unified worktree symlink manager
|
|
3
|
+
# Called from GO.md Step 0b at every session start. Idempotent.
|
|
4
|
+
#
|
|
5
|
+
# Pattern:
|
|
6
|
+
# Main repo → hosts real dirs
|
|
7
|
+
# Worktree → symlink to main repo's dirs
|
|
8
|
+
# Already linked → skip
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
DIRS=(
|
|
13
|
+
.code-review-graph
|
|
14
|
+
.codegraph
|
|
15
|
+
.hc
|
|
16
|
+
.test_plans
|
|
17
|
+
.handoffs
|
|
18
|
+
.evidence
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# ── Find main worktree ──
|
|
22
|
+
MAIN=$(git worktree list --porcelain 2>/dev/null | awk '
|
|
23
|
+
/^worktree / { p=$2 }
|
|
24
|
+
/^bare / { bare=($2=="true") }
|
|
25
|
+
/^HEAD / { head=$2 }
|
|
26
|
+
/^$/ { if(bare!="true" && p!="") print p; p=""; bare=""; head="" }
|
|
27
|
+
' | head -1)
|
|
28
|
+
|
|
29
|
+
[ -z "$MAIN" ] && MAIN=$(git worktree list 2>/dev/null | head -1 | awk '{print $1}')
|
|
30
|
+
[ -z "$MAIN" ] && exit 0
|
|
31
|
+
|
|
32
|
+
CURRENT="$(pwd)"
|
|
33
|
+
|
|
34
|
+
# ── Link each dir ──
|
|
35
|
+
for dir in "${DIRS[@]}"; do
|
|
36
|
+
# Already valid symlink → skip
|
|
37
|
+
if [ -L "$dir" ]; then
|
|
38
|
+
[ -d "$(readlink "$dir")" ] && continue
|
|
39
|
+
rm -f "$dir"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Main worktree: ensure real dir exists (hosting)
|
|
43
|
+
if [ "$CURRENT" = "$MAIN" ]; then
|
|
44
|
+
[ -d "$dir" ] || mkdir -p "$dir"
|
|
45
|
+
continue
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Worktree: main must have dir, then symlink
|
|
49
|
+
[ -d "$MAIN/$dir" ] || mkdir -p "$MAIN/$dir"
|
|
50
|
+
|
|
51
|
+
# Migrate stray real dir → main
|
|
52
|
+
if [ -d "$dir" ] && [ ! -L "$dir" ]; then
|
|
53
|
+
mv "$dir"/* "$MAIN/$dir/" 2>/dev/null || true
|
|
54
|
+
rm -rf "$dir"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
ln -s "$MAIN/$dir" "$dir"
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
exit 0
|