jun-claude-code 0.6.2 → 0.6.4

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const crypto = __importStar(require("crypto"));
41
+ const metadata_1 = require("../metadata");
42
+ function createTmpDir() {
43
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'metadata-test-'));
44
+ }
45
+ function cleanupDir(dir) {
46
+ fs.rmSync(dir, { recursive: true, force: true });
47
+ }
48
+ function writeFile(dir, relativePath, content) {
49
+ const filePath = path.join(dir, relativePath);
50
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
+ fs.writeFileSync(filePath, content, 'utf-8');
52
+ }
53
+ function hashContent(content) {
54
+ return crypto.createHash('sha256').update(Buffer.from(content)).digest('hex');
55
+ }
56
+ (0, vitest_1.describe)('metadata', () => {
57
+ let tmpDir;
58
+ (0, vitest_1.beforeEach)(() => {
59
+ tmpDir = createTmpDir();
60
+ });
61
+ (0, vitest_1.afterEach)(() => {
62
+ cleanupDir(tmpDir);
63
+ });
64
+ (0, vitest_1.describe)('getMetadataPath', () => {
65
+ (0, vitest_1.it)('should return correct path', () => {
66
+ (0, vitest_1.expect)((0, metadata_1.getMetadataPath)('/home/user/.claude')).toBe('/home/user/.claude/.jun-installed.json');
67
+ });
68
+ });
69
+ (0, vitest_1.describe)('loadMetadata', () => {
70
+ (0, vitest_1.it)('should return null when file does not exist', () => {
71
+ (0, vitest_1.expect)((0, metadata_1.loadMetadata)(tmpDir)).toBeNull();
72
+ });
73
+ (0, vitest_1.it)('should return null when file is invalid JSON', () => {
74
+ fs.writeFileSync(path.join(tmpDir, '.jun-installed.json'), 'not json', 'utf-8');
75
+ (0, vitest_1.expect)((0, metadata_1.loadMetadata)(tmpDir)).toBeNull();
76
+ });
77
+ (0, vitest_1.it)('should return metadata when valid', () => {
78
+ const meta = {
79
+ version: '1.0.0',
80
+ installedAt: '2025-01-01T00:00:00.000Z',
81
+ files: { 'agents/test.md': { hash: 'abc123' } },
82
+ };
83
+ fs.writeFileSync(path.join(tmpDir, '.jun-installed.json'), JSON.stringify(meta), 'utf-8');
84
+ (0, vitest_1.expect)((0, metadata_1.loadMetadata)(tmpDir)).toEqual(meta);
85
+ });
86
+ });
87
+ (0, vitest_1.describe)('saveMetadata', () => {
88
+ (0, vitest_1.it)('should write metadata as formatted JSON', () => {
89
+ const meta = {
90
+ version: '1.0.0',
91
+ installedAt: '2025-01-01T00:00:00.000Z',
92
+ files: { 'agents/test.md': { hash: 'abc123' } },
93
+ };
94
+ (0, metadata_1.saveMetadata)(tmpDir, meta);
95
+ const content = fs.readFileSync(path.join(tmpDir, '.jun-installed.json'), 'utf-8');
96
+ (0, vitest_1.expect)(JSON.parse(content)).toEqual(meta);
97
+ (0, vitest_1.expect)(content.endsWith('\n')).toBe(true);
98
+ });
99
+ });
100
+ (0, vitest_1.describe)('buildMetadata', () => {
101
+ (0, vitest_1.it)('should create metadata with correct hashes', () => {
102
+ const sourceDir = createTmpDir();
103
+ try {
104
+ writeFile(sourceDir, 'agents/test.md', 'hello');
105
+ writeFile(sourceDir, 'skills/Git/SKILL.md', 'world');
106
+ const meta = (0, metadata_1.buildMetadata)(['agents/test.md', 'skills/Git/SKILL.md'], sourceDir, '1.0.0');
107
+ (0, vitest_1.expect)(meta.version).toBe('1.0.0');
108
+ (0, vitest_1.expect)(meta.files['agents/test.md'].hash).toBe(hashContent('hello'));
109
+ (0, vitest_1.expect)(meta.files['skills/Git/SKILL.md'].hash).toBe(hashContent('world'));
110
+ }
111
+ finally {
112
+ cleanupDir(sourceDir);
113
+ }
114
+ });
115
+ (0, vitest_1.it)('should skip non-existent files', () => {
116
+ const sourceDir = createTmpDir();
117
+ try {
118
+ writeFile(sourceDir, 'agents/test.md', 'hello');
119
+ const meta = (0, metadata_1.buildMetadata)(['agents/test.md', 'agents/missing.md'], sourceDir, '1.0.0');
120
+ (0, vitest_1.expect)(Object.keys(meta.files)).toHaveLength(1);
121
+ (0, vitest_1.expect)(meta.files['agents/test.md']).toBeDefined();
122
+ (0, vitest_1.expect)(meta.files['agents/missing.md']).toBeUndefined();
123
+ }
124
+ finally {
125
+ cleanupDir(sourceDir);
126
+ }
127
+ });
128
+ });
129
+ (0, vitest_1.describe)('mergeMetadata', () => {
130
+ (0, vitest_1.it)('should create new metadata when existing is null', () => {
131
+ const sourceDir = createTmpDir();
132
+ try {
133
+ writeFile(sourceDir, 'agents/test.md', 'hello');
134
+ const meta = (0, metadata_1.mergeMetadata)(null, ['agents/test.md'], sourceDir, '1.0.0');
135
+ (0, vitest_1.expect)(meta.version).toBe('1.0.0');
136
+ (0, vitest_1.expect)(meta.files['agents/test.md'].hash).toBe(hashContent('hello'));
137
+ }
138
+ finally {
139
+ cleanupDir(sourceDir);
140
+ }
141
+ });
142
+ (0, vitest_1.it)('should preserve existing entries and add new ones', () => {
143
+ const sourceDir = createTmpDir();
144
+ try {
145
+ writeFile(sourceDir, 'agents/new.md', 'new content');
146
+ const existing = {
147
+ version: '0.9.0',
148
+ installedAt: '2025-01-01T00:00:00.000Z',
149
+ files: { 'agents/old.md': { hash: 'oldhash' } },
150
+ };
151
+ const meta = (0, metadata_1.mergeMetadata)(existing, ['agents/new.md'], sourceDir, '1.0.0');
152
+ (0, vitest_1.expect)(meta.version).toBe('1.0.0');
153
+ (0, vitest_1.expect)(meta.files['agents/old.md'].hash).toBe('oldhash');
154
+ (0, vitest_1.expect)(meta.files['agents/new.md'].hash).toBe(hashContent('new content'));
155
+ }
156
+ finally {
157
+ cleanupDir(sourceDir);
158
+ }
159
+ });
160
+ (0, vitest_1.it)('should override existing entries for the same file', () => {
161
+ const sourceDir = createTmpDir();
162
+ try {
163
+ writeFile(sourceDir, 'agents/test.md', 'updated content');
164
+ const existing = {
165
+ version: '0.9.0',
166
+ installedAt: '2025-01-01T00:00:00.000Z',
167
+ files: { 'agents/test.md': { hash: 'oldhash' } },
168
+ };
169
+ const meta = (0, metadata_1.mergeMetadata)(existing, ['agents/test.md'], sourceDir, '1.0.0');
170
+ (0, vitest_1.expect)(meta.files['agents/test.md'].hash).toBe(hashContent('updated content'));
171
+ (0, vitest_1.expect)(meta.files['agents/test.md'].hash).not.toBe('oldhash');
172
+ }
173
+ finally {
174
+ cleanupDir(sourceDir);
175
+ }
176
+ });
177
+ });
178
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const crypto = __importStar(require("crypto"));
41
+ const update_1 = require("../update");
42
+ function createTmpDir() {
43
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'update-test-'));
44
+ }
45
+ function cleanupDir(dir) {
46
+ fs.rmSync(dir, { recursive: true, force: true });
47
+ }
48
+ function writeFile(dir, relativePath, content) {
49
+ const filePath = path.join(dir, relativePath);
50
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
+ fs.writeFileSync(filePath, content, 'utf-8');
52
+ }
53
+ function hashContent(content) {
54
+ return crypto.createHash('sha256').update(Buffer.from(content)).digest('hex');
55
+ }
56
+ function createMetadata(files, version = '1.0.0') {
57
+ const fileEntries = {};
58
+ for (const [name, content] of Object.entries(files)) {
59
+ fileEntries[name] = { hash: hashContent(content) };
60
+ }
61
+ return {
62
+ version,
63
+ installedAt: '2025-01-01T00:00:00.000Z',
64
+ files: fileEntries,
65
+ };
66
+ }
67
+ (0, vitest_1.describe)('computeUpdateStatus', () => {
68
+ let sourceDir;
69
+ let destDir;
70
+ (0, vitest_1.beforeEach)(() => {
71
+ sourceDir = createTmpDir();
72
+ destDir = createTmpDir();
73
+ });
74
+ (0, vitest_1.afterEach)(() => {
75
+ cleanupDir(sourceDir);
76
+ cleanupDir(destDir);
77
+ });
78
+ (0, vitest_1.it)('should return new-file when no base and no current file exists', () => {
79
+ writeFile(sourceDir, 'agents/new.md', 'template content');
80
+ const status = (0, update_1.computeUpdateStatus)('agents/new.md', sourceDir, destDir, null);
81
+ (0, vitest_1.expect)(status).toBe('new-file');
82
+ });
83
+ (0, vitest_1.it)('should return new-file when base exists but user deleted file', () => {
84
+ writeFile(sourceDir, 'agents/deleted.md', 'template content');
85
+ const metadata = createMetadata({ 'agents/deleted.md': 'old content' });
86
+ const status = (0, update_1.computeUpdateStatus)('agents/deleted.md', sourceDir, destDir, metadata);
87
+ (0, vitest_1.expect)(status).toBe('new-file');
88
+ });
89
+ (0, vitest_1.it)('should return unchanged when no base but current matches new template', () => {
90
+ const content = 'same content';
91
+ writeFile(sourceDir, 'agents/test.md', content);
92
+ writeFile(destDir, 'agents/test.md', content);
93
+ const status = (0, update_1.computeUpdateStatus)('agents/test.md', sourceDir, destDir, null);
94
+ (0, vitest_1.expect)(status).toBe('unchanged');
95
+ });
96
+ (0, vitest_1.it)('should return user-modified for legacy file (no base, current differs from new)', () => {
97
+ writeFile(sourceDir, 'agents/test.md', 'new template');
98
+ writeFile(destDir, 'agents/test.md', 'user customized');
99
+ const status = (0, update_1.computeUpdateStatus)('agents/test.md', sourceDir, destDir, null);
100
+ (0, vitest_1.expect)(status).toBe('user-modified');
101
+ });
102
+ (0, vitest_1.it)('should return unchanged when all three hashes match', () => {
103
+ const content = 'same for all';
104
+ writeFile(sourceDir, 'agents/test.md', content);
105
+ writeFile(destDir, 'agents/test.md', content);
106
+ const metadata = createMetadata({ 'agents/test.md': content });
107
+ const status = (0, update_1.computeUpdateStatus)('agents/test.md', sourceDir, destDir, metadata);
108
+ (0, vitest_1.expect)(status).toBe('unchanged');
109
+ });
110
+ (0, vitest_1.it)('should return update-available when base==current but new is different', () => {
111
+ const originalContent = 'original';
112
+ const newContent = 'updated template';
113
+ writeFile(sourceDir, 'agents/test.md', newContent);
114
+ writeFile(destDir, 'agents/test.md', originalContent);
115
+ const metadata = createMetadata({ 'agents/test.md': originalContent });
116
+ const status = (0, update_1.computeUpdateStatus)('agents/test.md', sourceDir, destDir, metadata);
117
+ (0, vitest_1.expect)(status).toBe('update-available');
118
+ });
119
+ (0, vitest_1.it)('should return user-modified when base!=current but base==new', () => {
120
+ const originalContent = 'original';
121
+ const userContent = 'user changed this';
122
+ writeFile(sourceDir, 'agents/test.md', originalContent);
123
+ writeFile(destDir, 'agents/test.md', userContent);
124
+ const metadata = createMetadata({ 'agents/test.md': originalContent });
125
+ const status = (0, update_1.computeUpdateStatus)('agents/test.md', sourceDir, destDir, metadata);
126
+ (0, vitest_1.expect)(status).toBe('user-modified');
127
+ });
128
+ (0, vitest_1.it)('should return conflict when base!=current and base!=new', () => {
129
+ const originalContent = 'original';
130
+ const userContent = 'user changed this';
131
+ const newContent = 'template also changed';
132
+ writeFile(sourceDir, 'agents/test.md', newContent);
133
+ writeFile(destDir, 'agents/test.md', userContent);
134
+ const metadata = createMetadata({ 'agents/test.md': originalContent });
135
+ const status = (0, update_1.computeUpdateStatus)('agents/test.md', sourceDir, destDir, metadata);
136
+ (0, vitest_1.expect)(status).toBe('conflict');
137
+ });
138
+ (0, vitest_1.it)('should return unchanged when base!=current but current==new (user applied same change)', () => {
139
+ const originalContent = 'original';
140
+ const newContent = 'both changed to same';
141
+ writeFile(sourceDir, 'agents/test.md', newContent);
142
+ writeFile(destDir, 'agents/test.md', newContent);
143
+ const metadata = createMetadata({ 'agents/test.md': originalContent });
144
+ const status = (0, update_1.computeUpdateStatus)('agents/test.md', sourceDir, destDir, metadata);
145
+ // base != current, base != new → conflict (even though current == new)
146
+ (0, vitest_1.expect)(status).toBe('conflict');
147
+ });
148
+ (0, vitest_1.it)('should handle metadata with no entry for the file', () => {
149
+ writeFile(sourceDir, 'agents/new.md', 'template content');
150
+ const metadata = createMetadata({ 'agents/other.md': 'other content' });
151
+ const status = (0, update_1.computeUpdateStatus)('agents/new.md', sourceDir, destDir, metadata);
152
+ (0, vitest_1.expect)(status).toBe('new-file');
153
+ });
154
+ });
package/dist/cli.js CHANGED
@@ -105,6 +105,31 @@ program
105
105
  process.exit(1);
