@conscience-technology/ctds 0.0.0-stage → 0.1.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 +128 -2
- package/assets/components/AppFrame/AppFrame.module.css +177 -0
- package/assets/components/AppFrame/AppFrame.tsx +93 -0
- package/assets/components/AppFrame/index.ts +6 -0
- package/assets/components/AttachmentChip/AttachmentChip.module.css +71 -0
- package/assets/components/AttachmentChip/AttachmentChip.tsx +74 -0
- package/assets/components/AttachmentChip/index.ts +2 -0
- package/assets/components/AttachmentThumb/AttachmentThumb.module.css +107 -0
- package/assets/components/AttachmentThumb/AttachmentThumb.tsx +91 -0
- package/assets/components/AttachmentThumb/index.ts +2 -0
- package/assets/components/Avatar/Avatar.module.css +167 -0
- package/assets/components/Avatar/Avatar.tsx +228 -0
- package/assets/components/Avatar/index.ts +2 -0
- package/assets/components/AvatarUpload/AvatarUpload.module.css +130 -0
- package/assets/components/AvatarUpload/AvatarUpload.tsx +139 -0
- package/assets/components/AvatarUpload/index.ts +2 -0
- package/assets/components/Badge/Badge.module.css +76 -0
- package/assets/components/Badge/Badge.tsx +68 -0
- package/assets/components/Badge/index.ts +2 -0
- package/assets/components/Breadcrumb/Breadcrumb.module.css +213 -0
- package/assets/components/Breadcrumb/Breadcrumb.tsx +279 -0
- package/assets/components/Breadcrumb/index.ts +8 -0
- package/assets/components/Button/Button.module.css +216 -0
- package/assets/components/Button/Button.tsx +149 -0
- package/assets/components/Button/index.ts +2 -0
- package/assets/components/Callout/Callout.module.css +146 -0
- package/assets/components/Callout/Callout.tsx +138 -0
- package/assets/components/Callout/index.ts +2 -0
- package/assets/components/ChatInput/ChatInput.module.css +241 -0
- package/assets/components/ChatInput/ChatInput.tsx +322 -0
- package/assets/components/ChatInput/ModelSelector.tsx +40 -0
- package/assets/components/ChatInput/SendButton.tsx +41 -0
- package/assets/components/ChatInput/index.ts +10 -0
- package/assets/components/Checkbox/Checkbox.module.css +66 -0
- package/assets/components/Checkbox/Checkbox.tsx +36 -0
- package/assets/components/Checkbox/index.ts +2 -0
- package/assets/components/Chip/Chip.module.css +128 -0
- package/assets/components/Chip/Chip.tsx +103 -0
- package/assets/components/Chip/index.ts +2 -0
- package/assets/components/ChoiceCard/ChoiceCard.module.css +142 -0
- package/assets/components/ChoiceCard/ChoiceCard.tsx +132 -0
- package/assets/components/ChoiceCard/index.ts +2 -0
- package/assets/components/ContextMenu/ContextMenu.module.css +102 -0
- package/assets/components/ContextMenu/ContextMenu.tsx +392 -0
- package/assets/components/ContextMenu/index.ts +9 -0
- package/assets/components/DatePicker/Calendar.tsx +234 -0
- package/assets/components/DatePicker/DESIGN_NOTES.md +106 -0
- package/assets/components/DatePicker/DESIGN_NOTES_TIME.md +125 -0
- package/assets/components/DatePicker/DatePicker.module.css +295 -0
- package/assets/components/DatePicker/DatePicker.tsx +387 -0
- package/assets/components/DatePicker/TimeSelector.tsx +69 -0
- package/assets/components/DatePicker/index.ts +8 -0
- package/assets/components/DatePicker/presets.ts +80 -0
- package/assets/components/DatePicker/utils.ts +148 -0
- package/assets/components/Disclosure/Disclosure.module.css +73 -0
- package/assets/components/Disclosure/Disclosure.tsx +129 -0
- package/assets/components/Disclosure/index.ts +6 -0
- package/assets/components/Drawer/Drawer.module.css +257 -0
- package/assets/components/Drawer/Drawer.tsx +126 -0
- package/assets/components/Drawer/index.ts +7 -0
- package/assets/components/EmptyState/EmptyState.module.css +102 -0
- package/assets/components/EmptyState/EmptyState.tsx +80 -0
- package/assets/components/EmptyState/index.ts +2 -0
- package/assets/components/FileIcon/FileIcon.module.css +66 -0
- package/assets/components/FileIcon/FileIcon.tsx +84 -0
- package/assets/components/FileIcon/fileExtMap.ts +112 -0
- package/assets/components/FileIcon/index.ts +9 -0
- package/assets/components/FilterButton/FilterButton.module.css +130 -0
- package/assets/components/FilterButton/FilterButton.tsx +94 -0
- package/assets/components/FilterButton/index.ts +2 -0
- package/assets/components/GalleryItem/GalleryItem.module.css +220 -0
- package/assets/components/GalleryItem/GalleryItem.tsx +163 -0
- package/assets/components/GalleryItem/index.ts +7 -0
- package/assets/components/Icon/Icon.tsx +616 -0
- package/assets/components/Icon/context.tsx +39 -0
- package/assets/components/Icon/index.ts +4 -0
- package/assets/components/IconButton/IconButton.module.css +109 -0
- package/assets/components/IconButton/IconButton.tsx +110 -0
- package/assets/components/IconButton/index.ts +2 -0
- package/assets/components/Input/Input.module.css +338 -0
- package/assets/components/Input/Input.tsx +371 -0
- package/assets/components/Input/index.ts +2 -0
- package/assets/components/ListItem/ListItem.module.css +318 -0
- package/assets/components/ListItem/ListItem.tsx +262 -0
- package/assets/components/ListItem/index.ts +9 -0
- package/assets/components/MenuItem/MenuItem.module.css +437 -0
- package/assets/components/MenuItem/MenuItem.tsx +529 -0
- package/assets/components/MenuItem/index.ts +8 -0
- package/assets/components/MetricCard/MetricCard.module.css +153 -0
- package/assets/components/MetricCard/MetricCard.tsx +140 -0
- package/assets/components/MetricCard/index.ts +2 -0
- package/assets/components/Modal/Modal.module.css +175 -0
- package/assets/components/Modal/Modal.tsx +98 -0
- package/assets/components/Modal/index.ts +2 -0
- package/assets/components/Onboarding/DocsIndex.tsx +82 -0
- package/assets/components/Onboarding/LayoutBuilder.tsx +160 -0
- package/assets/components/Onboarding/Onboarding.module.css +1394 -0
- package/assets/components/Onboarding/OnboardingAxisPanel.tsx +515 -0
- package/assets/components/Onboarding/OnboardingPage.tsx +1193 -0
- package/assets/components/Onboarding/PresetsGallery.tsx +65 -0
- package/assets/components/Onboarding/actions.ts +250 -0
- package/assets/components/Onboarding/index.ts +8 -0
- package/assets/components/Onboarding/palette.css +40 -0
- package/assets/components/Onboarding/wizardData.ts +265 -0
- package/assets/components/Pagination/Pagination.module.css +82 -0
- package/assets/components/Pagination/Pagination.tsx +248 -0
- package/assets/components/Pagination/index.ts +6 -0
- package/assets/components/ProgressBar/ProgressBar.module.css +64 -0
- package/assets/components/ProgressBar/ProgressBar.tsx +54 -0
- package/assets/components/ProgressBar/index.ts +2 -0
- package/assets/components/QuotationBar/QuotationBar.module.css +61 -0
- package/assets/components/QuotationBar/QuotationBar.tsx +48 -0
- package/assets/components/QuotationBar/index.ts +2 -0
- package/assets/components/Radio/Radio.module.css +63 -0
- package/assets/components/Radio/Radio.tsx +33 -0
- package/assets/components/Radio/index.ts +2 -0
- package/assets/components/Select/Select.module.css +151 -0
- package/assets/components/Select/Select.tsx +425 -0
- package/assets/components/Select/index.ts +10 -0
- package/assets/components/SelectableCard/SelectableCard.module.css +177 -0
- package/assets/components/SelectableCard/SelectableCard.tsx +83 -0
- package/assets/components/SelectableCard/index.ts +2 -0
- package/assets/components/Shortcut/Shortcut.module.css +56 -0
- package/assets/components/Shortcut/Shortcut.tsx +156 -0
- package/assets/components/Shortcut/index.ts +2 -0
- package/assets/components/Sidebar/Sidebar.module.css +242 -0
- package/assets/components/Sidebar/Sidebar.tsx +264 -0
- package/assets/components/Sidebar/index.ts +7 -0
- package/assets/components/SidebarSplit/SidebarSplit.module.css +126 -0
- package/assets/components/SidebarSplit/SidebarSplit.tsx +222 -0
- package/assets/components/SidebarSplit/index.ts +6 -0
- package/assets/components/Slider/Slider.module.css +120 -0
- package/assets/components/Slider/Slider.tsx +88 -0
- package/assets/components/Slider/index.ts +2 -0
- package/assets/components/SplitButton/SplitButton.module.css +190 -0
- package/assets/components/SplitButton/SplitButton.tsx +145 -0
- package/assets/components/SplitButton/index.ts +6 -0
- package/assets/components/Stepper/Stepper.module.css +182 -0
- package/assets/components/Stepper/Stepper.tsx +185 -0
- package/assets/components/Stepper/index.ts +2 -0
- package/assets/components/Switch/Switch.module.css +116 -0
- package/assets/components/Switch/Switch.tsx +58 -0
- package/assets/components/Switch/index.ts +2 -0
- package/assets/components/TableCell/TableCell.module.css +137 -0
- package/assets/components/TableCell/TableCellAvatar.tsx +42 -0
- package/assets/components/TableCell/TableCellBadge.tsx +23 -0
- package/assets/components/TableCell/TableCellIcon.tsx +39 -0
- package/assets/components/TableCell/TableCellImage.tsx +49 -0
- package/assets/components/TableCell/TableCellLink.tsx +33 -0
- package/assets/components/TableCell/TableCellMeter.tsx +55 -0
- package/assets/components/TableCell/TableCellNumber.tsx +43 -0
- package/assets/components/TableCell/TableCellText.tsx +30 -0
- package/assets/components/TableCell/index.ts +23 -0
- package/assets/components/TableHeader/TableHeader.module.css +287 -0
- package/assets/components/TableHeader/TableHeader.tsx +590 -0
- package/assets/components/TableHeader/index.ts +9 -0
- package/assets/components/TableRow/TableRow.module.css +290 -0
- package/assets/components/TableRow/TableRow.tsx +253 -0
- package/assets/components/TableRow/index.ts +8 -0
- package/assets/components/TableRowEdit/TableRowEdit.module.css +126 -0
- package/assets/components/TableRowEdit/TableRowEdit.tsx +247 -0
- package/assets/components/TableRowEdit/index.ts +2 -0
- package/assets/components/TableRowSelect/TableRowSelect.module.css +23 -0
- package/assets/components/TableRowSelect/TableRowSelect.tsx +175 -0
- package/assets/components/TableRowSelect/index.ts +2 -0
- package/assets/components/Tabs/Tabs.module.css +234 -0
- package/assets/components/Tabs/Tabs.tsx +426 -0
- package/assets/components/Tabs/index.ts +9 -0
- package/assets/components/Textarea/Textarea.module.css +318 -0
- package/assets/components/Textarea/Textarea.tsx +426 -0
- package/assets/components/Textarea/index.ts +7 -0
- package/assets/components/ThemeController.tsx +271 -0
- package/assets/components/TimePicker/TimePicker.module.css +317 -0
- package/assets/components/TimePicker/TimePicker.tsx +821 -0
- package/assets/components/TimePicker/index.ts +8 -0
- package/assets/components/TimePicker/utils.ts +115 -0
- package/assets/components/Toast/Toast.module.css +264 -0
- package/assets/components/Toast/Toast.tsx +243 -0
- package/assets/components/Toast/index.ts +15 -0
- package/assets/components/Toolbar/Toolbar.module.css +72 -0
- package/assets/components/Toolbar/Toolbar.tsx +75 -0
- package/assets/components/Toolbar/index.ts +2 -0
- package/assets/components/Tooltip/Tooltip.module.css +42 -0
- package/assets/components/Tooltip/Tooltip.tsx +332 -0
- package/assets/components/Tooltip/index.ts +2 -0
- package/assets/components/Topbar/Topbar.module.css +26 -0
- package/assets/components/Topbar/Topbar.tsx +47 -0
- package/assets/components/Topbar/index.ts +2 -0
- package/assets/components/_cn.ts +3 -0
- package/assets/components/_color.ts +48 -0
- package/assets/components/_data.ts +659 -0
- package/assets/components/_utils.css +19 -0
- package/assets/components/axisOptions.ts +24 -0
- package/assets/components/tokens.css +742 -0
- package/assets/prompts/GUIDE.md +251 -0
- package/assets/prompts/OVERRIDE.md +170 -0
- package/assets/prompts/PRINCIPLES.md +130 -0
- package/assets/prompts/axes/density.md +38 -0
- package/assets/prompts/axes/font.md +36 -0
- package/assets/prompts/axes/hue.md +36 -0
- package/assets/prompts/axes/icon.md +48 -0
- package/assets/prompts/axes/mode.md +50 -0
- package/assets/prompts/axes/motion.md +77 -0
- package/assets/prompts/axes/palette.md +35 -0
- package/assets/prompts/axes/shape.md +33 -0
- package/assets/prompts/axes/voice.md +54 -0
- package/assets/prompts/components/README.md +84 -0
- package/assets/prompts/components/app-frame.md +94 -0
- package/assets/prompts/components/attachment-chip.md +46 -0
- package/assets/prompts/components/attachment-thumb.md +49 -0
- package/assets/prompts/components/avatar.md +86 -0
- package/assets/prompts/components/badge.md +104 -0
- package/assets/prompts/components/breadcrumb.md +77 -0
- package/assets/prompts/components/button.md +99 -0
- package/assets/prompts/components/callout.md +81 -0
- package/assets/prompts/components/chat-input.md +84 -0
- package/assets/prompts/components/checkbox.md +46 -0
- package/assets/prompts/components/chip.md +84 -0
- package/assets/prompts/components/choice-card.md +127 -0
- package/assets/prompts/components/context-menu.md +145 -0
- package/assets/prompts/components/date-picker.md +104 -0
- package/assets/prompts/components/drawer.md +152 -0
- package/assets/prompts/components/empty-state.md +69 -0
- package/assets/prompts/components/file-icon.md +73 -0
- package/assets/prompts/components/filter-button.md +42 -0
- package/assets/prompts/components/form-field.md +74 -0
- package/assets/prompts/components/gallery-item.md +158 -0
- package/assets/prompts/components/icon-button.md +82 -0
- package/assets/prompts/components/icon.md +68 -0
- package/assets/prompts/components/input.md +95 -0
- package/assets/prompts/components/list-item.md +235 -0
- package/assets/prompts/components/menu-item.md +234 -0
- package/assets/prompts/components/metric-card.md +132 -0
- package/assets/prompts/components/modal.md +131 -0
- package/assets/prompts/components/pagination.md +92 -0
- package/assets/prompts/components/progress-bar.md +61 -0
- package/assets/prompts/components/quotation-bar.md +39 -0
- package/assets/prompts/components/radio.md +41 -0
- package/assets/prompts/components/select.md +104 -0
- package/assets/prompts/components/selectable-card.md +67 -0
- package/assets/prompts/components/shortcut.md +76 -0
- package/assets/prompts/components/sidebar.md +107 -0
- package/assets/prompts/components/slider.md +89 -0
- package/assets/prompts/components/split-button.md +71 -0
- package/assets/prompts/components/stepper.md +93 -0
- package/assets/prompts/components/switch.md +86 -0
- package/assets/prompts/components/table-cell.md +87 -0
- package/assets/prompts/components/table-header.md +157 -0
- package/assets/prompts/components/table-row-edit.md +129 -0
- package/assets/prompts/components/table-row-select.md +82 -0
- package/assets/prompts/components/table-row.md +156 -0
- package/assets/prompts/components/tabs.md +122 -0
- package/assets/prompts/components/textarea.md +105 -0
- package/assets/prompts/components/toast.md +149 -0
- package/assets/prompts/components/tooltip.md +71 -0
- package/assets/prompts/components/topbar.md +148 -0
- package/assets/prompts/primitives/emerge.md +43 -0
- package/assets/prompts/primitives/fade.md +31 -0
- package/assets/prompts/primitives/scale.md +32 -0
- package/assets/prompts/primitives/scroll.md +75 -0
- package/assets/prompts/primitives/slide.md +31 -0
- package/assets/prompts/themes/_SCHEMA.md +135 -0
- package/assets/prompts/themes/_TEMPLATE.md +73 -0
- package/assets/prompts/themes/expressive.md +118 -0
- package/assets/prompts/themes/pure.md +87 -0
- package/assets/prompts/themes/spare.md +73 -0
- package/assets/prompts/tokens/breakpoint.md +29 -0
- package/assets/prompts/tokens/color.md +78 -0
- package/assets/prompts/tokens/radius.md +33 -0
- package/assets/prompts/tokens/shadow.md +35 -0
- package/assets/prompts/tokens/spacing.md +86 -0
- package/assets/prompts/tokens/typography.md +76 -0
- package/assets/prompts/tokens/z-index.md +35 -0
- package/assets/public/expressive.png +0 -0
- package/assets/public/pure.png +0 -0
- package/assets/public/spare.png +0 -0
- package/assets/skill/SKILL.md +36 -0
- package/bin/ctds.js +76 -0
- package/package.json +35 -4
- package/src/commands/init.js +215 -0
- package/src/commands/update.js +275 -0
- package/src/utils/assets.js +78 -0
- package/src/utils/deps.js +51 -0
- package/src/utils/detect.js +47 -0
- package/src/utils/diff.js +52 -0
- package/src/utils/hash.js +16 -0
- package/src/utils/log.js +41 -0
- package/src/utils/meta.js +73 -0
- package/src/utils/prompt.js +28 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ctds update` — 설치된 CTDS 자산을 최신으로 교체.
|
|
3
|
+
*
|
|
4
|
+
* 하는 일:
|
|
5
|
+
* 1) components/ctds/.ctds-meta.json 로드. 없으면 "init으로 재설치" 안내 + exit.
|
|
6
|
+
* 2) components/ctds — 해시 비교로 3단 큐 분류:
|
|
7
|
+
* - 신규 (사용자 쪽에 없음) → 추가
|
|
8
|
+
* - 안전 (해시 매치 또는 번들과 동일) → 조용히 덮어쓰기
|
|
9
|
+
* - 충돌 (사용자 수정) → prompt (y/n/d/A/N/q) 또는 --force 전부 덮어쓰기
|
|
10
|
+
* 3) prompts/, .claude/skills/ctds/, public/onboarding/ — 조용히 전부 덮어쓰기
|
|
11
|
+
* 4) components/ctds/.ctds-meta.json 재생성
|
|
12
|
+
*
|
|
13
|
+
* 플래그:
|
|
14
|
+
* --dry 쓰기 없이 변경·충돌 보고만
|
|
15
|
+
* --force 수정 감지 무시, 전부 덮어쓰기 (git 커밋 후에만)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
cpSync,
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
rmSync,
|
|
25
|
+
statSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
import { detectProject } from "../utils/detect.js";
|
|
31
|
+
import { getAssetSources } from "../utils/assets.js";
|
|
32
|
+
import { hashFile } from "../utils/hash.js";
|
|
33
|
+
import { loadMeta, writeMeta, computeFilesMap, rebuildMeta } from "../utils/meta.js";
|
|
34
|
+
import { askConflict } from "../utils/prompt.js";
|
|
35
|
+
import { renderDiff } from "../utils/diff.js";
|
|
36
|
+
import { c, step, success, warn, info, kv } from "../utils/log.js";
|
|
37
|
+
|
|
38
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const CLI_PKG_PATH = join(__dirname, "..", "..", "package.json");
|
|
40
|
+
function readCliVersion() {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(readFileSync(CLI_PKG_PATH, "utf8")).version;
|
|
43
|
+
} catch {
|
|
44
|
+
return "unknown";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const META_NAME = ".ctds-meta.json";
|
|
49
|
+
|
|
50
|
+
export async function run(args) {
|
|
51
|
+
const dry = args.includes("--dry");
|
|
52
|
+
const force = args.includes("--force");
|
|
53
|
+
|
|
54
|
+
console.log("");
|
|
55
|
+
console.log(c.bold("CTDS 업데이트"));
|
|
56
|
+
console.log(c.dim("—").repeat(40));
|
|
57
|
+
|
|
58
|
+
const cwd = process.cwd();
|
|
59
|
+
const project = detectProject(cwd);
|
|
60
|
+
if (!project.ok) {
|
|
61
|
+
console.error(c.red("✗"), project.reason);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const componentsDir = resolve(
|
|
66
|
+
cwd,
|
|
67
|
+
project.useSrcDir ? "src/components/ctds" : "components/ctds",
|
|
68
|
+
);
|
|
69
|
+
const promptsDir = resolve(cwd, "prompts");
|
|
70
|
+
const skillDir = resolve(cwd, ".claude/skills/ctds");
|
|
71
|
+
const publicDir = resolve(cwd, "public/onboarding");
|
|
72
|
+
|
|
73
|
+
// 1) 메타 로드
|
|
74
|
+
const meta = loadMeta(componentsDir);
|
|
75
|
+
if (!meta) {
|
|
76
|
+
console.error(c.red("✗"), `${META_NAME}이 없습니다.`);
|
|
77
|
+
console.error("");
|
|
78
|
+
console.error(" 이 프로젝트는 ctds init으로 설치되지 않았거나 구버전입니다.");
|
|
79
|
+
console.error(" `npx ctds init --force`로 재설치하거나 수동으로 생성 후 다시 시도하세요.");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
kv("프로젝트", project.name);
|
|
83
|
+
kv("설치 버전", meta.version);
|
|
84
|
+
kv("CLI 버전", readCliVersion());
|
|
85
|
+
if (dry) kv("모드", c.yellow("dry run (쓰기 없음)"));
|
|
86
|
+
if (force) kv("모드", c.yellow("force (충돌 무시, 전부 덮어쓰기)"));
|
|
87
|
+
console.log("");
|
|
88
|
+
|
|
89
|
+
// 2) 자산 소스 로드
|
|
90
|
+
const assets = getAssetSources();
|
|
91
|
+
info(`자산 소스: ${assets.mode === "bundled" ? "번들" : "로컬 레포"}`);
|
|
92
|
+
console.log("");
|
|
93
|
+
|
|
94
|
+
// 3) components 비교 — 3단 큐 분류
|
|
95
|
+
const sourceComponents = assets.components;
|
|
96
|
+
if (!existsSync(sourceComponents)) {
|
|
97
|
+
console.error(c.red("✗"), `번들 components 소스를 찾을 수 없습니다: ${sourceComponents}`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const bundleFiles = computeFilesMap(sourceComponents); // { relPath: "sha256:hex" }
|
|
102
|
+
const addQueue = []; // 사용자 쪽에 없음
|
|
103
|
+
const safeQueue = []; // 사용자가 안 건드림 (meta 해시 일치)
|
|
104
|
+
const conflictQueue = []; // 사용자 수정
|
|
105
|
+
|
|
106
|
+
for (const [relPath, bundleHash] of Object.entries(bundleFiles)) {
|
|
107
|
+
const userPath = join(componentsDir, relPath);
|
|
108
|
+
if (!existsSync(userPath)) {
|
|
109
|
+
addQueue.push(relPath);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const metaHash = meta.files?.[relPath];
|
|
113
|
+
const userHash = `sha256:${hashFile(userPath)}`;
|
|
114
|
+
if (!metaHash) {
|
|
115
|
+
// meta엔 없는데 파일은 있음 → 사용자가 수동 추가. 신규 번들 파일이 아니면 보존하는 게 안전.
|
|
116
|
+
// 이번 번들에도 있으면 사용자 커스텀 추가분으로 간주 — 충돌 처리.
|
|
117
|
+
conflictQueue.push({ relPath, userHash, bundleHash, reason: "untracked" });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (userHash === metaHash) {
|
|
121
|
+
// 사용자가 안 건드림
|
|
122
|
+
if (userHash !== bundleHash) safeQueue.push(relPath);
|
|
123
|
+
// 번들과 동일하면 아무것도 안 함
|
|
124
|
+
} else {
|
|
125
|
+
// 사용자가 수정함
|
|
126
|
+
if (userHash !== bundleHash) {
|
|
127
|
+
conflictQueue.push({ relPath, userHash, bundleHash, reason: "modified" });
|
|
128
|
+
}
|
|
129
|
+
// 수정했는데 번들과 동일한 경우(이론상 거의 없음) → 조용히 통과
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 번들에서 삭제된 파일(사용자 쪽에만 있음)은 보존 — 안전 측
|
|
134
|
+
const removedInBundle = Object.keys(meta.files ?? {}).filter((p) => !(p in bundleFiles));
|
|
135
|
+
|
|
136
|
+
// 요약
|
|
137
|
+
console.log(c.bold("요약"));
|
|
138
|
+
kv("추가", String(addQueue.length));
|
|
139
|
+
kv("안전 덮어쓰기", String(safeQueue.length));
|
|
140
|
+
kv("충돌", String(conflictQueue.length));
|
|
141
|
+
if (removedInBundle.length > 0) {
|
|
142
|
+
kv("번들에서 삭제됨 (보존)", String(removedInBundle.length));
|
|
143
|
+
}
|
|
144
|
+
console.log("");
|
|
145
|
+
|
|
146
|
+
if (dry) {
|
|
147
|
+
if (addQueue.length > 0) {
|
|
148
|
+
console.log(c.bold("추가 예정:"));
|
|
149
|
+
for (const p of addQueue) console.log(` + ${p}`);
|
|
150
|
+
console.log("");
|
|
151
|
+
}
|
|
152
|
+
if (safeQueue.length > 0) {
|
|
153
|
+
console.log(c.bold("안전 덮어쓰기 예정:"));
|
|
154
|
+
for (const p of safeQueue) console.log(` ~ ${p}`);
|
|
155
|
+
console.log("");
|
|
156
|
+
}
|
|
157
|
+
if (conflictQueue.length > 0) {
|
|
158
|
+
console.log(c.bold("충돌 (실제 실행 시 prompt):"));
|
|
159
|
+
for (const { relPath, reason } of conflictQueue) {
|
|
160
|
+
console.log(` ! ${relPath} ${c.dim(`(${reason})`)}`);
|
|
161
|
+
}
|
|
162
|
+
console.log("");
|
|
163
|
+
}
|
|
164
|
+
info("dry run — 실제 변경 없음.");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 4) 안전·추가 처리
|
|
169
|
+
for (const relPath of safeQueue) {
|
|
170
|
+
copyFile(sourceComponents, componentsDir, relPath);
|
|
171
|
+
step("업데이트", relPath);
|
|
172
|
+
}
|
|
173
|
+
for (const relPath of addQueue) {
|
|
174
|
+
copyFile(sourceComponents, componentsDir, relPath);
|
|
175
|
+
step("추가", relPath);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 5) 충돌 처리
|
|
179
|
+
let overwriteAll = force;
|
|
180
|
+
let keepAll = false;
|
|
181
|
+
const kept = [];
|
|
182
|
+
const overwritten = [];
|
|
183
|
+
for (const item of conflictQueue) {
|
|
184
|
+
const { relPath, userHash, bundleHash } = item;
|
|
185
|
+
if (overwriteAll) {
|
|
186
|
+
copyFile(sourceComponents, componentsDir, relPath);
|
|
187
|
+
overwritten.push(relPath);
|
|
188
|
+
step("덮어쓰기 (force)", relPath);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (keepAll) {
|
|
192
|
+
kept.push(relPath);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let decided = false;
|
|
197
|
+
while (!decided) {
|
|
198
|
+
const choice = await askConflict(relPath);
|
|
199
|
+
if (choice === "y") {
|
|
200
|
+
copyFile(sourceComponents, componentsDir, relPath);
|
|
201
|
+
overwritten.push(relPath);
|
|
202
|
+
decided = true;
|
|
203
|
+
} else if (choice === "n") {
|
|
204
|
+
kept.push(relPath);
|
|
205
|
+
decided = true;
|
|
206
|
+
} else if (choice === "d") {
|
|
207
|
+
const userContent = readFileSync(join(componentsDir, relPath), "utf8");
|
|
208
|
+
const bundleContent = readFileSync(join(sourceComponents, relPath), "utf8");
|
|
209
|
+
console.log("");
|
|
210
|
+
console.log(renderDiff(userContent, bundleContent, { label: relPath }));
|
|
211
|
+
} else if (choice === "A") {
|
|
212
|
+
copyFile(sourceComponents, componentsDir, relPath);
|
|
213
|
+
overwritten.push(relPath);
|
|
214
|
+
overwriteAll = true;
|
|
215
|
+
decided = true;
|
|
216
|
+
} else if (choice === "N") {
|
|
217
|
+
kept.push(relPath);
|
|
218
|
+
keepAll = true;
|
|
219
|
+
decided = true;
|
|
220
|
+
} else if (choice === "q") {
|
|
221
|
+
console.log("");
|
|
222
|
+
warn("중단됨. 이미 처리된 파일은 유지됩니다.");
|
|
223
|
+
_finalReport(overwritten, kept, addQueue, safeQueue);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 6) 보조 자산 — 조용히 덮어쓰기
|
|
230
|
+
for (const [src, destAbs, label] of [
|
|
231
|
+
[assets.prompts, promptsDir, "prompts"],
|
|
232
|
+
[assets.skill, skillDir, "skill"],
|
|
233
|
+
[assets.publicAssets, publicDir, "onboarding"],
|
|
234
|
+
]) {
|
|
235
|
+
if (!existsSync(src)) {
|
|
236
|
+
warn(`자산 누락 (스킵): ${label}`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
rmSyncIfExists(destAbs);
|
|
240
|
+
mkdirSync(destAbs, { recursive: true });
|
|
241
|
+
cpSync(src, destAbs, { recursive: true });
|
|
242
|
+
step("덮어쓰기", label);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 7) meta 재생성
|
|
246
|
+
writeMeta(componentsDir, rebuildMeta(componentsDir, meta, readCliVersion()));
|
|
247
|
+
step("메타 갱신", `${componentsDir.replace(cwd + sep, "")}${sep}${META_NAME}`);
|
|
248
|
+
|
|
249
|
+
console.log("");
|
|
250
|
+
_finalReport(overwritten, kept, addQueue, safeQueue);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 번들 소스에서 사용자 플젝으로 파일 하나 복사. 디렉토리 자동 생성. */
|
|
254
|
+
function copyFile(sourceRoot, destRoot, relPath) {
|
|
255
|
+
const src = join(sourceRoot, relPath);
|
|
256
|
+
const dst = join(destRoot, relPath);
|
|
257
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
258
|
+
cpSync(src, dst);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function rmSyncIfExists(path) {
|
|
262
|
+
if (existsSync(path)) rmSync(path, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function _finalReport(overwritten, kept, addQueue, safeQueue) {
|
|
266
|
+
console.log(c.bold("완료"));
|
|
267
|
+
success(`${safeQueue.length + overwritten.length}개 업데이트`);
|
|
268
|
+
if (addQueue.length > 0) success(`${addQueue.length}개 추가`);
|
|
269
|
+
if (kept.length > 0) {
|
|
270
|
+
warn(`${kept.length}개 유지 (사용자 수정 보존):`);
|
|
271
|
+
for (const p of kept) console.log(` · ${p}`);
|
|
272
|
+
console.log("");
|
|
273
|
+
info("나중에 수동으로 머지하려면: git diff --no-index <현재> <원본>");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI가 복사해줄 자산(components/, prompts/, .claude/skills/ctds/)의 루트 경로.
|
|
3
|
+
*
|
|
4
|
+
* 두 가지 상황을 지원:
|
|
5
|
+
* 1) 로컬 개발 — `npm link`로 심링크된 상태. 이 파일 → ../../assets가 비어있을 수 있어
|
|
6
|
+
* repo root로 올라가 ../../web/components, ../../prompts 를 참조.
|
|
7
|
+
* 2) publish 후 — publish 전 `bundle-assets` 스크립트가 자산을 packages/ctds-cli/assets/
|
|
8
|
+
* 로 복사해놓음. 이때는 assets/components, assets/prompts 가 존재.
|
|
9
|
+
*
|
|
10
|
+
* 우선순위: bundled assets → repo fallback.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
export function getAssetRoot() {
|
|
20
|
+
const cliRoot = join(__dirname, "..", "..");
|
|
21
|
+
|
|
22
|
+
// 1) bundled
|
|
23
|
+
const bundledAssets = join(cliRoot, "assets");
|
|
24
|
+
if (existsSync(join(bundledAssets, "components"))) {
|
|
25
|
+
return bundledAssets;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2) local repo fallback: packages/ctds-cli → repo root
|
|
29
|
+
const repoRoot = join(cliRoot, "..", "..");
|
|
30
|
+
return makeRepoView(repoRoot);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* repo 소스를 "bundled 구조"처럼 보이게 하는 뷰 경로 매핑.
|
|
35
|
+
* cp 할 때 join(assetRoot, "components") 같은 식으로 조립하니까
|
|
36
|
+
* 실제 소스 경로가 뭐든 그 구조를 흉내내야 함.
|
|
37
|
+
*
|
|
38
|
+
* 간단한 해결: repoRoot를 그대로 돌려주되, caller가 "components" 대신
|
|
39
|
+
* "web/components"로 요청하도록 하면 안 돼. 그래서 symlink·실제 복사 대신
|
|
40
|
+
* **tagged root 객체**를 리턴하지 않고, assetRoot를 바탕으로 상대 경로 리스트를
|
|
41
|
+
* caller 쪽에서 적절히 재지정하는 게 깔끔.
|
|
42
|
+
*
|
|
43
|
+
* 지금은 간단히: repo fallback 모드에선 repoRoot를 돌려주고 caller는
|
|
44
|
+
* "web/components" 식 경로를 쓰도록 — getAssetMap()으로 분리.
|
|
45
|
+
*/
|
|
46
|
+
function makeRepoView(repoRoot) {
|
|
47
|
+
return repoRoot;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 자산 복사 매핑. 번들/레포 모드에 따라 소스 경로가 다름.
|
|
52
|
+
* returns: Array<[absoluteSrcDir, label]>
|
|
53
|
+
* label은 로그 표시용.
|
|
54
|
+
*/
|
|
55
|
+
export function getAssetSources() {
|
|
56
|
+
const cliRoot = join(__dirname, "..", "..");
|
|
57
|
+
const bundledAssets = join(cliRoot, "assets");
|
|
58
|
+
|
|
59
|
+
if (existsSync(join(bundledAssets, "components"))) {
|
|
60
|
+
return {
|
|
61
|
+
mode: "bundled",
|
|
62
|
+
components: join(bundledAssets, "components"),
|
|
63
|
+
prompts: join(bundledAssets, "prompts"),
|
|
64
|
+
skill: join(bundledAssets, "skill"),
|
|
65
|
+
publicAssets: join(bundledAssets, "public"),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// repo fallback
|
|
70
|
+
const repoRoot = join(cliRoot, "..", "..");
|
|
71
|
+
return {
|
|
72
|
+
mode: "repo",
|
|
73
|
+
components: join(repoRoot, "web", "components"),
|
|
74
|
+
prompts: join(repoRoot, "prompts"),
|
|
75
|
+
skill: join(repoRoot, ".claude", "skills", "ctds"),
|
|
76
|
+
publicAssets: join(repoRoot, "web", "public", "onboarding"),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CTDS 런타임 의존성 체크 + 설치 명령 생성.
|
|
3
|
+
* 사용자 플젝 package.json을 읽어 빠진 패키지만 안내.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
/** CTDS 컴포넌트가 실제로 import하는 외부 패키지. */
|
|
10
|
+
export const CTDS_RUNTIME_DEPS = {
|
|
11
|
+
"@phosphor-icons/react": "^2.1.10",
|
|
12
|
+
"solar-icon-set": "^2.0.1",
|
|
13
|
+
"lucide-react": "^1.47.0",
|
|
14
|
+
"@radix-ui/react-popover": "^1.1.23",
|
|
15
|
+
"@radix-ui/react-select": "^2.3.7",
|
|
16
|
+
"@radix-ui/react-tooltip": "^1.2.16",
|
|
17
|
+
"@radix-ui/react-context-menu": "^2.3.7",
|
|
18
|
+
"@radix-ui/react-slot": "^1.3.3",
|
|
19
|
+
"@dnd-kit/core": "^6.3.1",
|
|
20
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
21
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 사용자 플젝의 package.json을 읽어 CTDS가 필요로 하는 deps 중 빠진 것 리스트.
|
|
26
|
+
* @returns { missing: string[], installed: string[] }
|
|
27
|
+
*/
|
|
28
|
+
export function checkMissingDeps(projectRoot) {
|
|
29
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
30
|
+
if (!existsSync(pkgPath)) {
|
|
31
|
+
return { missing: Object.keys(CTDS_RUNTIME_DEPS), installed: [] };
|
|
32
|
+
}
|
|
33
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
34
|
+
const allDeps = {
|
|
35
|
+
...(pkg.dependencies ?? {}),
|
|
36
|
+
...(pkg.devDependencies ?? {}),
|
|
37
|
+
};
|
|
38
|
+
const missing = [];
|
|
39
|
+
const installed = [];
|
|
40
|
+
for (const name of Object.keys(CTDS_RUNTIME_DEPS)) {
|
|
41
|
+
if (allDeps[name]) installed.push(name);
|
|
42
|
+
else missing.push(name);
|
|
43
|
+
}
|
|
44
|
+
return { missing, installed };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** `npm install` 명령어 문자열 생성. */
|
|
48
|
+
export function renderInstallCmd(missing) {
|
|
49
|
+
if (missing.length === 0) return null;
|
|
50
|
+
return `npm install ${missing.join(" ")}`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js app router 플젝 감지.
|
|
3
|
+
* - package.json에 next 의존성이 있어야 함
|
|
4
|
+
* - app/ 또는 src/app/ 디렉토리가 있어야 함
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
export function detectProject(cwd) {
|
|
11
|
+
const pkgPath = join(cwd, "package.json");
|
|
12
|
+
if (!existsSync(pkgPath)) {
|
|
13
|
+
return { ok: false, reason: "package.json을 찾을 수 없습니다." };
|
|
14
|
+
}
|
|
15
|
+
let pkg;
|
|
16
|
+
try {
|
|
17
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
18
|
+
} catch (e) {
|
|
19
|
+
return { ok: false, reason: `package.json 파싱 실패: ${e.message}` };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hasNext =
|
|
23
|
+
(pkg.dependencies && pkg.dependencies.next) ||
|
|
24
|
+
(pkg.devDependencies && pkg.devDependencies.next);
|
|
25
|
+
if (!hasNext) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
reason: "package.json에 next 의존성이 없습니다. Next.js 플젝이 아닙니다.",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const useSrcDir = existsSync(join(cwd, "src", "app"));
|
|
33
|
+
const useRootDir = existsSync(join(cwd, "app"));
|
|
34
|
+
if (!useSrcDir && !useRootDir) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
reason: "app/ 또는 src/app/ 디렉토리를 찾을 수 없습니다. App Router 플젝이 아닙니다.",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
43
|
+
name: pkg.name ?? "(unnamed)",
|
|
44
|
+
useSrcDir,
|
|
45
|
+
appDir: useSrcDir ? "src/app" : "app",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 간단 line-by-line diff 포매터. 외부 의존성 없음.
|
|
3
|
+
*
|
|
4
|
+
* 전략: 두 파일을 라인 단위로 비교해 공통 접두어·접미어는 생략하고,
|
|
5
|
+
* 차이 블록만 `--- / +++` 형태로 출력. maxLines로 cap, 넘으면 "... (N more lines)".
|
|
6
|
+
*
|
|
7
|
+
* 실제 LCS diff가 아니라 "변경된 라인 범위"만 보여주는 축약판.
|
|
8
|
+
* 완벽한 diff는 git diff --no-index를 유도 (가이드 문서에 명시).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { c } from "./log.js";
|
|
12
|
+
|
|
13
|
+
export function renderDiff(before, after, { label, maxLines = 50 } = {}) {
|
|
14
|
+
const b = before.split("\n");
|
|
15
|
+
const a = after.split("\n");
|
|
16
|
+
|
|
17
|
+
// 공통 접두어·접미어 길이 계산
|
|
18
|
+
let prefix = 0;
|
|
19
|
+
while (prefix < b.length && prefix < a.length && b[prefix] === a[prefix]) prefix++;
|
|
20
|
+
|
|
21
|
+
let suffix = 0;
|
|
22
|
+
while (
|
|
23
|
+
suffix < b.length - prefix &&
|
|
24
|
+
suffix < a.length - prefix &&
|
|
25
|
+
b[b.length - 1 - suffix] === a[a.length - 1 - suffix]
|
|
26
|
+
) {
|
|
27
|
+
suffix++;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const bChanged = b.slice(prefix, b.length - suffix);
|
|
31
|
+
const aChanged = a.slice(prefix, a.length - suffix);
|
|
32
|
+
|
|
33
|
+
const lines = [];
|
|
34
|
+
if (label) {
|
|
35
|
+
lines.push(c.dim(`--- ${label} (현재)`));
|
|
36
|
+
lines.push(c.dim(`+++ ${label} (ctds 최신)`));
|
|
37
|
+
}
|
|
38
|
+
lines.push(c.dim(`@@ line ${prefix + 1} @@`));
|
|
39
|
+
|
|
40
|
+
const changed = [];
|
|
41
|
+
for (const l of bChanged) changed.push(c.red(`- ${l}`));
|
|
42
|
+
for (const l of aChanged) changed.push(c.green(`+ ${l}`));
|
|
43
|
+
|
|
44
|
+
if (changed.length <= maxLines) {
|
|
45
|
+
lines.push(...changed);
|
|
46
|
+
} else {
|
|
47
|
+
lines.push(...changed.slice(0, maxLines));
|
|
48
|
+
lines.push(c.dim(`... (${changed.length - maxLines} more lines, 전체는 git diff로 확인)`));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHA-256 해시 유틸. 외부 의존성 없이 Node 내장 crypto만 사용.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
/** 파일 경로 → sha256 hex. */
|
|
9
|
+
export function hashFile(path) {
|
|
10
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 문자열 → sha256 hex. */
|
|
14
|
+
export function hashString(s) {
|
|
15
|
+
return createHash("sha256").update(s, "utf8").digest("hex");
|
|
16
|
+
}
|
package/src/utils/log.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 미니멀 로그 헬퍼. ANSI 색 지원 (TTY에서만).
|
|
3
|
+
* 외부 의존성 없이 cross-platform.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const isTTY = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
7
|
+
|
|
8
|
+
function wrap(code) {
|
|
9
|
+
return (s) => (isTTY ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const c = {
|
|
13
|
+
bold: wrap(1),
|
|
14
|
+
dim: wrap(2),
|
|
15
|
+
red: wrap(31),
|
|
16
|
+
green: wrap(32),
|
|
17
|
+
yellow: wrap(33),
|
|
18
|
+
blue: wrap(34),
|
|
19
|
+
cyan: wrap(36),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function step(verb, msg) {
|
|
23
|
+
console.log(` ${c.cyan("•")} ${c.dim(verb)} ${msg}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function success(msg) {
|
|
27
|
+
console.log(` ${c.green("✓")} ${msg}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function warn(msg) {
|
|
31
|
+
console.log(` ${c.yellow("!")} ${msg}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function info(msg) {
|
|
35
|
+
console.log(` ${c.blue("i")} ${msg}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 키-값 라인 (들여쓰기 2칸, 키는 dim) */
|
|
39
|
+
export function kv(key, val) {
|
|
40
|
+
console.log(` ${c.dim(key.padEnd(14))} ${val}`);
|
|
41
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .ctds-meta.json 읽기/쓰기 + 파일 트리 해시 맵 계산.
|
|
3
|
+
*
|
|
4
|
+
* 메타 포맷:
|
|
5
|
+
* {
|
|
6
|
+
* "version": "0.1.0",
|
|
7
|
+
* "installedAt": "ISO-8601",
|
|
8
|
+
* "updatedAt": "ISO-8601",
|
|
9
|
+
* "files": { "<상대경로>": "sha256:<hex>", ... }
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* 파일 경로는 메타 파일이 담긴 디렉토리 기준 상대 경로 (POSIX 슬래시).
|
|
13
|
+
* .ctds-meta.json 자신은 files 맵에서 제외.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { join, relative, sep } from "node:path";
|
|
18
|
+
import { hashFile } from "./hash.js";
|
|
19
|
+
|
|
20
|
+
const META_NAME = ".ctds-meta.json";
|
|
21
|
+
|
|
22
|
+
export function metaPath(dir) {
|
|
23
|
+
return join(dir, META_NAME);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 메타 로드. 없으면 null. */
|
|
27
|
+
export function loadMeta(dir) {
|
|
28
|
+
const p = metaPath(dir);
|
|
29
|
+
if (!existsSync(p)) return null;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 메타 쓰기. updatedAt 자동 갱신. */
|
|
38
|
+
export function writeMeta(dir, meta) {
|
|
39
|
+
const next = { ...meta, updatedAt: new Date().toISOString() };
|
|
40
|
+
writeFileSync(metaPath(dir), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 디렉토리 재귀 walk → { 상대경로(posix): "sha256:hex" } 맵. 메타 파일 자체는 제외. */
|
|
44
|
+
export function computeFilesMap(dir) {
|
|
45
|
+
const out = {};
|
|
46
|
+
walk(dir, dir, out);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function walk(rootDir, currentDir, out) {
|
|
51
|
+
for (const name of readdirSync(currentDir)) {
|
|
52
|
+
if (name === META_NAME) continue;
|
|
53
|
+
const full = join(currentDir, name);
|
|
54
|
+
const st = statSync(full);
|
|
55
|
+
if (st.isDirectory()) {
|
|
56
|
+
walk(rootDir, full, out);
|
|
57
|
+
} else if (st.isFile()) {
|
|
58
|
+
const rel = relative(rootDir, full).split(sep).join("/");
|
|
59
|
+
out[rel] = `sha256:${hashFile(full)}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** version·installedAt 유지, files·updatedAt만 재계산한 새 메타 생성. */
|
|
65
|
+
export function rebuildMeta(dir, prevMeta, version) {
|
|
66
|
+
const now = new Date().toISOString();
|
|
67
|
+
return {
|
|
68
|
+
version,
|
|
69
|
+
installedAt: prevMeta?.installedAt ?? now,
|
|
70
|
+
updatedAt: now,
|
|
71
|
+
files: computeFilesMap(dir),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 대화형 prompt — readline 기반. 외부 의존성 없음.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
7
|
+
|
|
8
|
+
const VALID = new Set(["y", "n", "d", "A", "N", "q"]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 충돌 파일에 대해 사용자 선택을 받음.
|
|
12
|
+
* 반환: 'y' | 'n' | 'd' | 'A' | 'N' | 'q'
|
|
13
|
+
*/
|
|
14
|
+
export async function askConflict(relPath) {
|
|
15
|
+
const rl = createInterface({ input, output });
|
|
16
|
+
try {
|
|
17
|
+
while (true) {
|
|
18
|
+
console.log("");
|
|
19
|
+
console.log(` ${relPath} 가 수정됐어요.`);
|
|
20
|
+
console.log(` (y) 덮어쓰기 (n) 유지 (d) diff 보기 (A) 남은 전부 y (N) 남은 전부 n (q) 중단`);
|
|
21
|
+
const answer = (await rl.question(" > ")).trim();
|
|
22
|
+
if (VALID.has(answer)) return answer;
|
|
23
|
+
console.log(" (올바른 선택지: y / n / d / A / N / q)");
|
|
24
|
+
}
|
|
25
|
+
} finally {
|
|
26
|
+
rl.close();
|
|
27
|
+
}
|
|
28
|
+
}
|