106
106
  }
107
107
  });
108
+ program
109
+ .command('update')
110
+ .description('Update installed templates while preserving user customizations')
111
+ .option('-p, --project', 'Update project .claude/')
112
+ .option('-f, --force', 'Overwrite all including user-modified')
113
+ .option('-d, --dry-run', 'Preview changes without applying')
114
+ .action(async (options) => {
115
+ try {
116
+ const { updateClaudeFiles } = await Promise.resolve().then(() => __importStar(require('./update')));
117
+ await updateClaudeFiles({
118
+ dryRun: options.dryRun,
119
+ force: options.force,
120
+ project: options.project,
121
+ });
122
+ }
123
+ catch (error) {
124
+ if (error instanceof Error) {
125
+ console.error('Error:', error.message);
126
+ }
127
+ else {
128
+ console.error('An unexpected error occurred');
129
+ }
130
+ process.exit(1);
131
+ }
132
+ });
108
133
  program
109
134
  .command('validate')
110
135
  .description('Validate template directory structure')
package/dist/copy.d.ts CHANGED
@@ -3,6 +3,42 @@ export interface CopyOptions {
3
3
  force?: boolean;
4
4
  project?: boolean;
5
5
  }
6
+ export type FileStatus = 'new' | 'changed' | 'unchanged';
7
+ export interface CategorizedFiles {
8
+ agents: string[];
9
+ skills: string[];
10
+ others: string[];
11
+ }
12
+ export declare const EXCLUDE_ALWAYS: string[];
13
+ export declare const EXCLUDE_FROM_PROJECT: string[];
14
+ /**
15
+ * Calculate SHA-256 hash of a file
16
+ */
17
+ export declare function getFileHash(filePath: string): string;
18
+ /**
19
+ * Get all files recursively from a directory
20
+ */
21
+ export declare function getAllFiles(dirPath: string, basePath?: string): string[];
22
+ /**
23
+ * Ensure directory exists
24
+ */
25
+ export declare function ensureDir(dirPath: string): void;
26
+ /**
27
+ * Copy a single file
28
+ */
29
+ export declare function copyFile(src: string, dest: string): void;
30
+ /**
31
+ * Get the source templates/global directory path (from package installation)
32
+ */
33
+ export declare function getSourceGlobalDir(): string;
34
+ /**
35
+ * Get the destination .claude directory path (user's home)
36
+ */
37
+ export declare function getDestClaudeDir(): string;
38
+ /**
39
+ * Categorize files into agents, skills (top-level dirs), and others
40
+ */
41
+ export declare function categorizeFiles(files: string[]): CategorizedFiles;
6
42
  /**
7
43
  * Merge settings.json from source into destination.
8
44
  * Hooks are merged per event key; duplicate hook entries (by deep equality) are skipped.
package/dist/copy.js CHANGED
@@ -36,14 +36,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.EXCLUDE_FROM_PROJECT = exports.EXCLUDE_ALWAYS = void 0;
40
+ exports.getFileHash = getFileHash;
41
+ exports.getAllFiles = getAllFiles;
42
+ exports.ensureDir = ensureDir;
43
+ exports.copyFile = copyFile;
44
+ exports.getSourceGlobalDir = getSourceGlobalDir;
45
+ exports.getDestClaudeDir = getDestClaudeDir;
46
+ exports.categorizeFiles = categorizeFiles;
39
47
  exports.mergeSettingsJson = mergeSettingsJson;
40
48
  exports.copyClaudeFiles = copyClaudeFiles;
41
49
  const fs = __importStar(require("fs"));
42
50
  const path = __importStar(require("path"));
43
51
  const crypto = __importStar(require("crypto"));
44
52
  const chalk_1 = __importDefault(require("chalk"));
53
+ const metadata_1 = require("./metadata");
45
54
  // eslint-disable-next-line @typescript-eslint/no-var-requires
46
55
  const { MultiSelect } = require('enquirer');
56
+ // Files to exclude from all copies (merge-handled separately)
57
+ exports.EXCLUDE_ALWAYS = [
58
+ 'settings.json',
59
+ 'statusline-command.sh',
60
+ ];
61
+ // Files to exclude only when installing to project
62
+ exports.EXCLUDE_FROM_PROJECT = [
63
+ 'hooks/_dedup.sh',
64
+ ];
47
65
  /**
48
66
  * Calculate SHA-256 hash of a file
49
67
  */
@@ -387,21 +405,12 @@ async function copyClaudeFiles(options = {}) {
387
405
  console.error(chalk_1.default.red('Error:'), 'Source templates/global directory not found');
388
406
  process.exit(1);
389
407
  }
390
- // Files to exclude from all copies (merge-handled separately)
391
- const EXCLUDE_ALWAYS = [
392
- 'settings.json',
393
- 'statusline-command.sh',
394
- ];
395
- // Files to exclude only when installing to project
396
- const EXCLUDE_FROM_PROJECT = [
397
- 'hooks/_dedup.sh',
398
- ];
399
408
  // Get all files to copy
400
409
  const allFiles = getAllFiles(sourceDir);
401
410
  const files = allFiles.filter((file) => {
402
- if (EXCLUDE_ALWAYS.includes(file))
411
+ if (exports.EXCLUDE_ALWAYS.includes(file))
403
412
  return false;
404
- if (project && EXCLUDE_FROM_PROJECT.includes(file))
413
+ if (project && exports.EXCLUDE_FROM_PROJECT.includes(file))
405
414
  return false;
406
415
  return true;
407
416
  });
@@ -524,6 +533,12 @@ async function copyClaudeFiles(options = {}) {
524
533
  const skippedCount = files.length - copiedCount;
525
534
  // Merge settings.json (hooks are merged, not overwritten)
526
535
  mergeSettingsJson(sourceDir, destDir, { project });
536
+ // Save installation metadata
537
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
538
+ const currentVersion = require('../package.json').version;
539
+ const existingMeta = (0, metadata_1.loadMetadata)(destDir);
540
+ const updatedMeta = (0, metadata_1.mergeMetadata)(existingMeta, allFilesToCopy, sourceDir, currentVersion);
541
+ (0, metadata_1.saveMetadata)(destDir, updatedMeta);
527
542
  console.log();
528
543
  console.log(chalk_1.default.green(`Done! Copied ${copiedCount} files, skipped ${skippedCount} files.`));
529
544
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { copyClaudeFiles, CopyOptions } from './copy';
2
+ export { updateClaudeFiles, UpdateOptions, UpdateFileStatus } from './update';
2
3
  export { initProject } from './init-project';
3
4
  export { initContext } from './init-context';
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.initContext = exports.initProject = exports.copyClaudeFiles = void 0;
3
+ exports.initContext = exports.initProject = exports.updateClaudeFiles = exports.copyClaudeFiles = void 0;
4
4
  var copy_1 = require("./copy");
5
5
  Object.defineProperty(exports, "copyClaudeFiles", { enumerable: true, get: function () { return copy_1.copyClaudeFiles; } });
6
+ var update_1 = require("./update");
7
+ Object.defineProperty(exports, "updateClaudeFiles", { enumerable: true, get: function () { return update_1.updateClaudeFiles; } });
6
8
  var init_project_1 = require("./init-project");
7
9
  Object.defineProperty(exports, "initProject", { enumerable: true, get: function () { return init_project_1.initProject; } });
8
10
  var init_context_1 = require("./init-context");
@@ -0,0 +1,29 @@
1
+ export interface FileMetadata {
2
+ hash: string;
3
+ }
4
+ export interface InstalledMetadata {
5
+ version: string;
6
+ installedAt: string;
7
+ files: Record<string, FileMetadata>;
8
+ }
9
+ /**
10
+ * Get metadata file path for a destination directory
11
+ */
12
+ export declare function getMetadataPath(destDir: string): string;
13
+ /**
14
+ * Load installation metadata. Returns null if not found or invalid.
15
+ */
16
+ export declare function loadMetadata(destDir: string): InstalledMetadata | null;
17
+ /**
18
+ * Save installation metadata
19
+ */
20
+ export declare function saveMetadata(destDir: string, metadata: InstalledMetadata): void;
21
+ /**
22
+ * Build metadata from a list of files in the source directory
23
+ */
24
+ export declare function buildMetadata(files: string[], sourceDir: string, version: string): InstalledMetadata;
25
+ /**
26
+ * Merge new file entries into existing metadata.
27
+ * Existing entries are preserved; new entries override matching keys.
28
+ */
29
+ export declare function mergeMetadata(existing: InstalledMetadata | null, newFiles: string[], sourceDir: string, version: string): InstalledMetadata;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getMetadataPath = getMetadataPath;
37
+ exports.loadMetadata = loadMetadata;
38
+ exports.saveMetadata = saveMetadata;
39
+ exports.buildMetadata = buildMetadata;
40
+ exports.mergeMetadata = mergeMetadata;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const crypto = __importStar(require("crypto"));
44
+ const METADATA_FILENAME = '.jun-installed.json';
45
+ /**
46
+ * Compute SHA-256 hash of a file
47
+ */
48
+ function computeFileHash(filePath) {
49
+ const content = fs.readFileSync(filePath);
50
+ return crypto.createHash('sha256').update(content).digest('hex');
51
+ }
52
+ /**
53
+ * Get metadata file path for a destination directory
54
+ */
55
+ function getMetadataPath(destDir) {
56
+ return path.join(destDir, METADATA_FILENAME);
57
+ }
58
+ /**
59
+ * Load installation metadata. Returns null if not found or invalid.
60
+ */
61
+ function loadMetadata(destDir) {
62
+ const metaPath = getMetadataPath(destDir);
63
+ if (!fs.existsSync(metaPath))
64
+ return null;
65
+ try {
66
+ return JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ /**
73
+ * Save installation metadata
74
+ */
75
+ function saveMetadata(destDir, metadata) {
76
+ const metaPath = getMetadataPath(destDir);
77
+ fs.writeFileSync(metaPath, JSON.stringify(metadata, null, 2) + '\n', 'utf-8');
78
+ }
79
+ /**
80
+ * Build metadata from a list of files in the source directory
81
+ */
82
+ function buildMetadata(files, sourceDir, version) {
83
+ const fileEntries = {};
84
+ for (const file of files) {
85
+ const filePath = path.join(sourceDir, file);
86
+ if (fs.existsSync(filePath)) {
87
+ fileEntries[file] = { hash: computeFileHash(filePath) };
88
+ }
89
+ }
90
+ return {
91
+ version,
92
+ installedAt: new Date().toISOString(),
93
+ files: fileEntries,
94
+ };
95
+ }
96
+ /**
97
+ * Merge new file entries into existing metadata.
98
+ * Existing entries are preserved; new entries override matching keys.
99
+ */
100
+ function mergeMetadata(existing, newFiles, sourceDir, version) {
101
+ const newMeta = buildMetadata(newFiles, sourceDir, version);
102
+ if (!existing)
103
+ return newMeta;
104
+ return {
105
+ version,
106
+ installedAt: new Date().toISOString(),
107
+ files: {
108
+ ...existing.files,
109
+ ...newMeta.files,
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,16 @@
1
+ import { InstalledMetadata } from './metadata';
2
+ export type UpdateFileStatus = 'update-available' | 'user-modified' | 'conflict' | 'new-file' | 'unchanged' | 'removed-upstream';
3
+ export interface UpdateOptions {
4
+ dryRun?: boolean;
5
+ force?: boolean;
6
+ project?: boolean;
7
+ }
8
+ /**
9
+ * Compute 3-way update status for a file.
10
+ * Compares base (from metadata), current (on disk), and new (from template).
11
+ */
12
+ export declare function computeUpdateStatus(file: string, sourceDir: string, destDir: string, metadata: InstalledMetadata | null): UpdateFileStatus;
13
+ /**
14
+ * Update installed templates while preserving user customizations
15
+ */
16
+ export declare function updateClaudeFiles(options?: UpdateOptions): Promise<void>;